diff --git a/.editorconfig b/.editorconfig index 12b66846f4c..069b618a1e6 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,19 +1,17 @@ # Top-most EditorConfig file root = true -# AutoFormat All Files [*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true +max_line_length = 80 -# Format Source Code -[*.{md,mdx,js,jsx,json,scss,hbs}] -charset = utf-8 -indent_style = space -indent_size = 2 -quote_type = single +[*.md] +trim_trailing_whitespace = false -# Format Configs -[.eslintignore,*rc] -indent_style = space -indent_size = 2 +[*.snap] +trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index a9b8dd3d172..00000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,64 +0,0 @@ -module.exports = { - root: true, - extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'], - parser: '@babel/eslint-parser', - env: { - browser: true, - es6: true, - node: true, - jest: true, - 'cypress/globals': true, - }, - plugins: ['cypress', 'react-hooks'], - rules: { - 'no-console': 'off', - semi: ['error', 'always'], - quotes: ['error', 'single'], - 'no-duplicate-imports': 'error', - 'react/jsx-uses-react': 'off', // no longer needed with new jsx transform - 'react/react-in-jsx-scope': 'off', // ditto - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - 'react/no-unknown-property': [ - 'error', - { - ignore: ['watch', 'align'], - }, - ], - }, - settings: { - react: { - version: 'detect', - }, - }, - overrides: [ - { files: ['src/**/*.jsx'] }, // eslint would lint .js only by default - { - files: ['**/*.mdx'], - extends: ['plugin:mdx/recommended'], - globals: { - Badge: true, - StackBlitzPreview: true, - }, - rules: { - semi: ['off'], - }, - settings: { - 'mdx/code-blocks': true, - }, - }, - { - files: ['**/*.mdx/*.{js,javascript}'], // we don't lint ts at the moment - rules: { - indent: ['error', 2], - quotes: ['error', 'single', { allowTemplateLiterals: true }], - 'no-undef': 'off', - 'no-unused-vars': 'off', - 'no-constant-condition': 'off', - 'no-useless-escape': 'off', - 'no-dupe-keys': 'off', - 'no-duplicate-imports': 'off', - }, - }, - ], -}; diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index d4ab933ab3d..44464c2df29 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -55,6 +55,26 @@ Run `git config user.email` to see your Git email, and verify it with your [GitH The [.editorconfig][6] in the root should ensure consistent formatting. Please make sure you've [installed the plugin][7] if your text editor needs one. +## Testing + +Run the full test suite (lint + Jest) with: + +```bash +yarn test +``` + +To run only Jest tests: + +```bash +yarn jest +``` + +To update snapshots after intentional UI changes: + +```bash +NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.mjs --updateSnapshot +``` + ## Branching Your Changes Making a branch in your fork for your contribution is helpful in the following ways: @@ -81,6 +101,9 @@ After getting some feedback, push to your fork branch and submit a pull request. suggest some changes or improvements or alternatives, but for small changes your pull request should be accepted and merged fairly quick. +> Before submitting a pull request, ensure your feature branch is up to date with the latest changes from the upstream `main` branch to avoid conflicts during review. +> You can go through this article to learn about [rebase technique][14] + Issue the PR to the [main][8] branch. > See [GitHub documentation][9] for more help. @@ -126,3 +149,4 @@ any time spent fixing typos or clarifying sections in the documentation. [10]: http://conventionalcommits.org/ [11]: https://github.com/conventional-changelog/standard-version [13]: https://yarnpkg.com/lang/en/docs/install +[14]: https://dev.to/matks/what-it-means-to-rebase-a-pull-request-submitted-on-github-5717 diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md index eb12b0bb8bc..fa1c0c41cb3 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE.md @@ -4,4 +4,5 @@ about: Create Contribution Issue. --- ## Summary -- [ ] Translate ```파일경로/파일이름.md``` + +- [ ] Translate `파일경로/파일이름.md` diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 707569d6ca2..4512fbb6ec3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,4 +1,4 @@ -## Summary +## Summary // Issues 링크 diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d7a566f8ba2..d57daac6ead 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,12 +10,18 @@ updates: ignore: - dependency-name: "react" - dependency-name: "react-dom" + - dependency-name: "react-router-dom" + - dependency-name: "@docsearch/react" + - dependency-name: "tailwindcss" groups: dependencies: patterns: - "*" - # Maintain dependencies for GitHub Actions - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" \ No newline at end of file + interval: "weekly" + groups: + dependencies: + patterns: + - "*" diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml index 938c158e40b..b5088960f1c 100644 --- a/.github/workflows/dependency-review.yml +++ b/.github/workflows/dependency-review.yml @@ -1,4 +1,4 @@ -name: 'Dependency Review' +name: "Dependency Review" on: [pull_request] permissions: @@ -8,7 +8,7 @@ jobs: dependency-review: runs-on: ubuntu-latest steps: - - name: 'Checkout Repository' - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 - - name: 'Dependency Review' - uses: actions/dependency-review-action@3c4e3dcb1aa7874d2c16be7d79418e9b7efd6261 # v4.8.2 + - name: "Checkout Repository" + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: "Dependency Review" + uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index bf3bfdaf949..34910c2aad0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -5,7 +5,7 @@ on: branches: - main schedule: - - cron: '0 0 * * *' + - cron: "0 0 * * *" jobs: deploy: name: Deploy Site @@ -15,10 +15,10 @@ jobs: node-version: [lts/*] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node-version }} cache: yarn @@ -35,7 +35,7 @@ jobs: - run: yarn lint:links - name: Deploy - uses: JamesIves/github-pages-deploy-action@4a3abc783e1a24aeb44c16e869ad83caf6b4cc23 # v4.7.4 + uses: JamesIves/github-pages-deploy-action@d92aa235d04922e8f08b40ce78cc5442fcfbfa2f # v4.8.0 with: token: ${{ secrets.GITHUB_TOKEN }} folder: dist diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index d04ec3a8f0e..68ac69523b1 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -16,10 +16,10 @@ jobs: node-version: [lts/*] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node-version }} cache: yarn @@ -35,10 +35,10 @@ jobs: node-version: [lts/*] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node-version }} cache: yarn @@ -55,10 +55,10 @@ jobs: node-version: [lts/*] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node-version }} cache: yarn @@ -81,10 +81,10 @@ jobs: node-version: [lts/*] runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 + uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 with: node-version: ${{ matrix.node-version }} cache: yarn @@ -95,7 +95,7 @@ jobs: uses: ./.github/actions/webpack-persistent-cache - name: Cypress run - uses: cypress-io/github-action@7ef72e250a9e564efb4ed4c2433971ada4cc38b4 # v6.10.4 + uses: cypress-io/github-action@bc22e01685c56e89e7813fd8e26f33dc47f87e15 # v7.1.5 with: browser: chrome config-file: cypress.config.js diff --git a/.gitignore b/.gitignore index b677660e7ac..1cf0f2e8b46 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules dist +**/feed.xml !examples/**/dist src/**/_*.json src/**/_*.mdx diff --git a/.markdownlint.json b/.markdownlint.json index af31573e941..f7c39cd9aae 100644 --- a/.markdownlint.json +++ b/.markdownlint.json @@ -13,6 +13,7 @@ "MD034": false, "MD036": false, "MD041": false, + "MD059": false, "no-hard-tabs": false, "whitespace": false } diff --git a/.markdownlintignore b/.markdownlintignore index 29415160452..d2dc8375ce1 100644 --- a/.markdownlintignore +++ b/.markdownlintignore @@ -1,3 +1,4 @@ node_modules src/content/loaders/_*.mdx src/content/plugins/_*.mdx +src/content/contribute/Governance-*.mdx diff --git a/.prettierignore b/.prettierignore index 4d6880d3d58..ae29e0284f9 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,3 @@ dist examples +src/content/contribute/Governance-*.mdx diff --git a/.vale.ini b/.vale.ini index c785d69dfae..714e3ac6504 100644 --- a/.vale.ini +++ b/.vale.ini @@ -7,7 +7,7 @@ StylesPath = .vale MinAlertLevel = warning [*.{md,mdx}] -BasedOnStyles = proselint +BasedOnStyles = proselint, Speciesism proselint.But = NO proselint.Typography = NO diff --git a/.vale/Speciesism/AnimalIdioms.yml b/.vale/Speciesism/AnimalIdioms.yml new file mode 100644 index 00000000000..d85fc0f0667 --- /dev/null +++ b/.vale/Speciesism/AnimalIdioms.yml @@ -0,0 +1,32 @@ +extends: substitution +message: "Consider using '%s' instead of '%s'. This phrase normalizes violence toward animals." +link: https://doi.org/10.1007/s43681-023-00380-w +level: warning +ignorecase: true +swap: + 'kill two birds with one stone': accomplish two things at once + 'killing two birds with one stone': accomplishing two things at once + 'killed two birds with one stone': accomplished two things at once + 'beat a dead horse': belabor the point + 'beating a dead horse': belaboring the point + 'flog a dead horse': belabor the point + 'flogging a dead horse': belaboring the point + 'bring home the bacon': bring home the results + 'bringing home the bacon': bringing home the results + 'brought home the bacon': brought home the results + 'more than one way to skin a cat': more than one way to solve this + 'many ways to skin a cat': many ways to approach this + 'let the cat out of the bag': reveal the secret + 'letting the cat out of the bag': revealing the secret + 'open a can of worms': create a complicated situation + 'opening a can of worms': creating a complicated situation + 'opened a can of worms': created a complicated situation + 'wild goose chase': pointless pursuit + 'take the bull by the horns': face the challenge head-on + 'taking the bull by the horns': facing the challenge head-on + 'took the bull by the horns': faced the challenge head-on + 'like shooting fish in a barrel': extremely easy + 'straight from the horse''s mouth': directly from the source + 'from the horse''s mouth': from a reliable source + 'whack-a-mole': recurring problem + 'whack a mole': recurring problem diff --git a/.vale/Speciesism/AnimalMetaphors.yml b/.vale/Speciesism/AnimalMetaphors.yml new file mode 100644 index 00000000000..6526f66f06f --- /dev/null +++ b/.vale/Speciesism/AnimalMetaphors.yml @@ -0,0 +1,14 @@ +extends: substitution +message: "Consider using '%s' instead of '%s'. This term references animals as objects or tools." +link: https://doi.org/10.1007/s43681-023-00380-w +level: warning +ignorecase: true +swap: + 'guinea pig': test subject + 'sacred cow': unquestioned belief + 'sacred cows': unquestioned beliefs + 'scapegoat(?:ed|ing)?': wrongly blamed + 'dog-eat-dog': ruthlessly competitive + 'dog eat dog': ruthlessly competitive + 'rat race': competitive grind + 'red herring': false lead diff --git a/.vale/Speciesism/TechTerminology.yml b/.vale/Speciesism/TechTerminology.yml new file mode 100644 index 00000000000..62949b5418a --- /dev/null +++ b/.vale/Speciesism/TechTerminology.yml @@ -0,0 +1,13 @@ +extends: substitution +message: "Consider using '%s' instead of '%s'. This technical term has a more precise alternative." +level: suggestion +ignorecase: true +swap: + 'canary deployment': progressive rollout + 'canary release': progressive rollout + 'canary test(?:ing)?': incremental testing + 'monkey[- ]?patch(?:ed|ing)?': runtime patch + 'duck[- ]?typ(?:ed|ing)': structural typing + 'dogfood(?:ing)?': self-hosting + 'eat(?:ing)? (?:your|our|their) own dogfood': self-testing + 'rubber duck(?:ing)? debugging': talk-through debugging diff --git a/.vale/proselint/AnimalLabels.yml b/.vale/proselint/AnimalLabels.yml index b92e06fcb45..c3fe221b672 100644 --- a/.vale/proselint/AnimalLabels.yml +++ b/.vale/proselint/AnimalLabels.yml @@ -4,45 +4,45 @@ level: error action: name: replace swap: - (?:bull|ox)-like: taurine + (?:bull|ox)-like: taurine (?:calf|veal)-like: vituline (?:crow|raven)-like: corvine (?:leopard|panther)-like: pardine - bird-like: avine - centipede-like: scolopendrine - crab-like: cancrine - crocodile-like: crocodiline - deer-like: damine - eagle-like: aquiline - earthworm-like: lumbricine - falcon-like: falconine - ferine: wild animal-like - fish-like: piscine - fox-like: vulpine - frog-like: ranine - goat-like: hircine - goose-like: anserine - gull-like: laridine - hare-like: leporine - hawk-like: accipitrine - hippopotamus-like: hippopotamine - lizard-like: lacertine - mongoose-like: viverrine - mouse-like: murine - ostrich-like: struthionine - peacock-like: pavonine - porcupine-like: hystricine - rattlesnake-like: crotaline - sable-like: zibeline - sheep-like: ovine - shrew-like: soricine - sparrow-like: passerine - swallow-like: hirundine - swine-like: suilline - tiger-like: tigrine - viper-like: viperine - vulture-like: vulturine - wasp-like: vespine - wolf-like: lupine - woodpecker-like: picine - zebra-like: zebrine + bird-like: avine + centipede-like: scolopendrine + crab-like: cancrine + crocodile-like: crocodiline + deer-like: damine + eagle-like: aquiline + earthworm-like: lumbricine + falcon-like: falconine + ferine: wild animal-like + fish-like: piscine + fox-like: vulpine + frog-like: ranine + goat-like: hircine + goose-like: anserine + gull-like: laridine + hare-like: leporine + hawk-like: accipitrine + hippopotamus-like: hippopotamine + lizard-like: lacertine + mongoose-like: viverrine + mouse-like: murine + ostrich-like: struthionine + peacock-like: pavonine + porcupine-like: hystricine + rattlesnake-like: crotaline + sable-like: zibeline + sheep-like: ovine + shrew-like: soricine + sparrow-like: passerine + swallow-like: hirundine + swine-like: suilline + tiger-like: tigrine + viper-like: viperine + vulture-like: vulturine + wasp-like: vespine + wolf-like: lupine + woodpecker-like: picine + zebra-like: zebrine diff --git a/.vale/proselint/DenizenLabels.yml b/.vale/proselint/DenizenLabels.yml index bc3dd8abba1..db1cdc967ff 100644 --- a/.vale/proselint/DenizenLabels.yml +++ b/.vale/proselint/DenizenLabels.yml @@ -4,49 +4,49 @@ ignorecase: false action: name: replace swap: - (?:Afrikaaner|Afrikander): Afrikaner - (?:Hong Kongite|Hong Kongian): Hong Konger - (?:Indianan|Indianian): Hoosier - (?:Michiganite|Michiganian): Michigander + (?:Afrikaaner|Afrikander): Afrikaner + (?:Hong Kongite|Hong Kongian): Hong Konger + (?:Indianan|Indianian): Hoosier + (?:Michiganite|Michiganian): Michigander (?:New Hampshireite|New Hampshireman): New Hampshirite - (?:Newcastlite|Newcastleite): Novocastrian - (?:Providencian|Providencer): Providentian - (?:Trentian|Trentonian): Tridentine - (?:Warsawer|Warsawian): Varsovian + (?:Newcastlite|Newcastleite): Novocastrian + (?:Providencian|Providencer): Providentian + (?:Trentian|Trentonian): Tridentine + (?:Warsawer|Warsawian): Varsovian (?:Wolverhamptonite|Wolverhamptonian): Wulfrunian - Alabaman: Alabamian - Albuquerquian: Albuquerquean - Anchoragite: Anchorageite - Arizonian: Arizonan - Arkansawyer: Arkansan - Belarusan: Belarusian - Cayman Islander: Caymanian - Coloradoan: Coloradan - Connecticuter: Nutmegger - Fairbanksian: Fairbanksan - Fort Worther: Fort Worthian - Grenadian: Grenadan - Halifaxer: Haligonian - Hartlepoolian: Hartlepudlian - Illinoisian: Illinoisan - Iowegian: Iowan - Leedsian: Leodenisian - Liverpoolian: Liverpudlian - Los Angelean: Angeleno - Manchesterian: Mancunian - Minneapolisian: Minneapolitan - Missouran: Missourian - Monacan: Monegasque - Neopolitan: Neapolitan - New Jerseyite: New Jerseyan - New Orleansian: New Orleanian - Oklahoma Citian: Oklahoma Cityan - Oklahomian: Oklahoman - Saudi Arabian: Saudi - Seattlite: Seattleite - Surinamer: Surinamese - Tallahassean: Tallahasseean - Tennesseean: Tennessean - Trois-Rivièrester: Trifluvian - Utahan: Utahn - Valladolidian: Vallisoletano + Alabaman: Alabamian + Albuquerquian: Albuquerquean + Anchoragite: Anchorageite + Arizonian: Arizonan + Arkansawyer: Arkansan + Belarusan: Belarusian + Cayman Islander: Caymanian + Coloradoan: Coloradan + Connecticuter: Nutmegger + Fairbanksian: Fairbanksan + Fort Worther: Fort Worthian + Grenadian: Grenadan + Halifaxer: Haligonian + Hartlepoolian: Hartlepudlian + Illinoisian: Illinoisan + Iowegian: Iowan + Leedsian: Leodenisian + Liverpoolian: Liverpudlian + Los Angelean: Angeleno + Manchesterian: Mancunian + Minneapolisian: Minneapolitan + Missouran: Missourian + Monacan: Monegasque + Neopolitan: Neapolitan + New Jerseyite: New Jerseyan + New Orleansian: New Orleanian + Oklahoma Citian: Oklahoma Cityan + Oklahomian: Oklahoman + Saudi Arabian: Saudi + Seattlite: Seattleite + Surinamer: Surinamese + Tallahassean: Tallahasseean + Tennesseean: Tennessean + Trois-Rivièrester: Trifluvian + Utahan: Utahn + Valladolidian: Vallisoletano diff --git a/.vale/proselint/GenderBias.yml b/.vale/proselint/GenderBias.yml index d98d3cf45f3..921213bfffc 100644 --- a/.vale/proselint/GenderBias.yml +++ b/.vale/proselint/GenderBias.yml @@ -5,41 +5,41 @@ level: error action: name: replace swap: - (?:alumnae|alumni): graduates - (?:alumna|alumnus): graduate - air(?:m[ae]n|wom[ae]n): pilot(s) - anchor(?:m[ae]n|wom[ae]n): anchor(s) - authoress: author - camera(?:m[ae]n|wom[ae]n): camera operator(s) - chair(?:m[ae]n|wom[ae]n): chair(s) + (?:alumnae|alumni): graduates + (?:alumna|alumnus): graduate + air(?:m[ae]n|wom[ae]n): pilot(s) + anchor(?:m[ae]n|wom[ae]n): anchor(s) + authoress: author + camera(?:m[ae]n|wom[ae]n): camera operator(s) + chair(?:m[ae]n|wom[ae]n): chair(s) congress(?:m[ae]n|wom[ae]n): member(s) of congress - door(?:m[ae]|wom[ae]n): concierge(s) - draft(?:m[ae]n|wom[ae]n): drafter(s) - fire(?:m[ae]n|wom[ae]n): firefighter(s) - fisher(?:m[ae]n|wom[ae]n): fisher(s) - fresh(?:m[ae]n|wom[ae]n): first-year student(s) - garbage(?:m[ae]n|wom[ae]n): waste collector(s) - lady lawyer: lawyer - ladylike: courteous - landlord: building manager - mail(?:m[ae]n|wom[ae]n): mail carriers - man and wife: husband and wife - man enough: strong enough - mankind: human kind - manmade: manufactured - men and girls: men and women - middle(?:m[ae]n|wom[ae]n): intermediary - news(?:m[ae]n|wom[ae]n): journalist(s) - ombuds(?:man|woman): ombuds - oneupmanship: upstaging - poetess: poet - police(?:m[ae]n|wom[ae]n): police officer(s) - repair(?:m[ae]n|wom[ae]n): technician(s) - sales(?:m[ae]n|wom[ae]n): salesperson or sales people - service(?:m[ae]n|wom[ae]n): soldier(s) - steward(?:ess)?: flight attendant - tribes(?:m[ae]n|wom[ae]n): tribe member(s) - waitress: waiter - woman doctor: doctor - woman scientist[s]?: scientist(s) - work(?:m[ae]n|wom[ae]n): worker(s) + door(?:m[ae]|wom[ae]n): concierge(s) + draft(?:m[ae]n|wom[ae]n): drafter(s) + fire(?:m[ae]n|wom[ae]n): firefighter(s) + fisher(?:m[ae]n|wom[ae]n): fisher(s) + fresh(?:m[ae]n|wom[ae]n): first-year student(s) + garbage(?:m[ae]n|wom[ae]n): waste collector(s) + lady lawyer: lawyer + ladylike: courteous + landlord: building manager + mail(?:m[ae]n|wom[ae]n): mail carriers + man and wife: husband and wife + man enough: strong enough + mankind: human kind + manmade: manufactured + men and girls: men and women + middle(?:m[ae]n|wom[ae]n): intermediary + news(?:m[ae]n|wom[ae]n): journalist(s) + ombuds(?:man|woman): ombuds + oneupmanship: upstaging + poetess: poet + police(?:m[ae]n|wom[ae]n): police officer(s) + repair(?:m[ae]n|wom[ae]n): technician(s) + sales(?:m[ae]n|wom[ae]n): salesperson or sales people + service(?:m[ae]n|wom[ae]n): soldier(s) + steward(?:ess)?: flight attendant + tribes(?:m[ae]n|wom[ae]n): tribe member(s) + waitress: waiter + woman doctor: doctor + woman scientist[s]?: scientist(s) + work(?:m[ae]n|wom[ae]n): worker(s) diff --git a/.vale/proselint/GroupTerms.yml b/.vale/proselint/GroupTerms.yml index 7a59fa48a70..acb4bed9e65 100644 --- a/.vale/proselint/GroupTerms.yml +++ b/.vale/proselint/GroupTerms.yml @@ -4,36 +4,36 @@ ignorecase: true action: name: replace swap: - (?:bunch|group|pack|flock) of chickens: brood of chickens - (?:bunch|group|pack|flock) of crows: murder of crows - (?:bunch|group|pack|flock) of hawks: cast of hawks - (?:bunch|group|pack|flock) of parrots: pandemonium of parrots - (?:bunch|group|pack|flock) of peacocks: muster of peacocks - (?:bunch|group|pack|flock) of penguins: muster of penguins - (?:bunch|group|pack|flock) of sparrows: host of sparrows - (?:bunch|group|pack|flock) of turkeys: rafter of turkeys + (?:bunch|group|pack|flock) of chickens: brood of chickens + (?:bunch|group|pack|flock) of crows: murder of crows + (?:bunch|group|pack|flock) of hawks: cast of hawks + (?:bunch|group|pack|flock) of parrots: pandemonium of parrots + (?:bunch|group|pack|flock) of peacocks: muster of peacocks + (?:bunch|group|pack|flock) of penguins: muster of penguins + (?:bunch|group|pack|flock) of sparrows: host of sparrows + (?:bunch|group|pack|flock) of turkeys: rafter of turkeys (?:bunch|group|pack|flock) of woodpeckers: descent of woodpeckers - (?:bunch|group|pack|herd) of apes: shrewdness of apes - (?:bunch|group|pack|herd) of baboons: troop of baboons - (?:bunch|group|pack|herd) of badgers: cete of badgers - (?:bunch|group|pack|herd) of bears: sloth of bears - (?:bunch|group|pack|herd) of bullfinches: bellowing of bullfinches - (?:bunch|group|pack|herd) of bullocks: drove of bullocks + (?:bunch|group|pack|herd) of apes: shrewdness of apes + (?:bunch|group|pack|herd) of baboons: troop of baboons + (?:bunch|group|pack|herd) of badgers: cete of badgers + (?:bunch|group|pack|herd) of bears: sloth of bears + (?:bunch|group|pack|herd) of bullfinches: bellowing of bullfinches + (?:bunch|group|pack|herd) of bullocks: drove of bullocks (?:bunch|group|pack|herd) of caterpillars: army of caterpillars - (?:bunch|group|pack|herd) of cats: clowder of cats - (?:bunch|group|pack|herd) of colts: rag of colts - (?:bunch|group|pack|herd) of crocodiles: bask of crocodiles - (?:bunch|group|pack|herd) of dolphins: school of dolphins - (?:bunch|group|pack|herd) of foxes: skulk of foxes - (?:bunch|group|pack|herd) of gorillas: band of gorillas - (?:bunch|group|pack|herd) of hippopotami: bloat of hippopotami - (?:bunch|group|pack|herd) of horses: drove of horses - (?:bunch|group|pack|herd) of jellyfish: fluther of jellyfish - (?:bunch|group|pack|herd) of kangeroos: mob of kangeroos - (?:bunch|group|pack|herd) of monkeys: troop of monkeys - (?:bunch|group|pack|herd) of oxen: yoke of oxen - (?:bunch|group|pack|herd) of rhinoceros: crash of rhinoceros - (?:bunch|group|pack|herd) of wild boar: sounder of wild boar - (?:bunch|group|pack|herd) of wild pigs: drift of wild pigs - (?:bunch|group|pack|herd) of zebras: zeal of wild pigs - (?:bunch|group|pack|school) of trout: hover of trout + (?:bunch|group|pack|herd) of cats: clowder of cats + (?:bunch|group|pack|herd) of colts: rag of colts + (?:bunch|group|pack|herd) of crocodiles: bask of crocodiles + (?:bunch|group|pack|herd) of dolphins: school of dolphins + (?:bunch|group|pack|herd) of foxes: skulk of foxes + (?:bunch|group|pack|herd) of gorillas: band of gorillas + (?:bunch|group|pack|herd) of hippopotami: bloat of hippopotami + (?:bunch|group|pack|herd) of horses: drove of horses + (?:bunch|group|pack|herd) of jellyfish: fluther of jellyfish + (?:bunch|group|pack|herd) of kangeroos: mob of kangeroos + (?:bunch|group|pack|herd) of monkeys: troop of monkeys + (?:bunch|group|pack|herd) of oxen: yoke of oxen + (?:bunch|group|pack|herd) of rhinoceros: crash of rhinoceros + (?:bunch|group|pack|herd) of wild boar: sounder of wild boar + (?:bunch|group|pack|herd) of wild pigs: drift of wild pigs + (?:bunch|group|pack|herd) of zebras: zeal of wild pigs + (?:bunch|group|pack|school) of trout: hover of trout diff --git a/.vale/proselint/Hedging.yml b/.vale/proselint/Hedging.yml index a8615f8bb28..3f924e11232 100644 --- a/.vale/proselint/Hedging.yml +++ b/.vale/proselint/Hedging.yml @@ -4,5 +4,5 @@ ignorecase: true level: error tokens: - I would argue that - - ', so to speak' + - ", so to speak" - to a certain degree diff --git a/.vale/proselint/Hyperbole.yml b/.vale/proselint/Hyperbole.yml index 0361772ce7e..64a672a7a4f 100644 --- a/.vale/proselint/Hyperbole.yml +++ b/.vale/proselint/Hyperbole.yml @@ -3,4 +3,4 @@ message: "'%s' is hyperbolic." level: error nonword: true tokens: - - '[a-z]+[!?]{2,}' + - "[a-z]+[!?]{2,}" diff --git a/.vale/proselint/LGBTTerms.yml b/.vale/proselint/LGBTTerms.yml index efdf268866c..572755f5381 100644 --- a/.vale/proselint/LGBTTerms.yml +++ b/.vale/proselint/LGBTTerms.yml @@ -4,12 +4,12 @@ ignorecase: true action: name: replace swap: - homosexual man: gay man - homosexual men: gay men - homosexual woman: lesbian - homosexual women: lesbians - homosexual people: gay people - homosexual couple: gay couple - sexual preference: sexual orientation + homosexual man: gay man + homosexual men: gay men + homosexual woman: lesbian + homosexual women: lesbians + homosexual people: gay people + homosexual couple: gay couple + sexual preference: sexual orientation (?:admitted homosexual|avowed homosexual): openly gay - special rights: equal rights + special rights: equal rights diff --git a/.vale/proselint/Needless.yml b/.vale/proselint/Needless.yml index 1f2732e173b..bab26eaceab 100644 --- a/.vale/proselint/Needless.yml +++ b/.vale/proselint/Needless.yml @@ -4,11 +4,11 @@ ignorecase: true action: name: replace swap: - '(?:cell phone|cell-phone)': cellphone - '(?:cliquey|cliquy)': cliquish - '(?:pygmean|pygmaen)': pygmy - '(?:retributional|retributionary)': retributive - '(?:revokable|revokeable)': revocable + "(?:cell phone|cell-phone)": cellphone + "(?:cliquey|cliquy)": cliquish + "(?:pygmean|pygmaen)": pygmy + "(?:retributional|retributionary)": retributive + "(?:revokable|revokeable)": revocable abolishment: abolition accessary: accessory accreditate: accredit diff --git a/.vale/proselint/RASSyndrome.yml b/.vale/proselint/RASSyndrome.yml index deae9c7d327..e2f93d9c5df 100644 --- a/.vale/proselint/RASSyndrome.yml +++ b/.vale/proselint/RASSyndrome.yml @@ -4,9 +4,9 @@ level: error action: name: edit params: - - split - - ' ' - - '0' + - split + - " " + - "0" tokens: - ABM missile - ACT test diff --git a/.vale/proselint/Typography.yml b/.vale/proselint/Typography.yml index 60283ebf01a..782750b24fa 100644 --- a/.vale/proselint/Typography.yml +++ b/.vale/proselint/Typography.yml @@ -8,4 +8,4 @@ swap: '\(TM\)': ™ '\(tm\)': ™ '\([rR]\)': ® - '[0-9]+ ?x ?[0-9]+': × + "[0-9]+ ?x ?[0-9]+": × diff --git a/.vale/proselint/Uncomparables.yml b/.vale/proselint/Uncomparables.yml index 9b96f42b022..34f432f0e4b 100644 --- a/.vale/proselint/Uncomparables.yml +++ b/.vale/proselint/Uncomparables.yml @@ -5,9 +5,9 @@ level: error action: name: edit params: - - split - - ' ' - - '1' + - split + - " " + - "1" raw: - \b(?:absolutely|most|more|less|least|very|quite|largely|extremely|increasingly|kind of|mildy|hardly|greatly|sort of)\b\s* tokens: @@ -18,7 +18,7 @@ tokens: - certain - devoid - entire - - 'false' + - "false" - fatal - favorite - final @@ -39,7 +39,7 @@ tokens: - singular - stationary - sufficient - - 'true' + - "true" - unanimous - unavoidable - unbroken diff --git a/cypress.config.js b/cypress.config.js index f7db28ad523..144cc4b903b 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,4 +1,6 @@ -const { defineConfig } = require('cypress'); +"use strict"; + +const { defineConfig } = require("cypress"); module.exports = defineConfig({ video: false, @@ -6,8 +8,8 @@ module.exports = defineConfig({ // We've imported your old cypress plugins here. // You may want to clean this up later by importing these. setupNodeEvents(on, config) { - return require('./cypress/plugins/index.js')(on, config); + return require("./cypress/plugins/index")(on, config); }, - baseUrl: 'http://localhost:4200', + baseUrl: "http://localhost:3000", }, }); diff --git a/cypress/e2e/check-server-side-rendering.cy.js b/cypress/e2e/check-server-side-rendering.cy.js index 5a6ccc64663..4f52ec57e25 100644 --- a/cypress/e2e/check-server-side-rendering.cy.js +++ b/cypress/e2e/check-server-side-rendering.cy.js @@ -1,37 +1,39 @@ -describe('server side rendered page', () => { - it('should find meta description', () => { - cy.visit('/guides/getting-started/'); +"use strict"; + +describe("server side rendered page", () => { + it("should find meta description", () => { + cy.visit("/guides/getting-started/"); cy.get('head meta[name="description"]').should( - 'have.attr', - 'content', - 'Learn how to bundle a JavaScript application with webpack 5.' + "have.attr", + "content", + "Learn how to bundle a JavaScript application with webpack 5.", ); }); - it('should find html tag with lang', () => { - cy.visit('/'); + it("should find html tag with lang", () => { + cy.visit("/"); cy.get('html[lang="ko"]'); }); - it('should find meta charset', () => { - cy.visit('/'); + it("should find meta charset", () => { + cy.visit("/"); cy.get('meta[charset="utf-8"]'); }); - it('should find the default meta description', () => { - cy.visit('/'); + it("should find the default meta description", () => { + cy.visit("/"); cy.get('head meta[name="description"]').should( - 'have.attr', - 'content', - '웹팩은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.' + "have.attr", + "content", + "Webpack은 모듈 번들러입니다. 주요 목적은 브라우저에서 사용할 수 있도록 JavaScript 파일을 번들로 묶는 것이지만, 리소스나 애셋을 변환하고 번들링 또는 패키징할 수도 있습니다.", ); }); - it('should find title', () => { - cy.visit('/'); - cy.title().should('eq', 'webpack'); + it("should find title", () => { + cy.visit("/"); + cy.title().should("eq", "webpack"); - cy.visit('/guides/getting-started/'); - cy.title().should('eq', 'Getting Started | 웹팩'); + cy.visit("/guides/getting-started/"); + cy.title().should("eq", "Getting Started | Webpack"); }); }); diff --git a/cypress/e2e/check-sub-navigation.cy.js b/cypress/e2e/check-sub-navigation.cy.js index 78507c91003..54ef8968189 100644 --- a/cypress/e2e/check-sub-navigation.cy.js +++ b/cypress/e2e/check-sub-navigation.cy.js @@ -1,17 +1,19 @@ -describe('Detect sub navigation', () => { - it('should show sub navigation', () => { - cy.visit('/concepts/'); +"use strict"; + +describe("Detect sub navigation", () => { + it("should show sub navigation", () => { + cy.visit("/concepts/"); const selector = '[data-testid="sub-navigation"]'; - cy.get(selector).should('exist'); + cy.get(selector).should("exist"); }); - it('should not show sub navigation', () => { - cy.visit('/'); + it("should not show sub navigation", () => { + cy.visit("/"); const selector = '[data-testid="sub-navigation"]'; - cy.get(selector).should('not.exist'); + cy.get(selector).should("not.exist"); }); }); diff --git a/cypress/e2e/click-menu-scroll-top.cy.js b/cypress/e2e/click-menu-scroll-top.cy.js index 266d0397cee..71278044128 100644 --- a/cypress/e2e/click-menu-scroll-top.cy.js +++ b/cypress/e2e/click-menu-scroll-top.cy.js @@ -1,6 +1,8 @@ -describe('Click menu', () => { - it('scroll to top when menu clicked', () => { - cy.visit('/concepts/modules/'); +"use strict"; + +describe("Click menu", () => { + it("scroll to top when menu clicked", () => { + cy.visit("/concepts/modules/"); // scroll to Contributors section // note that there's no hash in url cy.get('[data-testid="contributors"]').scrollIntoView(); diff --git a/cypress/e2e/client-side-redirection.cy.js b/cypress/e2e/client-side-redirection.cy.js index d7e877a652b..5f213696148 100644 --- a/cypress/e2e/client-side-redirection.cy.js +++ b/cypress/e2e/client-side-redirection.cy.js @@ -1,15 +1,17 @@ -describe('Redirect on client side', () => { - it('should redirect', () => { - const target = '/configuration/other-options/#cache'; +"use strict"; + +describe("Redirect on client side", () => { + it("should redirect", () => { + const target = "/configuration/other-options/#cache"; cy.visit(target); - cy.url().should('include', '/configuration/cache/'); + cy.url().should("include", "/configuration/cache/"); }); - it('should not redirect', () => { - cy.visit('/concepts/entry-points/#multi-page-application'); + it("should not redirect", () => { + cy.visit("/concepts/entry-points/#multi-page-application"); cy.url().should( - 'include', - '/concepts/entry-points/#multi-page-application' + "include", + "/concepts/entry-points/#multi-page-application", ); }); }); diff --git a/cypress/e2e/code-block-with-copy.cy.js b/cypress/e2e/code-block-with-copy.cy.js new file mode 100644 index 00000000000..e9d49b8f8cd --- /dev/null +++ b/cypress/e2e/code-block-with-copy.cy.js @@ -0,0 +1,90 @@ +"use strict"; + +const visitWithClipboardSpy = (url) => { + // Load page with a controllable clipboard implementation for this test run. + cy.visit(url, { + onBeforeLoad(win) { + Object.defineProperty(win, "isSecureContext", { + value: true, + configurable: true, + }); + + Object.defineProperty(win.navigator, "clipboard", { + value: { + writeText: () => Promise.resolve(), + }, + configurable: true, + }); + }, + }); + + // Stub clipboard writes so tests can assert exact copied text. + cy.window().then((win) => { + cy.stub(win.navigator.clipboard, "writeText") + .as("clipboardWriteText") + .resolves(); + }); +}; + +const getFirstWebpackConfigBlock = (aliasName) => { + cy.contains("strong", "webpack.config.js") + .first() + .parent() + .next(".code-block-wrapper") + .as(aliasName); +}; + +describe("CodeBlockWithCopy", () => { + it("copies diff code blocks without removed lines or diff prefixes", () => { + visitWithClipboardSpy("/guides/output-management/"); + + // Select the first webpack.config.js diff example and its copy wrapper. + getFirstWebpackConfigBlock("diffCodeBlock"); + + // Trigger copy for that specific diff code block. + cy.get("@diffCodeBlock").find("code.language-diff").should("exist"); + cy.get("@diffCodeBlock").find("button.copy-button").click(); + + // Assert copied output strips diff markers and removed lines. + cy.get("@clipboardWriteText").should("have.been.calledOnce"); + cy.get("@clipboardWriteText").then((clipboardWriteText) => { + const [copiedText] = clipboardWriteText.getCall(0).args; + + expect(copiedText).to.include("entry: {"); + expect(copiedText).to.include("index: './src/index.js',"); + expect(copiedText).to.include("print: './src/print.js',"); + expect(copiedText).to.include("filename: '[name].bundle.js',"); + + expect(copiedText).to.not.include("entry: './src/index.js',"); + expect(copiedText).to.not.include("filename: 'bundle.js',"); + expect(copiedText).to.not.match(/^[+-]/m); + }); + }); + + it("copies non-diff code blocks without altering content", () => { + visitWithClipboardSpy("/concepts/"); + + // Select the first webpack.config.js example and its copy wrapper. + getFirstWebpackConfigBlock("standardCodeBlock"); + + // Capture the rendered source text and trigger copy from this non-diff snippet. + cy.get("@standardCodeBlock").find("code.language-diff").should("not.exist"); + cy.get("@standardCodeBlock") + .find("code") + .invoke("text") + .as("expectedCopiedText"); + cy.get("@standardCodeBlock").find("button.copy-button").click(); + + // Assert copied output is unchanged for regular code blocks. + cy.get("@clipboardWriteText").should("have.been.calledOnce"); + cy.get("@clipboardWriteText").then((clipboardWriteText) => { + const [copiedText] = clipboardWriteText.getCall(0).args; + + cy.get("@expectedCopiedText").then((expectedCopiedText) => { + expect(copiedText).to.eq(expectedCopiedText); + expect(copiedText).to.include("module.exports = {"); + expect(copiedText).to.include('entry: "./path/to/my/entry/file.js",'); + }); + }); + }); +}); diff --git a/cypress/e2e/mobile-and-404.cy.js b/cypress/e2e/mobile-and-404.cy.js new file mode 100644 index 00000000000..b9c403b8b23 --- /dev/null +++ b/cypress/e2e/mobile-and-404.cy.js @@ -0,0 +1,34 @@ +"use strict"; + +describe("Mobile Sidebar", () => { + beforeEach(() => { + cy.viewport("iphone-x"); + cy.visit("/concepts/"); + }); + + it("should toggle the mobile sidebar visibility", () => { + cy.get(".sidebar-mobile").should( + "not.have.class", + "sidebar-mobile--visible", + ); + + cy.get('button[aria-label="Toggle navigation menu"]').click(); + cy.get(".sidebar-mobile").should("have.class", "sidebar-mobile--visible"); + + cy.get(".sidebar-mobile__close").click(); + cy.get(".sidebar-mobile").should( + "not.have.class", + "sidebar-mobile--visible", + ); + }); + + it("should close sidebar when clicking outside", () => { + cy.get('button[aria-label="Toggle navigation menu"]').click(); + // Click to the right of the 300px wide sidebar + cy.get("body").click(350, 500, { force: true }); + cy.get(".sidebar-mobile").should( + "not.have.class", + "sidebar-mobile--visible", + ); + }); +}); diff --git a/cypress/e2e/offline.cy.js b/cypress/e2e/offline.cy.js index 95a3c9c819e..60e6b5f7381 100644 --- a/cypress/e2e/offline.cy.js +++ b/cypress/e2e/offline.cy.js @@ -1,76 +1,78 @@ +"use strict"; + // https://github.com/cypress-io/cypress-example-recipes/blob/master/examples/server-communication__offline/cypress/integration/offline-spec.js const goOffline = () => { - cy.log('**go offline**') - .then(() => { - return Cypress.automation('remote:debugger:protocol', { - command: 'Network.enable', - }); - }) - .then(() => { - return Cypress.automation('remote:debugger:protocol', { - command: 'Network.emulateNetworkConditions', + cy.log("**go offline**") + .then(() => + Cypress.automation("remote:debugger:protocol", { + command: "Network.enable", + }), + ) + .then(() => + Cypress.automation("remote:debugger:protocol", { + command: "Network.emulateNetworkConditions", params: { offline: true, latency: -1, downloadThroughput: -1, uploadThroughput: -1, }, - }); - }); + }), + ); }; const goOnline = () => { // disable offline mode, otherwise we will break our tests :) - cy.log('**go online**') - .then(() => { + cy.log("**go online**") + .then(() => // https://chromedevtools.github.io/devtools-protocol/1-3/Network/#method-emulateNetworkConditions - return Cypress.automation('remote:debugger:protocol', { - command: 'Network.emulateNetworkConditions', + Cypress.automation("remote:debugger:protocol", { + command: "Network.emulateNetworkConditions", params: { offline: false, latency: -1, downloadThroughput: -1, uploadThroughput: -1, }, - }); - }) - .then(() => { - return Cypress.automation('remote:debugger:protocol', { - command: 'Network.disable', - }); - }); + }), + ) + .then(() => + Cypress.automation("remote:debugger:protocol", { + command: "Network.disable", + }), + ); }; -describe('offline', () => { - describe('site', { browser: '!firefox' }, () => { +describe("offline", () => { + describe("site", { browser: "!firefox" }, () => { // make sure we get back online, even if a test fails // otherwise the Cypress can lose the browser connection beforeEach(goOnline); afterEach(goOnline); - it('shows /migrate/ page', () => { - const url = '/migrate/'; - const text = 'Migrate'; + it("shows /migrate/ page", () => { + const url = "/migrate/"; + const text = "Migrate"; cy.visit(url); - cy.get('h1').contains(text); + cy.get("h1").contains(text); goOffline(); cy.visit(url); - cy.get('h1').contains(text); + cy.get("h1").contains(text); // click `guides` link cy.get('a[title="guides"]').click(); - cy.get('h1').contains('Guides'); + cy.get("h1").contains("Guides"); }); - it('open print dialog when accessing /printable url', () => { - const url = '/migrate/printable'; + it("open print dialog when accessing /printable url", () => { + const url = "/migrate/printable"; cy.visit(url, { onBeforeLoad: (win) => { - cy.stub(win, 'print'); + cy.stub(win, "print"); }, }); cy.window().then((win) => { diff --git a/cypress/e2e/pr_4435.cy.js b/cypress/e2e/pr_4435.cy.js index b62e84cc050..3d8ae80ac4b 100644 --- a/cypress/e2e/pr_4435.cy.js +++ b/cypress/e2e/pr_4435.cy.js @@ -1,10 +1,12 @@ +"use strict"; + // set scrollBehavior to false // because we don't want cypress to scroll itself -describe('Open page in new tab', { scrollBehavior: false }, () => { - it('should not scroll to top when right clicked', () => { - cy.visit('/concepts/plugins/', { +describe("Open page in new tab", { scrollBehavior: false }, () => { + it("should not scroll to top when right clicked", () => { + cy.visit("/concepts/plugins/", { onBeforeLoad: (win) => { - cy.stub(win, 'scrollTo'); + cy.stub(win, "scrollTo"); }, }); // there's one call in Page.jsx when componentDidMount @@ -21,7 +23,7 @@ describe('Open page in new tab', { scrollBehavior: false }, () => { expect(win.scrollTo).to.be.calledTwice; }); - if (Cypress.platform === 'darwin') { + if (Cypress.platform === "darwin") { cy.get(selector).click({ metaKey: true, }); @@ -29,7 +31,7 @@ describe('Open page in new tab', { scrollBehavior: false }, () => { cy.window().then((win) => { expect(win.scrollTo).to.be.calledTwice; }); - } else if (Cypress.platform === 'win32' || Cypress.platform === 'linux') { + } else if (Cypress.platform === "win32" || Cypress.platform === "linux") { // win32, linux cy.get(selector).click({ ctrlKey: true, diff --git a/cypress/e2e/scroll.cy.js b/cypress/e2e/scroll.cy.js index dc2ac68f3d3..001e6b40b50 100644 --- a/cypress/e2e/scroll.cy.js +++ b/cypress/e2e/scroll.cy.js @@ -1,26 +1,29 @@ -const sizes = ['iphone-6', 'macbook-15']; -describe('Scroll Test', () => { - sizes.forEach((size) => { +"use strict"; + +const sizes = ["iphone-6", "macbook-15"]; + +describe("Scroll Test", () => { + for (const size of sizes) { it(`scroll to top when accessing new page on ${size}`, () => { cy.viewport(size); - cy.visit('/guides/getting-started'); + cy.visit("/guides/getting-started"); // scroll to Contributors section cy.get('[data-testid="contributors"]').scrollIntoView(); - cy.isNotInViewport('#basic-setup'); + cy.isNotInViewport("#basic-setup"); - cy.visit('/guides/build-performance/'); + cy.visit("/guides/build-performance/"); cy.isNotInViewport('[data-testid="contributors"]'); }); it(`scroll to fragment when accessing new page with fragment on ${size}`, () => { cy.viewport(size); - cy.visit('/guides/getting-started'); + cy.visit("/guides/getting-started"); cy.get('[data-testid="contributors"]').scrollIntoView(); - cy.visit('/guides/build-performance/#development'); + cy.visit("/guides/build-performance/#development"); // since we lazy load notification bar now, #development element is a little out of viewport now - cy.isInViewport('#compile-in-memory'); - cy.isNotInViewport('#general'); + cy.isInViewport("#compile-in-memory"); + cy.isNotInViewport("#general"); }); - }); + } }); diff --git a/cypress/e2e/search.cy.js b/cypress/e2e/search.cy.js index 67c9cc09670..65f85ca3fbb 100644 --- a/cypress/e2e/search.cy.js +++ b/cypress/e2e/search.cy.js @@ -1,8 +1,10 @@ -describe('Search', () => { - it('should visit entry page', () => { - cy.visit('/concepts/'); - cy.get('.DocSearch').click(); - cy.get('#docsearch-input').type('roadmap'); - cy.get('.DocSearch-Hits').should('be.visible'); +"use strict"; + +describe("Search", () => { + it("should visit entry page", () => { + cy.visit("/concepts/"); + cy.get(".DocSearch").click(); + cy.get("#docsearch-input").type("roadmap"); + cy.get(".DocSearch-Hits").should("be.visible"); }); }); diff --git a/cypress/e2e/toggle-dark-mode.cy.js b/cypress/e2e/toggle-dark-mode.cy.js index 12f57be86b0..3b75b7d0fcc 100644 --- a/cypress/e2e/toggle-dark-mode.cy.js +++ b/cypress/e2e/toggle-dark-mode.cy.js @@ -1,13 +1,15 @@ -describe('Toggle dark mode', () => { - it('should toggle .dark class for html element', () => { - cy.visit('/'); +"use strict"; + +describe("Toggle dark mode", () => { + it("should toggle .dark class for html element", () => { + cy.visit("/"); const selector = '[data-testid="hello-darkness"]'; cy.get(selector).click(); - cy.get('html').should('have.class', 'dark'); + cy.get("html").should("have.class", "dark"); cy.get(selector).click(); - cy.get('html').should('not.have.class', 'dark'); + cy.get("html").should("not.have.class", "dark"); }); }); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 8affdd41470..9f409e20a53 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -1,3 +1,5 @@ +"use strict"; + /// // *********************************************************** // This example plugins/index.js can be used to load plugins diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c55e724ae3b..d0679e41c45 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -1,4 +1,6 @@ -Cypress.Commands.add('isNotInViewport', (element) => { +"use strict"; + +Cypress.Commands.add("isNotInViewport", (element) => { cy.get(element).then(($el) => { // we won't have horizontal scollbar const rect = $el[0].getBoundingClientRect(); @@ -10,7 +12,7 @@ Cypress.Commands.add('isNotInViewport', (element) => { }); }); -Cypress.Commands.add('isInViewport', (element) => { +Cypress.Commands.add("isInViewport", (element) => { cy.get(element).then(($el) => { const rect = $el[0].getBoundingClientRect(); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js index 37a498fb5bf..d1879435d93 100644 --- a/cypress/support/e2e.js +++ b/cypress/support/e2e.js @@ -1,3 +1,5 @@ +"use strict"; + // *********************************************************** // This example support/index.js is processed and // loaded automatically before your test files. @@ -14,7 +16,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands'; +// import "./commands.js"; // Alternatively you can use CommonJS syntax: -// require('./commands') +require("./commands"); diff --git a/eslint.config.mjs b/eslint.config.mjs index a0f4b369363..7fc04bdf812 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,123 +1,90 @@ -import cypress from 'eslint-plugin-cypress'; -import reactHooks from 'eslint-plugin-react-hooks'; -import { fixupPluginRules } from '@eslint/compat'; -import globals from 'globals'; -import babelParser from '@babel/eslint-parser'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import js from '@eslint/js'; -import { FlatCompat } from '@eslint/eslintrc'; +import { defineConfig, globalIgnores } from "eslint/config"; +import configs from "eslint-config-webpack/configs.js"; +import cypressPlugin from "eslint-plugin-cypress"; +import * as mdx from "eslint-plugin-mdx"; +import globals from "globals"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const compat = new FlatCompat({ - baseDirectory: __dirname, - recommendedConfig: js.configs.recommended, - allConfig: js.configs.all, -}); - -export default [ +export default defineConfig([ + globalIgnores([ + "**/dist/", + "**/examples/", + "**/printable.mdx", + "src/content/loaders/_*.mdx", + "src/content/plugins/_*.mdx", + "src/content/contribute/Governance-*.mdx", + ".github/**/*.md", + "**/README.md", + "src/mdx-components.mjs", + ]), { - ignores: [ - '**/dist/', - '**/examples/', - 'src/content/loaders/_*.mdx', - 'src/content/plugins/_*.mdx', - '.github/**/*.md', - '**/README.md', - ], + extends: [configs.recommended], + rules: { + "no-console": "off", + "n/no-unsupported-features/node-builtins": "off", + }, }, - ...compat.extends( - 'eslint:recommended', - 'plugin:react/recommended', - 'prettier' - ), { - plugins: { - cypress, - 'react-hooks': fixupPluginRules(reactHooks), + extends: [configs["browser/recommended-outdated-module"]], + files: [ + "src/components/**/*.{js,jsx}", + "src/utilities/test-local-storage.js", + "src/*.jsx", + "src/sw.js", + ], + rules: { + "unicorn/prefer-global-this": "off", }, - + }, + { + files: ["cypress/**/*.js"], + extends: [cypressPlugin.configs.recommended], languageOptions: { globals: { ...globals.browser, ...globals.node, - ...globals.jest, - ...cypress.configs.globals.languageOptions.globals, - }, - - parser: babelParser, - }, - - settings: { - react: { - version: 'detect', + ...cypressPlugin.configs.globals.languageOptions.globals, }, }, - rules: { - 'no-console': 'off', - semi: ['error', 'always'], - quotes: ['error', 'single'], - 'no-duplicate-imports': 'error', - 'react/jsx-uses-react': 'off', - 'react/react-in-jsx-scope': 'off', - 'react-hooks/rules-of-hooks': 'error', - 'react-hooks/exhaustive-deps': 'warn', - - 'react/no-unknown-property': [ - 'error', - { - ignore: ['watch', 'align'], - }, - ], + "no-unused-expressions": "off", }, }, { - files: ['src/**/*.jsx'], + ...mdx.flat, + files: ["**/*.mdx"], + linterOptions: { + // Buggy with mdx + reportUnusedDisableDirectives: "off", + }, + processor: mdx.createRemarkProcessor({ + lintCodeBlocks: true, + }), }, - ...compat.extends('plugin:mdx/recommended').map((config) => ({ - ...config, - files: ['**/*.mdx'], - })), { - files: ['**/*.mdx'], - + ...mdx.flatCodeBlocks, languageOptions: { - globals: { - Badge: true, - StackBlitzPreview: true, + sourceType: "module", + ecmaVersion: "latest", + parserOptions: { + ecmaFeatures: { + jsx: true, + globalReturn: true, + impliedStrict: true, + }, }, }, - - settings: { - 'mdx/code-blocks': true, - }, - + files: ["**/*.mdx/*.{js,jsx,javascript}"], rules: { - semi: ['off'], - }, - }, - { - files: ['**/*.mdx/*.{js,javascript}'], - - rules: { - indent: ['error', 2], - - quotes: [ - 'error', - 'single', - { - allowTemplateLiterals: true, - }, - ], - - 'no-undef': 'off', - 'no-unused-vars': 'off', - 'no-constant-condition': 'off', - 'no-useless-escape': 'off', - 'no-dupe-keys': 'off', - 'no-duplicate-imports': 'off', + ...configs["markdown/recommended"].find( + (item) => item.name === "markdown/code-blocks/js", + ).rules, + "func-names": "off", + "no-duplicate-imports": "off", + "n/exports-style": "off", + "import/no-amd": "off", + "import/extensions": "off", + "import/default": "off", + "unicorn/prefer-top-level-await": "off", }, }, -]; +]); diff --git a/glossary.js b/glossary.js index eac10dfc47d..5c29da3d685 100644 --- a/glossary.js +++ b/glossary.js @@ -1,12 +1,14 @@ -const fs = require('fs'); -const glossary = require('./glossary.json'); +"use strict"; -const CONTENT_PATH_PREFIX = './src/content'; +const fs = require("node:fs"); +const glossary = require("./glossary.json"); + +const CONTENT_PATH_PREFIX = "./src/content"; const LOG_KEY = { - ERROR: 'ERROR', - GLOSSARY: 'GLOSSARY', + ERROR: "ERROR", + GLOSSARY: "GLOSSARY", }; -const docExtensions = ['md', 'mdx']; +const docExtensions = ["md", "mdx"]; const log = (cmd, msg) => { const color = ((cmd) => { @@ -19,50 +21,51 @@ const log = (cmd, msg) => { return 33; } })(cmd); - const data = `\x1b[${color}m[${cmd}]\x1b[0m ${msg}`; + const data = `\u001B[${color}m[${cmd}]\u001B[0m ${msg}`; console.info(data); }; -const getTargetPaths = (cmd) => { - return docExtensions.map((extension) => { - return `${CONTENT_PATH_PREFIX}/${cmd}.${extension}`; - }); -}; +const getTargetPaths = (cmd) => + docExtensions.map( + (extension) => `${CONTENT_PATH_PREFIX}/${cmd}.${extension}`, + ); const getSplitLineData = (targetPath) => { - const fileData = fs.readFileSync(targetPath, 'utf-8'); - const splitLines = fileData.toString().split('\n'); + const fileData = fs.readFileSync(targetPath, "utf8"); + const splitLines = fileData.toString().split("\n"); return splitLines; }; const checkLine = (line, index) => { try { - const tokens = line.split(' ').filter((text) => text); - tokens.forEach((token) => { - if (!glossary[token] || token === glossary[token]) return; + const tokens = line.split(" ").filter(Boolean); + for (const token of tokens) { + if (!glossary[token] || token === glossary[token]) continue; log(LOG_KEY.GLOSSARY, `${index + 1}: ${token} -> ${glossary[token]}`); - }); - } catch (e) { - log(LOG_KEY.ERROR, e.toString()); + } + } catch (err) { + log(LOG_KEY.ERROR, err.toString()); } }; -process.argv.forEach((cmd, index) => { - if (index < 2) return; - log('CMD', cmd); +for (const [index, cmd] of process.argv.entries()) { + if (index < 2) continue; + log("CMD", cmd); const targetPaths = getTargetPaths(cmd); // Parallel start - targetPaths.forEach((targetPath) => { - log('READ START', targetPath); + for (const targetPath of targetPaths) { + log("READ START", targetPath); try { const splitLines = getSplitLineData(targetPath); - splitLines.forEach(checkLine); - } catch (e) { - log(LOG_KEY.ERROR, 'File not exist'); - throw e; + for (const [index, line] of splitLines.entries()) { + checkLine(line, index); + } + } catch (err) { + log(LOG_KEY.ERROR, "File not exist"); + throw err; } - }); -}); + } +} diff --git a/ground-rules/README.md b/ground-rules/README.md index ec73bc68954..7984a57708d 100644 --- a/ground-rules/README.md +++ b/ground-rules/README.md @@ -1,7 +1,8 @@ # Translation ground rules --- -***NOTE*** + +**_NOTE_** 번역 관련하여 논의한 내용 및 자료를 모아두는 곳 --- diff --git a/jest.config.mjs b/jest.config.mjs index af67fed12ff..eea8faaf2ca 100644 --- a/jest.config.mjs +++ b/jest.config.mjs @@ -1,11 +1,28 @@ -import jestConfig from 'jest-config'; export default { verbose: true, - transform: {}, - moduleFileExtensions: [...jestConfig.defaults.moduleFileExtensions, 'mjs'], + testEnvironment: "node", + transform: { + "^.+\\.jsx?$": "babel-jest", + }, + moduleNameMapper: { + "\\.(scss|css)$": "/src/components/__mocks__/styleMock.js", + "\\.svg$": "/src/components/__mocks__/svgMock.js", + }, + moduleFileExtensions: [ + "js", + "mjs", + "cjs", + "jsx", + "ts", + "tsx", + "json", + "node", + ], testMatch: [ - '**/__tests__/**/*.[jt]s?(x)', - '**/?(*.)+(spec|test).[tj]s?(x)', - '**/*.test.mjs', + "**/__tests__/**/*.[jt]s?(x)", + "**/?(*.)+(spec|test).[tj]s?(x)", + "**/*.test.mjs", + "**/src/components/**/*.test.jsx", ], + testPathIgnorePatterns: ["/node_modules/"], }; diff --git a/package.json b/package.json index a9a7fe9e829..7c426bbc59a 100644 --- a/package.json +++ b/package.json @@ -3,26 +3,23 @@ "version": "0.1.2", "private": true, "description": "The main site for all things webpack.", - "homepage": "https://github.com/webpack/webpack.js.org", - "author": "Greg Venech", - "license": "CC BY 4.0", - "main": "n/a", "keywords": [ "webpack", "documentation", "build", "tool" ], - "repository": { - "type": "git", - "url": "https://github.com/webpack/webpack.js.org.git" - }, + "homepage": "https://github.com/webpack/webpack.js.org", "bugs": { "url": "https://github.com/webpack/webpack.js.org/issues" }, - "engines": { - "node": ">= 20.9.0" + "repository": { + "type": "git", + "url": "https://github.com/webpack/webpack.js.org.git" }, + "license": "CC BY 4.0", + "author": "Greg Venech", + "main": "n/a", "scripts": { "clean-dist": "rimraf ./dist", "clean-printable": "rimraf src/content/**/printable.mdx", @@ -38,136 +35,134 @@ "fetch:governance": "node src/utilities/fetch-governance.mjs", "fetch-all": "run-s fetch-repos fetch", "prebuild": "npm run clean", - "build": "run-s fetch-repos fetch:governance fetch content && webpack --config webpack.prod.mjs --config-node-env production && run-s printable content && webpack --config webpack.ssg.mjs --config-node-env production --env ssg", - "postbuild": "npm run sitemap", - "build-test": "npm run build && http-server --port 4200 dist/", - "serve-dist": "http-server --port 4200 dist/", - "test": "npm run lint", + "build": "run-s fetch-repos fetch content && webpack --config webpack.prod.mjs --config-node-env production && run-s printable content && webpack --config webpack.ssg.mjs --config-node-env production --env ssg", + "postbuild": "npm run sitemap && npm run rss", + "build-test": "npm run build && http-server --port 3000 dist/", + "serve-dist": "http-server --port 3000 dist/", + "test": "run-s lint jest", "lint": "run-s lint:*", - "lint:js": "npm run lint-js .", - "lint-js": "eslint --cache --cache-location .cache/.eslintcache", - "lint:markdown": "npm run lint-markdown '**/*.{md,mdx}'", - "lint-markdown": "markdownlint --config ./.markdownlint.json", + "lint:prettier": "prettier --cache --list-different --ignore-unknown .", + "lint:js": "eslint --cache --cache-location .cache/.eslintcache .", + "lint:markdown": "markdownlint --config ./.markdownlint.json '**/*.{md,mdx}'", "lint:prose": "vale --config='.vale.ini' src/content", "lint:links": "hyperlink -c 8 --root dist -r dist/index.html --canonicalroot https://webpack.kr/ --internal --skip '%E' --skip /plugins/extract-text-webpack-plugin/ --skip /printable --skip /contribute/Governance --skip https:// --skip http:// --skip sw.js --skip /vendor > internal-links.tap; cat internal-links.tap | tap-spot", "sitemap": "cd dist && sitemap-static --ignore-file=../sitemap-ignore.json --pretty --prefix=https://webpack.kr/ > sitemap.xml", + "rss": "node src/scripts/generate-rss.mjs", "serve": "npm run build && sirv start ./dist --port 4000", "preprintable": "npm run clean-printable", "printable": "node ./src/scripts/concatenate-docs.mjs", "jest": "NODE_OPTIONS=--experimental-vm-modules jest --config=jest.config.mjs", "cypress:open": "cypress open", "cypress:run": "cypress run --browser chrome", - "prettier": "prettier --write '**/*.{js,json,jsx,css,scss,md,mdx}'", + "prettier": "prettier --cache --write --ignore-unknown .", "prepare": "husky && rimraf ./node_modules/.cache/webpack && yarn-deduplicate --strategy fewer" }, "lint-staged": { - "*.{js,mjs,jsx,md,mdx}": [ - "npm run lint-js" + "*.{js,mjs,cjs,jsx,md,mdx}": [ + "npm run lint:js" ], "*.{md,mdx}": [ - "npm run lint-markdown" + "npm run lint:markdown" ], "*.{js,mjs,jsx,css,scss,md,mdx,json}": [ "prettier --write" ] }, + "resolutions": { + "sitemap-static/minimist": "1.2.5", + "eval": "^0.1.5" + }, + "dependencies": { + "@docsearch/react": "^3.9.0", + "@react-spring/web": "^10.0.3", + "path-browserify": "^1.0.1", + "prop-types": "^15.8.1", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "react-helmet-async": "^2.0.5", + "react-router-dom": "^6.28.0", + "react-tiny-popover": "5", + "react-use": "^17.6.0", + "react-visibility-sensor": "^5.0.2", + "webpack-pwa-manifest": "^4.3.0", + "workbox-window": "^7.4.0" + }, "devDependencies": { - "@babel/core": "^7.28.0", - "@babel/eslint-parser": "^7.28.0", + "@babel/core": "^7.29.0", "@babel/plugin-proposal-class-properties": "^7.17.12", - "@babel/preset-env": "^7.27.1", - "@babel/preset-react": "^7.27.1", - "@eslint/compat": "^1.3.1", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "^9.31.0", - "@mdx-js/loader": "^3.1.0", - "@mdx-js/react": "^3.1.0", - "@octokit/auth-action": "^5.1.1", - "@octokit/rest": "^21.1.1", - "@pmmmwh/react-refresh-webpack-plugin": "next", + "@babel/preset-env": "^7.29.0", + "@babel/preset-react": "^7.27.6", + "@mdx-js/loader": "^3.1.1", + "@mdx-js/react": "^3.1.1", + "@octokit/auth-action": "^6.0.2", + "@octokit/rest": "^22.0.1", + "@pmmmwh/react-refresh-webpack-plugin": "^0.6.2", "@svgr/webpack": "^8.1.0", - "autoprefixer": "^10.4.21", + "autoprefixer": "^10.4.24", "babel-loader": "^10.0.0", - "copy-webpack-plugin": "^13.0.0", - "css-loader": "^7.1.2", - "css-minimizer-webpack-plugin": "^7.0.2", - "cypress": "^13.16.0", - "directory-tree": "^3.5.2", + "copy-webpack-plugin": "^13.0.1", + "css-loader": "^7.1.3", + "css-minimizer-webpack-plugin": "^7.0.4", + "cypress": "^15.9.0", + "directory-tree": "^3.6.0", "directory-tree-webpack-plugin": "^1.0.3", "duplexer": "^0.1.1", - "eslint": "^9.31.0", - "eslint-config-prettier": "^10.1.5", - "eslint-plugin-cypress": "^5.1.0", + "eslint": "^9.39.2", + "eslint-config-webpack": "^4.9.3", + "eslint-plugin-cypress": "^5.2.1", "eslint-plugin-mdx": "^3.6.2", - "eslint-plugin-react": "^7.37.5", - "eslint-plugin-react-hooks": "^5.2.0", + "feed": "^5.0.0", "front-matter": "^4.0.2", "github-slugger": "^2.0.0", - "globals": "^16.3.0", - "html-webpack-plugin": "^5.6.3", + "globals": "^17.3.0", + "html-webpack-plugin": "^5.6.6", "http-server": "^14.1.1", "husky": "^9.1.7", "hyperlink": "^5.0.4", - "jest": "^29.7.0", - "lightningcss": "^1.30.1", - "lint-staged": "^15.4.3", - "lodash": "^4.17.21", - "markdownlint-cli": "^0.44.0", + "jest": "^30.2.0", + "jest-environment-jsdom": "^30.2.0", + "lightningcss": "^1.31.1", + "lint-staged": "^16.2.7", + "lodash": "^4.17.23", + "markdownlint-cli": "^0.47.0", "mdast-util-to-string": "^4.0.0", - "mini-css-extract-plugin": "^2.9.2", + "mini-css-extract-plugin": "^2.10.0", "mkdirp": "^3.0.1", "modularscale-sass": "^3.0.3", "npm-run-all": "^4.1.1", - "postcss": "^8.4.49", - "postcss-loader": "^8.1.1", - "prettier": "^3.6.2", - "react-refresh": "^0.14.2", + "postcss": "^8.5.6", + "postcss-loader": "^8.2.0", + "prettier": "^3.8.1", + "react-refresh": "^0.18.0", + "react-test-renderer": "^17.0.2", "redirect-webpack-plugin": "^1.0.0", "remark": "^15.0.1", "remark-autolink-headings": "7.0.1", - "remark-emoji": "^5.0.1", + "remark-emoji": "^5.0.2", "remark-extract-anchors": "1.1.1", "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", + "remark-gfm": "^4.0.1", "remark-html": "^16.0.1", "remark-refractor": "montogeek/remark-refractor", - "rimraf": "^6.0.1", - "sass": "^1.79.5", - "sass-loader": "^16.0.4", - "sirv-cli": "^3.0.0", + "rimraf": "^6.1.2", + "sass": "^1.97.2", + "sass-loader": "^16.0.6", + "sirv-cli": "^3.0.1", "sitemap-static": "^0.4.2", "static-site-generator-webpack-plugin": "^3.4.1", "style-loader": "^4.0.0", "tailwindcss": "^3.4.16", "tap-spot": "^1.1.2", - "unist-util-visit": "^5.0.0", - "webpack": "^5.97.1", - "webpack-bundle-analyzer": "^4.10.2", + "unist-util-visit": "^5.1.0", + "webpack": "^5.105.0", + "webpack-bundle-analyzer": "^5.2.0", "webpack-cli": "^6.0.1", - "webpack-dev-server": "^5.2.2", + "webpack-dev-server": "^5.2.3", "webpack-merge": "^6.0.1", - "workbox-webpack-plugin": "^7.3.0", + "workbox-webpack-plugin": "^7.4.0", "yarn-deduplicate": "^6.0.2" }, - "dependencies": { - "@docsearch/react": "^3.9.0", - "@react-spring/web": "^9.7.5", - "path-browserify": "^1.0.1", - "prop-types": "^15.8.1", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-helmet-async": "^2.0.5", - "react-router-dom": "^6.28.0", - "react-tiny-popover": "5", - "react-use": "^17.5.1", - "react-visibility-sensor": "^5.0.2", - "webpack-pwa-manifest": "^4.3.0", - "workbox-window": "^7.3.0" - }, - "resolutions": { - "sitemap-static/minimist": "1.2.5", - "ini": "1.3.7", - "eval": "^0.1.5", - "markdownlint-cli/markdownlint": "^0.37.4" + "engines": { + "node": ">= 20.9.0" } } diff --git a/postcss.config.js b/postcss.config.js index 3ea9307f403..989a8c7d4ac 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,3 +1,5 @@ +"use strict"; + module.exports = { - plugins: [require('tailwindcss'), require('autoprefixer')], + plugins: [require("tailwindcss"), require("autoprefixer")], }; diff --git a/prettier.config.js b/prettier.config.js deleted file mode 100644 index ca047dca859..00000000000 --- a/prettier.config.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - singleQuote: true, - semi: true, - trailingComma: 'es5', -}; diff --git a/prettier.config.mjs b/prettier.config.mjs new file mode 100644 index 00000000000..ee90dacf668 --- /dev/null +++ b/prettier.config.mjs @@ -0,0 +1,6 @@ +export default { + printWidth: 80, + tabWidth: 2, + trailingComma: "all", + arrowParens: "always", +}; diff --git a/src/AnalyticsRouter.jsx b/src/AnalyticsRouter.jsx index 4c683c1e742..ab663d679ae 100644 --- a/src/AnalyticsRouter.jsx +++ b/src/AnalyticsRouter.jsx @@ -2,14 +2,10 @@ * based on https://github.com/seeden/react-g-analytics * refactored against new version of react/react-router-dom */ -import { BrowserRouter, useLocation } from 'react-router-dom'; -import PropTypes from 'prop-types'; -import { useEffect } from 'react'; -AnalyticsRouter.propTypes = { - id: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - set: PropTypes.object, -}; +import PropTypes from "prop-types"; +import { useEffect } from "react"; +import { BrowserRouter, useLocation } from "react-router-dom"; + export default function AnalyticsRouter(props) { const { id, set, children } = props; @@ -22,13 +18,19 @@ export default function AnalyticsRouter(props) { ); } +AnalyticsRouter.propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + set: PropTypes.object, +}; + function loadScript() { - const gads = document.createElement('script'); + const gads = document.createElement("script"); gads.async = true; - gads.type = 'text/javascript'; - gads.src = '//www.google-analytics.com/analytics.js'; + gads.type = "text/javascript"; + gads.src = "//www.google-analytics.com/analytics.js"; - const head = document.getElementsByTagName('head')[0]; + const [head] = document.getElementsByTagName("head"); head.appendChild(gads); } @@ -37,45 +39,38 @@ function initGoogleAnalytics(id, set) { return; } - window.ga = - window.ga || - function () { - (ga.q = ga.q || []).push(arguments); // eslint-disable-line - }; + window.ga ||= function ga() { + (ga.q = ga.q || []).push(arguments); // eslint-disable-line + }; ga.l = +new Date(); // eslint-disable-line loadScript(); - window.ga('create', id, 'auto'); + window.ga("create", id, "auto"); if (set) { - Object.keys(set).forEach((key) => { - window.ga('set', key, set[key]); - }); + for (const key of Object.keys(set)) { + window.ga("set", key, set[key]); + } } } -GoogleAnalytics.propTypes = { - id: PropTypes.string.isRequired, - children: PropTypes.node.isRequired, - set: PropTypes.object, -}; function googleAnalyticsCommand(what, options, ...args) { if (!window.ga) { - throw new Error('Google analytics is not initialized'); + throw new Error("Google analytics is not initialized"); } - if (typeof options === 'string') { + if (typeof options === "string") { return window.ga(what, options, ...args); } return window.ga(what, options); } function googleAnalyticsSet(...options) { - return googleAnalyticsCommand('set', ...options); + return googleAnalyticsCommand("set", ...options); } function googleAnalyticsSend(...options) { - return googleAnalyticsCommand('send', ...options); + return googleAnalyticsCommand("send", ...options); } function GoogleAnalytics(props) { @@ -97,9 +92,15 @@ function GoogleAnalytics(props) { }); googleAnalyticsSend({ - hitType: 'pageview', + hitType: "pageview", }); }, [location]); return <>{children}; } + +GoogleAnalytics.propTypes = { + id: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, + set: PropTypes.object, +}; diff --git a/src/App.jsx b/src/App.jsx index 26b9df5dc08..26b692fbacd 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ -import Site from './components/Site/Site'; +import Site from "./components/Site/Site.jsx"; + export default function App() { return import(`./content/${path}`)} />; } diff --git a/src/ProdAssetsManifest.mjs b/src/ProdAssetsManifest.mjs index c8c480a9002..f048f7fa63b 100644 --- a/src/ProdAssetsManifest.mjs +++ b/src/ProdAssetsManifest.mjs @@ -1,4 +1,5 @@ -import webpack from 'webpack'; +import webpack from "webpack"; + const { Compilation, sources } = webpack; // collect assets data (vendor.[contenthash].js and index.[contenthash].js) for ssg @@ -6,45 +7,46 @@ class ProdAssetsManifest { apply(compiler) { let js = []; let css = []; - compiler.hooks.thisCompilation.tap('ProdAssetsManifest', (compilation) => { + compiler.hooks.thisCompilation.tap("ProdAssetsManifest", (compilation) => { compilation.hooks.processAssets.tap( { - name: 'ProdAssetsManifest', + name: "ProdAssetsManifest", stage: Compilation.PROCESS_ASSETS_STAGE_ANALYSE, }, () => { for (const [, entrypoint] of compilation.entrypoints) { const files = entrypoint.getFiles(); - js = js.concat( - files - .filter((file) => file.endsWith('.js')) // two js files - .sort((a) => { + js = [ + ...js, + ...files + .filter((file) => file.endsWith(".js")) // two js files + .toSorted((a) => { // vendor first - if (a.startsWith('vendor')) return -1; + if (a.startsWith("vendor")) return -1; return 1; }) - .map((file) => { - return compilation.outputOptions.publicPath + file; - }) - ); - css = css.concat( - files - .filter((file) => file.endsWith('.css')) - .sort((a) => { + .map((file) => compilation.outputOptions.publicPath + file), + ]; + css = [ + ...css, + ...files + .filter((file) => file.endsWith(".css")) + .toSorted((a) => { // index first - if (a.startsWith('index')) return -1; + if (a.startsWith("index")) return -1; return 1; }) - .map((file) => compilation.outputOptions.publicPath + file) - ); + .map((file) => compilation.outputOptions.publicPath + file), + ]; } compilation.emitAsset( - 'prod-assets-manifest.json', - new sources.RawSource(JSON.stringify({ js, css })) + "prod-assets-manifest.json", + new sources.RawSource(JSON.stringify({ js, css })), ); - } + }, ); }); } } + export default ProdAssetsManifest; diff --git a/src/assets/google2f228e794f2592f2.html b/src/assets/google2f228e794f2592f2.html index 4b8b261f86f..b8cc4e5c9c5 100644 --- a/src/assets/google2f228e794f2592f2.html +++ b/src/assets/google2f228e794f2592f2.html @@ -1 +1 @@ -google-site-verification: google2f228e794f2592f2.html \ No newline at end of file +google-site-verification: google2f228e794f2592f2.html diff --git a/src/assets/openjs-logo.png b/src/assets/openjs-logo.png new file mode 100644 index 00000000000..5e7756cdaeb Binary files /dev/null and b/src/assets/openjs-logo.png differ diff --git a/src/assets/supporters-noneedtostudy-logo-medium.png b/src/assets/supporters-noneedtostudy-logo-medium.png new file mode 100644 index 00000000000..ad66b46da15 Binary files /dev/null and b/src/assets/supporters-noneedtostudy-logo-medium.png differ diff --git a/src/components/Badge/Badge.js b/src/components/Badge/Badge.jsx similarity index 71% rename from src/components/Badge/Badge.js rename to src/components/Badge/Badge.jsx index 0fd3aa2903d..4f5128de5dd 100644 --- a/src/components/Badge/Badge.js +++ b/src/components/Badge/Badge.jsx @@ -1,8 +1,10 @@ -import PropTypes from 'prop-types'; -import './Badge.scss'; -Badge.propTypes = { - text: PropTypes.string.isRequired, -}; +import PropTypes from "prop-types"; +import "./Badge.scss"; + export default function Badge(props) { return {props.text}; } + +Badge.propTypes = { + text: PropTypes.string.isRequired, +}; diff --git a/src/components/Badge/Badge.test.jsx b/src/components/Badge/Badge.test.jsx new file mode 100644 index 00000000000..40a461e1f69 --- /dev/null +++ b/src/components/Badge/Badge.test.jsx @@ -0,0 +1,11 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { describe, expect, it } from "@jest/globals"; +import renderer from "react-test-renderer"; +import Badge from "./Badge.jsx"; + +describe("Badge", () => { + it("renders correctly with text prop", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Badge/__snapshots__/Badge.test.jsx.snap b/src/components/Badge/__snapshots__/Badge.test.jsx.snap new file mode 100644 index 00000000000..9c8f80aa892 --- /dev/null +++ b/src/components/Badge/__snapshots__/Badge.test.jsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Badge renders correctly with text prop 1`] = ` + + webpack + +`; diff --git a/src/components/BrandingSample.jsx b/src/components/BrandingSample.jsx index a1f0b81c256..dab4601808e 100644 --- a/src/components/BrandingSample.jsx +++ b/src/components/BrandingSample.jsx @@ -1,7 +1,9 @@ -import PropTypes from 'prop-types'; -BrandingSample.propTypes = { - color: PropTypes.string.isRequired, -}; +import PropTypes from "prop-types"; + export default function BrandingSample({ color }) { return
 
; } + +BrandingSample.propTypes = { + color: PropTypes.string.isRequired, +}; diff --git a/src/components/CodeBlockWithCopy/CodeBlockWithCopy.jsx b/src/components/CodeBlockWithCopy/CodeBlockWithCopy.jsx index 07dd90a377f..ca34c409b25 100644 --- a/src/components/CodeBlockWithCopy/CodeBlockWithCopy.jsx +++ b/src/components/CodeBlockWithCopy/CodeBlockWithCopy.jsx @@ -1,18 +1,47 @@ -import { useRef, useState } from 'react'; -import PropTypes from 'prop-types'; -import './CodeBlockWithCopy.scss'; +import PropTypes from "prop-types"; +import { useRef, useState } from "react"; +import "./CodeBlockWithCopy.scss"; export default function CodeBlockWithCopy({ children }) { const preRef = useRef(null); - const [copyStatus, setCopyStatus] = useState('copy'); + const [copyStatus, setCopyStatus] = useState("copy"); + const codeClassName = + typeof children?.props?.className === "string" + ? children.props.className + : ""; + const isDiffLanguage = codeClassName.split(/\s+/).includes("language-diff"); + + const getCodeText = (codeElement) => { + if (!isDiffLanguage) { + return codeElement.textContent || ""; + } + + const clonedCodeElement = codeElement.cloneNode(true); + + // Exclude +/-/unchanged tokens and removed lines + for (const element of clonedCodeElement.querySelectorAll( + ".token.prefix.inserted, .token.prefix.unchanged, .token.deleted-sign.deleted", + )) { + element.remove(); + } + + return clonedCodeElement.textContent || ""; + }; const handleCopy = async () => { if (!preRef.current) return; - const codeElement = preRef.current.querySelector('code'); + const codeElement = preRef.current.querySelector("code"); if (!codeElement) return; - const codeText = codeElement.textContent; + const codeText = getCodeText(codeElement); + + if (!codeText) { + setCopyStatus("error"); + setTimeout(() => setCopyStatus("copy"), 2000); + return; + } + let successfulCopy = false; // Try modern API (navigator.clipboard) -> as document.execCommand() deprecated @@ -27,17 +56,17 @@ export default function CodeBlockWithCopy({ children }) { // If modern API failed, fall back to deprecated document.execCommand('copy') if (!successfulCopy) { - const textarea = document.createElement('textarea'); + const textarea = document.createElement("textarea"); textarea.value = codeText; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; + textarea.style.position = "fixed"; + textarea.style.opacity = "0"; document.body.appendChild(textarea); textarea.select(); try { // This deprecated method is kept as a fallback for compatibility/iframe environments. - successfulCopy = document.execCommand('copy'); + successfulCopy = document.execCommand("copy"); } catch (err) { successfulCopy = false; console.log(err); @@ -46,23 +75,25 @@ export default function CodeBlockWithCopy({ children }) { document.body.removeChild(textarea); } - setCopyStatus(successfulCopy ? 'copied' : 'error'); - setTimeout(() => setCopyStatus('copy'), 2000); + setCopyStatus(successfulCopy ? "copied" : "error"); + setTimeout(() => setCopyStatus("copy"), 2000); }; return (
- -
-        {children}
-      
+
{children}
); } diff --git a/src/components/CodeBlockWithCopy/CodeBlockWithCopy.scss b/src/components/CodeBlockWithCopy/CodeBlockWithCopy.scss index 18e59c74236..4ec6b581381 100644 --- a/src/components/CodeBlockWithCopy/CodeBlockWithCopy.scss +++ b/src/components/CodeBlockWithCopy/CodeBlockWithCopy.scss @@ -1,27 +1,15 @@ .code-block-wrapper { position: relative; margin-bottom: 1.5rem; -} - -.code-block { - background-color: #2d3748; - color: #e2e8f0; - padding: 1rem; - padding-right: 3.5rem; - border-radius: 0.5rem; - overflow-x: auto; - font-size: 0.875rem; - line-height: 1.5; - box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1); - code { - font-family: monospace; + &:hover .copy-button { + opacity: 1; } } .copy-button { position: absolute; - top: 0.32rem; + top: 0.5rem; right: 0.5rem; z-index: 10; @@ -34,18 +22,19 @@ font-size: 0.75rem; font-weight: 500; - /* Always visible */ - opacity: 1; + /* visible on hover */ + opacity: 0; - background-color: #7c3aed; + background-color: #175d96; color: #e2e8f0; transition: background-color 0.2s, - transform 0.1s; + transform 0.1s, + opacity 0.3s; &:hover { - background-color: #6d28d9; + background-color: #2f85d0; } /* Success */ @@ -66,6 +55,7 @@ &:focus { outline: none; + opacity: 1; } &:active { diff --git a/src/components/Configuration/components.js b/src/components/Configuration/components.jsx similarity index 72% rename from src/components/Configuration/components.js rename to src/components/Configuration/components.jsx index 40a9c0f1373..3012b4f87cd 100644 --- a/src/components/Configuration/components.js +++ b/src/components/Configuration/components.jsx @@ -1,40 +1,40 @@ -import { isValidElement, Component } from 'react'; -import Popover from 'react-tiny-popover'; -import './Configuration.scss'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import { Component, isValidElement } from "react"; +import Popover from "react-tiny-popover"; +import "./Configuration.scss"; const DEFAULT_CHILDREN_SIZE = 4; -const isFirstChild = (child) => typeof child === 'string' && child !== ' '; +const isFirstChild = (child) => typeof child === "string" && child !== " "; const removeSpaces = (child) => (isFirstChild(child) ? child.trim() : child); -const addLink = (child, i, url) => { - return isFirstChild(child) ? ( +const addLink = (child, i, url) => + isFirstChild(child) ? ( {child} ) : ( child ); -}; -const Card = ({ body }) => { - return ( -
-
-        {body}
-      
-
- ); -}; +const Card = ({ body }) => ( +
+
+      {body}
+    
+
+); + Card.propTypes = { body: PropTypes.node, }; + export class Details extends Component { static propTypes = { url: PropTypes.string, myChilds: PropTypes.arrayOf(PropTypes.node), }; + constructor(props) { super(props); this.state = { @@ -57,13 +57,15 @@ export class Details extends Component { const closeDefaultTagIndex = myChilds.findIndex((child) => { if (isValidElement(child)) { return ( - child.props.className.includes('tag') && + child.props.className.includes("tag") && child.props.children.length === DEFAULT_CHILDREN_SIZE ); } + + return false; }); - const content = myChilds.slice(); + const content = [...myChilds]; // Summary is the part of the snippet that would be shown in the code snippet, // to get it we need to cut the enclosing tags @@ -78,10 +80,10 @@ export class Details extends Component { return ( } > {props.children}; } + +Container.propTypes = { + className: PropTypes.string, + children: PropTypes.node, +}; diff --git a/src/components/Container/Container.scss b/src/components/Container/Container.scss index 474eeebf173..8825d330ca9 100644 --- a/src/components/Container/Container.scss +++ b/src/components/Container/Container.scss @@ -1,5 +1,5 @@ -@import 'vars'; -@import 'mixins'; +@import "vars"; +@import "mixins"; .container { width: 100%; diff --git a/src/components/Container/Container.test.jsx b/src/components/Container/Container.test.jsx new file mode 100644 index 00000000000..4837676c510 --- /dev/null +++ b/src/components/Container/Container.test.jsx @@ -0,0 +1,28 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { describe, expect, it } from "@jest/globals"; +import renderer from "react-test-renderer"; +import Container from "./Container.jsx"; + +describe("Container", () => { + it("renders correctly with children and className", () => { + const tree = renderer + .create( + +

Child content

+
, + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("renders correctly without className", () => { + const tree = renderer + .create( + + Simple child + , + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Container/__snapshots__/Container.test.jsx.snap b/src/components/Container/__snapshots__/Container.test.jsx.snap new file mode 100644 index 00000000000..a12206efe49 --- /dev/null +++ b/src/components/Container/__snapshots__/Container.test.jsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Container renders correctly with children and className 1`] = ` +
+

+ Child content +

+
+`; + +exports[`Container renders correctly without className 1`] = ` +
+ + Simple child + +
+`; diff --git a/src/components/Contributors/404.js b/src/components/Contributors/404.js index dc56737e8f6..70fcfbf27e4 100644 --- a/src/components/Contributors/404.js +++ b/src/components/Contributors/404.js @@ -1,12 +1,12 @@ // contributors 404 export const contributorsNotFound = [ - 'bartushek', - 'rynclark', - 'henriquea', - 'makuzaverite', - 'MijaelWatts', - 'Kolhar730', - 'grrizzly', - 'maxloh', - 'varunjayaraman', + "bartushek", + "rynclark", + "henriquea", + "makuzaverite", + "MijaelWatts", + "Kolhar730", + "grrizzly", + "maxloh", + "varunjayaraman", ]; diff --git a/src/components/Contributors/Contributors.jsx b/src/components/Contributors/Contributors.jsx index 8d0eca7cc0b..aaf7af908a7 100644 --- a/src/components/Contributors/Contributors.jsx +++ b/src/components/Contributors/Contributors.jsx @@ -1,17 +1,9 @@ -import { useState } from 'react'; -import VisibilitySensor from 'react-visibility-sensor'; -import SmallIcon from '../../assets/icon-square-small-slack.png'; -import PropTypes from 'prop-types'; -import { contributorsNotFound } from './404.js'; +import PropTypes from "prop-types"; +import { useState } from "react"; +import VisibilitySensor from "react-visibility-sensor"; +import SmallIcon from "../../assets/icon-square-small-slack.png"; +import { contributorsNotFound } from "./404.js"; -Contributors.propTypes = { - contributors: PropTypes.array.isRequired, -}; - -Contributor.propTypes = { - contributor: PropTypes.string.isRequired, - inView: PropTypes.bool.isRequired, -}; function Contributor({ contributor, inView }) { return ( @@ -29,6 +21,11 @@ function Contributor({ contributor, inView }) { ); } +Contributor.propTypes = { + contributor: PropTypes.string.isRequired, + inView: PropTypes.bool.isRequired, +}; + export default function Contributors({ contributors }) { const [inView, setInView] = useState(false); const handleInView = (inView) => { @@ -51,7 +48,10 @@ export default function Contributors({ contributors }) {
{contributors - .filter((c) => contributorsNotFound.includes(c) === false) + .filter( + (contributor) => + contributorsNotFound.includes(contributor) === false, + ) .map((contributor) => ( ); } + +Contributors.propTypes = { + contributors: PropTypes.array.isRequired, +}; diff --git a/src/components/Cube/Cube.jsx b/src/components/Cube/Cube.jsx index 0450528c2f0..ba59ec6741f 100644 --- a/src/components/Cube/Cube.jsx +++ b/src/components/Cube/Cube.jsx @@ -1,9 +1,9 @@ // Import External Dependencies -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import { Component } from "react"; // Load Styling -import './Cube.scss'; +import "./Cube.scss"; export default class Cube extends Component { static propTypes = { @@ -17,7 +17,7 @@ export default class Cube extends Component { static defaultProps = { hover: false, - theme: 'dark', + theme: "dark", depth: 30, repeatDelay: 1000, }; @@ -30,8 +30,8 @@ export default class Cube extends Component { }; render() { - let { x, y, z } = this.state; - let { theme, depth, className = '' } = this.props; + const { x, y, z } = this.state; + const { theme, depth, className = "" } = this.props; return (
- {this._getFaces('outer')} + {this._getFaces("outer")}
- {this._getFaces('inner')} + {this._getFaces("inner")}
@@ -84,26 +84,27 @@ export default class Cube extends Component { } componentDidMount() { - let { hover, continuous, repeatDelay } = this.props; + const { hover, continuous, repeatDelay } = this.props; if (hover) { - this.container.addEventListener('mouseenter', this._spin); - this.container.addEventListener('mouseleave', this._reset); + this.container.addEventListener("mouseenter", this._spin); + this.container.addEventListener("mouseleave", this._reset); } else if (continuous) { let degrees = 0; - let axis = 'y'; + const axis = "y"; - let animation = () => { - let obj = {}; + const animation = () => { + const obj = {}; obj[axis] = degrees += 90; this.setState({ ...obj, iteration: (this.state.iteration + 1) % 4, }); + // eslint-disable-next-line no-use-before-define tick(); }; - let tick = () => + const tick = () => setTimeout(() => requestAnimationFrame(animation), repeatDelay); this._timeout = tick(); @@ -111,11 +112,11 @@ export default class Cube extends Component { } componentWillUnmount() { - let { hover, continuous } = this.props; + const { hover, continuous } = this.props; if (hover) { - this.container.removeEventListener('mouseenter', this._spin); - this.container.removeEventListener('mouseleave', this._reset); + this.container.removeEventListener("mouseenter", this._spin); + this.container.removeEventListener("mouseleave", this._reset); } else if (continuous) { clearTimeout(this._timeout); } @@ -128,7 +129,7 @@ export default class Cube extends Component { * @return {array} - An array of nodes */ _getFaces(type) { - let { iteration } = this.state; + const { iteration } = this.state; // Keep the thicker border on // the outside on each iteration @@ -172,15 +173,15 @@ export default class Cube extends Component { }; return [ - 'rotateX(0deg)', - 'rotateX(-90deg)', - 'rotateX(90deg)', - 'rotateY(-90deg)', - 'rotateY(90deg)', - 'rotateY(180deg)', + "rotateX(0deg)", + "rotateX(-90deg)", + "rotateX(90deg)", + "rotateY(-90deg)", + "rotateY(90deg)", + "rotateY(180deg)", ].map((rotation, i) => { const borderStyles = - type === 'outer' + type === "outer" ? { borderTopWidth: borderWidthMap[i].top[iteration], borderRightWidth: borderWidthMap[i].right[iteration], @@ -208,7 +209,7 @@ export default class Cube extends Component { * @return {string} - A random axis (i.e. x, y, or z) */ _getRandomAxis() { - let axes = Object.keys(this.state); + const axes = Object.keys(this.state); return axes[Math.floor(Math.random() * axes.length)]; } @@ -219,9 +220,9 @@ export default class Cube extends Component { * @param {object} e - Native event */ _spin = () => { - let obj = {}; - let axis = this._getRandomAxis(); - let sign = Math.random() < 0.5 ? -1 : 1; + const obj = {}; + const axis = this._getRandomAxis(); + const sign = Math.random() < 0.5 ? -1 : 1; obj[axis] = sign * 90; diff --git a/src/components/Cube/Cube.scss b/src/components/Cube/Cube.scss index a07a1574c23..543b2246f8d 100644 --- a/src/components/Cube/Cube.scss +++ b/src/components/Cube/Cube.scss @@ -1,4 +1,4 @@ -@import 'functions'; +@import "functions"; .cube { position: relative; diff --git a/src/components/Dropdown/Dropdown.jsx b/src/components/Dropdown/Dropdown.jsx index 41946b94d30..d3ff254674d 100644 --- a/src/components/Dropdown/Dropdown.jsx +++ b/src/components/Dropdown/Dropdown.jsx @@ -1,51 +1,53 @@ -import { Component } from 'react'; -import './Dropdown.scss'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import { Component } from "react"; + +import "./Dropdown.scss"; export default class Dropdown extends Component { static propTypes = { className: PropTypes.string, items: PropTypes.array, }; + state = { active: false, }; componentDidMount() { document.addEventListener( - 'keyup', + "keyup", this._closeDropdownOnEsc.bind(this), - true + true, ); document.addEventListener( - 'focus', + "focus", this._closeDropdownIfFocusLost.bind(this), - true + true, ); document.addEventListener( - 'click', + "click", this._closeDropdownIfFocusLost.bind(this), - true + true, ); } - _closeDropdownOnEsc(e) { - if (e.key === 'Escape' && this.state.active) { + _closeDropdownOnEsc(event) { + if (event.key === "Escape" && this.state.active) { this.setState({ active: false }, () => { this.dropdownButton.focus(); }); } } - _closeDropdownIfFocusLost(e) { - if (this.state.active && !this.dropdown.contains(e.target)) { + _closeDropdownIfFocusLost(event) { + if (this.state.active && !this.dropdown.contains(event.target)) { this.setState({ active: false }); } } render() { - let { className = '', items = [] } = this.props; - let activeMod = this.state.active ? 'dropdown__list--active' : ''; + const { className = "", items = [] } = this.props; + const activeMod = this.state.active ? "dropdown__list--active" : ""; return (
); } - _handleArrowKeys(currentIndex, lastIndex, e) { - if (['ArrowDown', 'ArrowUp'].includes(e.key)) { - e.preventDefault(); + _handleArrowKeys(currentIndex, lastIndex, event) { + if (["ArrowDown", "ArrowUp"].includes(event.key)) { + event.preventDefault(); } let newIndex = currentIndex; - if (e.key === 'ArrowDown') { + if (event.key === "ArrowDown") { newIndex++; if (newIndex > lastIndex) { newIndex = 0; } } - if (e.key === 'ArrowUp') { + if (event.key === "ArrowUp") { newIndex--; if (newIndex < 0) { newIndex = lastIndex; diff --git a/src/components/Dropdown/Dropdown.scss b/src/components/Dropdown/Dropdown.scss index 72c90df5035..1d63d89e012 100644 --- a/src/components/Dropdown/Dropdown.scss +++ b/src/components/Dropdown/Dropdown.scss @@ -1,4 +1,4 @@ -@import 'functions'; +@import "functions"; .dropdown { position: relative; @@ -22,7 +22,7 @@ .dropdown__arrow { &:before { - content: '\25be'; + content: "\25be"; } } diff --git a/src/components/Footer/Footer.jsx b/src/components/Footer/Footer.jsx index 317fdfec5c0..e0c9b474fa8 100644 --- a/src/components/Footer/Footer.jsx +++ b/src/components/Footer/Footer.jsx @@ -1,12 +1,51 @@ -import Link from '../Link/Link'; -import Container from '../Container/Container'; -import Icon from '../../assets/icon-square-small.svg'; -import CC from '../../assets/cc.svg'; -import BY from '../../assets/by.svg'; -import './Footer.scss'; +import BY from "../../assets/by.svg"; +import CC from "../../assets/cc.svg"; +import Icon from "../../assets/icon-square-small.svg"; +import OpenJSLogo from "../../assets/openjs-logo.png"; +import Container from "../Container/Container.jsx"; +import Link from "../Link/Link.jsx"; +import "./Footer.scss"; const Footer = () => (
{/* sub navigation */} {links - .filter((link) => { - // only those with children are displayed - return link.children; - }) + .filter( + (link) => + // only those with children are displayed + link.children, + ) .map((link) => { - if (link.isactive) { - // hide the children if the link is not active - if (!link.isactive({}, location)) { - return null; - } + if ( + link.isActive && // hide the children if the link is not active + !link.isActive({}, location) + ) { + return null; } return (
{link.children.map((child) => { const classNames = - 'text-blue-400 py-5 text-sm capitalize hover:text-black dark:hover:text-white'; + "text-blue-400 py-5 text-sm capitalize hover:text-black dark:hover:text-white"; const isActive = location.pathname.startsWith(child.url); return ( - {child.content === 'api' + {child.content === "api" ? child.content.toUpperCase() : child.content} @@ -260,4 +266,11 @@ function Navigation({ links, pathname, hash = '', toggleSidebar }) { ); } +Navigation.propTypes = { + pathname: PropTypes.string, + hash: PropTypes.string, + links: PropTypes.array, + toggleSidebar: PropTypes.func, +}; + export default Navigation; diff --git a/src/components/NotificationBar/MessageBar.jsx b/src/components/NotificationBar/MessageBar.jsx index ea5e009d7a7..b3035d12e52 100644 --- a/src/components/NotificationBar/MessageBar.jsx +++ b/src/components/NotificationBar/MessageBar.jsx @@ -1,26 +1,24 @@ -import CloseIcon from '../../styles/icons/cross.svg'; -import PropTypes from 'prop-types'; -import Content from './Notification.mdx'; -import { version, localStorageIsEnabled } from './NotificationBar'; -import { useTransition, animated, config } from '@react-spring/web'; -import { useEffect, useState } from 'react'; +import { animated, config, useTransition } from "@react-spring/web"; +import PropTypes from "prop-types"; +import { useEffect, useState } from "react"; +import CloseIcon from "../../styles/icons/cross.svg"; +import Content from "./Notification.mdx"; +import { localStorageIsEnabled, version } from "./NotificationBar.jsx"; -MessageBar.propTypes = { - onClose: PropTypes.func, -}; export default function MessageBar(props) { const [list, setList] = useState([]); const listTransitions = useTransition(list, { config: config.gentle, - from: { opacity: 0, transform: 'translate3d(-50%, 0px, 0px)' }, - enter: { opacity: 1, transform: 'translate3d(0px, 0px, 0px)' }, + from: { opacity: 0, transform: "translate3d(-50%, 0px, 0px)" }, + enter: { opacity: 1, transform: "translate3d(0px, 0px, 0px)" }, keys: list.map((item, index) => index), }); useEffect(() => { - setList(['']); + // eslint-disable-next-line react-hooks/set-state-in-effect + setList([""]); }, []); const close = () => { - localStorage.setItem('notification-dismissed', version); + localStorage.setItem("notification-dismissed", version); props.onClose(); }; return ( @@ -32,9 +30,10 @@ export default function MessageBar(props) { > {localStorageIsEnabled ? ( -
-
+ ) : null} ))} ); } + +MessageBar.propTypes = { + onClose: PropTypes.func, +}; diff --git a/src/components/NotificationBar/NotificationBar.jsx b/src/components/NotificationBar/NotificationBar.jsx index 8eac99fe382..99322748dc3 100644 --- a/src/components/NotificationBar/NotificationBar.jsx +++ b/src/components/NotificationBar/NotificationBar.jsx @@ -1,14 +1,14 @@ -import { useState, useEffect, lazy, Suspense } from 'react'; -import testLocalStorage from '../../utilities/test-local-storage'; +import { Suspense, lazy, useEffect, useState } from "react"; +import testLocalStorage from "../../utilities/test-local-storage.js"; -const MessageBar = lazy(() => import('./MessageBar')); +const MessageBar = lazy(() => import("./MessageBar.jsx")); -export const version = '3'; +export const version = "3"; export const localStorageIsEnabled = testLocalStorage() !== false; const barDismissed = () => { if (localStorageIsEnabled) { - return localStorage.getItem('notification-dismissed') === version; + return localStorage.getItem("notification-dismissed") === version; } return false; }; @@ -21,6 +21,7 @@ export default function NotificationBar() { }; useEffect(() => { // update dismissed value when component mounted + // eslint-disable-next-line react-hooks/set-state-in-effect setDismissed(() => barDismissed()); }, []); diff --git a/src/components/OfflineBanner/OfflineBanner.jsx b/src/components/OfflineBanner/OfflineBanner.jsx new file mode 100644 index 00000000000..a810db88cec --- /dev/null +++ b/src/components/OfflineBanner/OfflineBanner.jsx @@ -0,0 +1,49 @@ +import { useEffect, useState } from "react"; +import isClient from "../../utilities/is-client.js"; +import "./OfflineBanner.scss"; + +export default function OfflineBanner() { + const [online, setOnline] = useState(() => + isClient ? navigator.onLine : true, + ); + + useEffect(() => { + const goOnline = () => setOnline(true); + const goOffline = () => setOnline(false); + + window.addEventListener("online", goOnline); + window.addEventListener("offline", goOffline); + + return () => { + window.removeEventListener("online", goOnline); + window.removeEventListener("offline", goOffline); + }; + }, []); + + if (online) { + return null; + } + + return ( +
+
+ + + You are currently offline. Some features may be unavailable. + +
+
+ ); +} diff --git a/src/components/OfflineBanner/OfflineBanner.scss b/src/components/OfflineBanner/OfflineBanner.scss new file mode 100644 index 00000000000..3d08734b08b --- /dev/null +++ b/src/components/OfflineBanner/OfflineBanner.scss @@ -0,0 +1,29 @@ +.offline-banner { + display: flex; + align-items: center; + justify-content: center; + padding: 8px 20px; + background-color: #2b3a42; + color: #fff; + font-size: 14px; + font-weight: 500; + text-align: center; + border-bottom: 1px solid #465e69; + + &__content { + display: flex; + align-items: center; + gap: 8px; + } + + &__icon { + flex-shrink: 0; + width: 16px; + height: 16px; + fill: #8dd6f9; + } + + &__text { + letter-spacing: 0.01em; + } +} diff --git a/src/components/OfflineBanner/OfflineBanner.test.jsx b/src/components/OfflineBanner/OfflineBanner.test.jsx new file mode 100644 index 00000000000..a7ca5a3435b --- /dev/null +++ b/src/components/OfflineBanner/OfflineBanner.test.jsx @@ -0,0 +1,103 @@ +/** + * @jest-environment jsdom + */ + +// eslint-disable-next-line import/no-extraneous-dependencies +import { afterEach, beforeEach, describe, expect, it } from "@jest/globals"; +import renderer from "react-test-renderer"; +import OfflineBanner from "./OfflineBanner.jsx"; + +/** + * Helper to mock navigator.onLine + */ +function setNavigatorOnLine(value) { + Object.defineProperty(window.navigator, "onLine", { + configurable: true, + get: () => value, + }); +} + +describe("OfflineBanner", () => { + let originalOnLine; + + beforeEach(() => { + // Save original descriptor + originalOnLine = Object.getOwnPropertyDescriptor( + window.navigator, + "onLine", + ); + }); + + afterEach(() => { + // Restore original descriptor + if (originalOnLine) { + Object.defineProperty(window.navigator, "onLine", originalOnLine); + } else { + delete window.navigator.onLine; + } + }); + + it("renders nothing when online", () => { + setNavigatorOnLine(true); + const tree = renderer.create().toJSON(); + expect(tree).toBeNull(); + }); + + it("renders the offline banner when offline", () => { + setNavigatorOnLine(false); + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); + + it("shows the correct offline message text", () => { + setNavigatorOnLine(false); + const { root } = renderer.create(); + const textEl = root.findByProps({ className: "offline-banner__text" }); + expect(textEl.children).toContain( + "You are currently offline. Some features may be unavailable.", + ); + }); + + it("has accessible role and aria-live attributes", () => { + setNavigatorOnLine(false); + const { root } = renderer.create(); + const banner = root.findByProps({ "data-testid": "offline-banner" }); + expect(banner.props.role).toBe("status"); + expect(banner.props["aria-live"]).toBe("polite"); + }); + + it("shows banner when window goes offline", () => { + setNavigatorOnLine(true); + let component; + renderer.act(() => { + component = renderer.create(); + }); + // Initially online — nothing rendered + expect(component.toJSON()).toBeNull(); + + // Simulate going offline + renderer.act(() => { + setNavigatorOnLine(false); + window.dispatchEvent(new Event("offline")); + }); + expect(component.toJSON()).not.toBeNull(); + expect(component.toJSON()).toMatchSnapshot(); + }); + + it("hides banner when window comes back online", () => { + setNavigatorOnLine(false); + let component; + renderer.act(() => { + component = renderer.create(); + }); + // Initially offline — banner shown + expect(component.toJSON()).not.toBeNull(); + + // Simulate going online + renderer.act(() => { + setNavigatorOnLine(true); + window.dispatchEvent(new Event("online")); + }); + expect(component.toJSON()).toBeNull(); + }); +}); diff --git a/src/components/OfflineBanner/__snapshots__/OfflineBanner.test.jsx.snap b/src/components/OfflineBanner/__snapshots__/OfflineBanner.test.jsx.snap new file mode 100644 index 00000000000..dd0dbf640ab --- /dev/null +++ b/src/components/OfflineBanner/__snapshots__/OfflineBanner.test.jsx.snap @@ -0,0 +1,59 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`OfflineBanner renders the offline banner when offline 1`] = ` +
+
+ + + You are currently offline. Some features may be unavailable. + +
+
+`; + +exports[`OfflineBanner shows banner when window goes offline 1`] = ` +
+
+ + + You are currently offline. Some features may be unavailable. + +
+
+`; diff --git a/src/components/Page/AdjacentPages.jsx b/src/components/Page/AdjacentPages.jsx index a99d9883da4..17521292a7a 100644 --- a/src/components/Page/AdjacentPages.jsx +++ b/src/components/Page/AdjacentPages.jsx @@ -1,16 +1,6 @@ -import Link from '../Link/Link'; -import './AdjacentPages.scss'; -import PropTypes from 'prop-types'; -AdjacentPages.propTypes = { - previous: PropTypes.shape({ - url: PropTypes.string, - title: PropTypes.string, - }), - next: PropTypes.shape({ - url: PropTypes.string, - title: PropTypes.string, - }), -}; +import PropTypes from "prop-types"; +import Link from "../Link/Link.jsx"; +import "./AdjacentPages.scss"; export default function AdjacentPages({ previous, next }) { if (!previous && !next) return null; @@ -35,3 +25,14 @@ export default function AdjacentPages({ previous, next }) {
); } + +AdjacentPages.propTypes = { + previous: PropTypes.shape({ + url: PropTypes.string, + title: PropTypes.string, + }), + next: PropTypes.shape({ + url: PropTypes.string, + title: PropTypes.string, + }), +}; diff --git a/src/components/Page/Page.jsx b/src/components/Page/Page.jsx index 49a683de69d..271a77ca8ce 100644 --- a/src/components/Page/Page.jsx +++ b/src/components/Page/Page.jsx @@ -1,19 +1,20 @@ // Import External Dependencies -import { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import { useLocation } from 'react-router-dom'; +import PropTypes from "prop-types"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; // Import Components -import PageLinks from '../PageLinks/PageLinks'; -import Markdown from '../Markdown/Markdown'; -import Contributors from '../Contributors/Contributors'; -import Translators from '../Translators/Translators'; -import { PlaceholderString } from '../Placeholder/Placeholder'; -import AdjacentPages from './AdjacentPages'; +import Contributors from "../Contributors/Contributors.jsx"; +import Link from "../Link/Link.jsx"; +import Markdown from "../Markdown/Markdown.jsx"; +import PageLinks from "../PageLinks/PageLinks.jsx"; +import { placeholderString } from "../Placeholder/Placeholder.jsx"; +import Translators from "../Translators/Translators.jsx"; +import AdjacentPages from "./AdjacentPages.jsx"; // Load Styling -import './Page.scss'; -import Link from '../Link/Link'; +import "./Page.scss"; + export default function Page(props) { const { title, @@ -28,12 +29,10 @@ export default function Page(props) { const isDynamicContent = props.content instanceof Promise; const [content, setContent] = useState( isDynamicContent - ? PlaceholderString() - : () => props.content.default || props.content - ); - const [contentLoaded, setContentLoaded] = useState( - isDynamicContent ? false : true + ? placeholderString() + : () => props.content.default || props.content, ); + const [contentLoaded, setContentLoaded] = useState(!isDynamicContent); useEffect(() => { if (props.content instanceof Promise) { @@ -42,7 +41,7 @@ export default function Page(props) { setContent(() => mod.default || mod); setContentLoaded(true); }) - .catch(() => setContent('Error loading content.')); + .catch(() => setContent("Error loading content.")); } }, [props.content]); @@ -52,16 +51,18 @@ export default function Page(props) { let observer; if (contentLoaded) { if (hash) { - const target = document.querySelector('#md-content'); + const target = document.querySelector("#md-content"); // two cases here // 1. server side rendered page, so hash target is already there - if (document.querySelector(hash)) { - document.querySelector(hash).scrollIntoView(); + // Note: Why this change because we use getElementById instead of querySelector(hash) here because + // CSS selectors cannot start with a digit (e.g. #11-in-scope is invalid) + if (document.getElementById(hash.slice(1))) { + document.getElementById(hash.slice(1)).scrollIntoView(); } else { // 2. dynamic loaded content // we need to observe the dom change to tell if hash exists observer = new MutationObserver(() => { - const element = document.querySelector(hash); + const element = document.getElementById(hash.slice(1)); if (element) { element.scrollIntoView(); } @@ -92,7 +93,7 @@ export default function Page(props) { let contentRender; - if (typeof content === 'function') { + if (typeof content === "function") { contentRender = content({}).props.children; } else { contentRender = ( @@ -104,7 +105,7 @@ export default function Page(props) { ); } return ( -
+

{title}

@@ -141,8 +142,8 @@ export default function Page(props) { {loadContributors && (

- {numberOfContributors}{' '} - {numberOfContributors === 1 ? 'Contributor' : 'Contributors'} + {numberOfContributors}{" "} + {numberOfContributors === 1 ? "Contributor" : "Contributors"}

@@ -156,9 +157,10 @@ export default function Page(props) { )}
-
+ ); } + Page.propTypes = { title: PropTypes.string, contributors: PropTypes.array, @@ -168,6 +170,7 @@ Page.propTypes = { next: PropTypes.object, content: PropTypes.oneOfType([ PropTypes.shape({ + // eslint-disable-next-line unicorn/no-thenable then: PropTypes.func.isRequired, default: PropTypes.string, }), diff --git a/src/components/Page/Page.scss b/src/components/Page/Page.scss index 32270011707..82650d96269 100644 --- a/src/components/Page/Page.scss +++ b/src/components/Page/Page.scss @@ -1,5 +1,5 @@ -@import 'mixins'; -@import 'functions'; +@import "mixins"; +@import "functions"; .page { flex: 1 1 auto; diff --git a/src/components/PageLinks/PageLinks.jsx b/src/components/PageLinks/PageLinks.jsx index 455abbbca1e..40c9974d76a 100644 --- a/src/components/PageLinks/PageLinks.jsx +++ b/src/components/PageLinks/PageLinks.jsx @@ -1,22 +1,23 @@ -import Url from 'url'; -import PropTypes from 'prop-types'; +// eslint-disable-next-line n/prefer-node-protocol +import Url from "url"; +import PropTypes from "prop-types"; -const baseURL = 'https://github.com/line/webpack.kr/edit/kr/'; - -PageLinks.propTypes = { - page: PropTypes.shape({ - repo: PropTypes.string, - }), -}; +const baseURL = "https://github.com/line/webpack.kr/edit/kr/"; function Separator() { return ·; } const classes = - 'text-gray-500 dark:text-gray-500 text-sm cursor-pointer font-sans hover:underline'; + "text-gray-500 dark:text-gray-500 text-sm cursor-pointer font-sans hover:underline"; + +function _handlePrintClick(event) { + event.preventDefault(); + window.print(); +} export default function PageLinks({ page = {} }) { + // eslint-disable-next-line n/no-deprecated-api const editLink = page.edit || Url.resolve(baseURL, page.path); return ( @@ -40,7 +41,8 @@ export default function PageLinks({ page = {} }) { ); } -function _handlePrintClick(e) { - e.preventDefault(); - window.print(); -} +PageLinks.propTypes = { + page: PropTypes.shape({ + repo: PropTypes.string, + }), +}; diff --git a/src/components/PageNotFound/PageNotFound.jsx b/src/components/PageNotFound/PageNotFound.jsx index be6a243a74d..8180f574b8f 100644 --- a/src/components/PageNotFound/PageNotFound.jsx +++ b/src/components/PageNotFound/PageNotFound.jsx @@ -1,11 +1,16 @@ -import { Link } from 'react-router-dom'; +import { Helmet } from "react-helmet-async"; +import { Link } from "react-router-dom"; // Styles -import './PageNotFound.scss'; +import "./PageNotFound.scss"; export default function PageNotFound() { return (
+ + Page Not Found | webpack + +

Page Not Found

Oops! The page you are looking for has been removed or relocated.

diff --git a/src/components/PageNotFound/PageNotFound.test.jsx b/src/components/PageNotFound/PageNotFound.test.jsx new file mode 100644 index 00000000000..9a9f5f17c38 --- /dev/null +++ b/src/components/PageNotFound/PageNotFound.test.jsx @@ -0,0 +1,21 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { describe, expect, it } from "@jest/globals"; +import { HelmetProvider } from "react-helmet-async"; +import { MemoryRouter } from "react-router-dom"; +import renderer from "react-test-renderer"; +import PageNotFound from "./PageNotFound.jsx"; + +describe("PageNotFound", () => { + it("renders correctly", () => { + const tree = renderer + .create( + + + + + , + ) + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/PageNotFound/__snapshots__/PageNotFound.test.jsx.snap b/src/components/PageNotFound/__snapshots__/PageNotFound.test.jsx.snap new file mode 100644 index 00000000000..df6bbb63675 --- /dev/null +++ b/src/components/PageNotFound/__snapshots__/PageNotFound.test.jsx.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`PageNotFound renders correctly 1`] = ` +
+`; diff --git a/src/components/Placeholder/Placeholder.js b/src/components/Placeholder/Placeholder.jsx similarity index 88% rename from src/components/Placeholder/Placeholder.js rename to src/components/Placeholder/Placeholder.jsx index d2fc646fe2e..61503b53adf 100644 --- a/src/components/Placeholder/Placeholder.js +++ b/src/components/Placeholder/Placeholder.jsx @@ -1,7 +1,7 @@ -import './Placeholder.scss'; +import "./Placeholder.scss"; // Placeholder string -const PlaceholderString = () => ` +const placeholderString = () => `

 

 

@@ -31,4 +31,4 @@ function PlaceholderComponent() { ); } -export { PlaceholderString, PlaceholderComponent }; +export { PlaceholderComponent, placeholderString }; diff --git a/src/components/Placeholder/Placeholder.scss b/src/components/Placeholder/Placeholder.scss index 50ac2e52776..0e9a5e2b2ad 100644 --- a/src/components/Placeholder/Placeholder.scss +++ b/src/components/Placeholder/Placeholder.scss @@ -1,4 +1,4 @@ -@import 'functions'; +@import "functions"; .placeholder { h2, @@ -17,7 +17,7 @@ #fcfcfc, getColor(concrete) ); - content: ''; + content: ""; left: 0; height: 100%; position: absolute; diff --git a/src/components/Placeholder/Placeholder.test.jsx b/src/components/Placeholder/Placeholder.test.jsx new file mode 100644 index 00000000000..fd215b1a468 --- /dev/null +++ b/src/components/Placeholder/Placeholder.test.jsx @@ -0,0 +1,11 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import { describe, expect, it } from "@jest/globals"; +import renderer from "react-test-renderer"; +import { PlaceholderComponent } from "./Placeholder.jsx"; + +describe("PlaceholderComponent", () => { + it("renders correctly", () => { + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Placeholder/__snapshots__/Placeholder.test.jsx.snap b/src/components/Placeholder/__snapshots__/Placeholder.test.jsx.snap new file mode 100644 index 00000000000..c99c1c3b70b --- /dev/null +++ b/src/components/Placeholder/__snapshots__/Placeholder.test.jsx.snap @@ -0,0 +1,17 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`PlaceholderComponent renders correctly 1`] = ` +
+

+   +

+

+   +

+

+   +

+
+`; diff --git a/src/components/Print/Print.js b/src/components/Print/Print.jsx similarity index 70% rename from src/components/Print/Print.js rename to src/components/Print/Print.jsx index f278c17eec5..e3fcb808743 100644 --- a/src/components/Print/Print.js +++ b/src/components/Print/Print.jsx @@ -1,21 +1,27 @@ -import './Print.scss'; -import icon from '../../assets/icon-print.svg'; -import BarIcon from '../../styles/icons/vertical-bar.svg'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import "./Print.scss"; +import icon from "../../assets/icon-print.svg"; +import BarIcon from "../../styles/icons/vertical-bar.svg"; const PRINTABLE_SECTIONS = [ - 'api', - 'concepts', - 'configuration', - 'contribute', - 'guides', - 'loaders', - 'migrate', - 'plugins', + "api", + "concepts", + "configuration", + "contribute", + "guides", + "loaders", + "migrate", + "plugins", ]; -Print.propTypes = { - url: PropTypes.string, -}; + +function _printPageUrlFromUrl(urlRaw) { + const urlSplit = urlRaw.split("/"); + + return PRINTABLE_SECTIONS.includes(urlSplit[1]) + ? `/${urlSplit[1]}/printable/` + : false; +} + export default function Print(props) { const { url } = props; const printUrl = _printPageUrlFromUrl(url); @@ -26,7 +32,7 @@ export default function Print(props) { } return ( -
+
; } diff --git a/src/components/Shield/Shield.jsx b/src/components/Shield/Shield.jsx index 5f1e80afdf8..d7cbdfe68ea 100644 --- a/src/components/Shield/Shield.jsx +++ b/src/components/Shield/Shield.jsx @@ -1,4 +1,5 @@ -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; + const Shield = (props) => ( webpack shield { + it("renders correctly with content and label props", () => { + const tree = renderer + .create() + .toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/src/components/Shield/__snapshots__/Shield.test.jsx.snap b/src/components/Shield/__snapshots__/Shield.test.jsx.snap new file mode 100644 index 00000000000..d4f40eb531e --- /dev/null +++ b/src/components/Shield/__snapshots__/Shield.test.jsx.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`Shield renders correctly with content and label props 1`] = ` +webpack shield +`; diff --git a/src/components/Sidebar/Sidebar.jsx b/src/components/Sidebar/Sidebar.jsx index 5c01991eaf0..64e2ee3ba70 100644 --- a/src/components/Sidebar/Sidebar.jsx +++ b/src/components/Sidebar/Sidebar.jsx @@ -1,30 +1,24 @@ // Import External Dependencies -import SidebarItem from '../SidebarItem/SidebarItem'; -import Print from '../Print/Print'; -import PropTypes from 'prop-types'; +import PropTypes from "prop-types"; +import { useEffect, useState } from "react"; +import DownIcon from "../../styles/icons/chevron-down.svg"; +import LoadingIcon from "../../styles/icons/loading.svg"; +import Print from "../Print/Print.jsx"; +import SidebarItem from "../SidebarItem/SidebarItem.jsx"; // Load Styling -import './Sidebar.scss'; -import { useEffect, useState } from 'react'; - -import DownIcon from '../../styles/icons/chevron-down.svg'; -import LoadingIcon from '../../styles/icons/loading.svg'; +import "./Sidebar.scss"; const versions = [5, 4]; const currentDocsVersion = 5; -Sidebar.propTypes = { - className: PropTypes.string, - pages: PropTypes.array, - currentPage: PropTypes.string, -}; // Create and export the component -export default function Sidebar({ className = '', pages, currentPage }) { +export default function Sidebar({ className = "", pages, currentPage }) { const [version, setVersion] = useState(currentDocsVersion); const [loading, setLoading] = useState(false); useEffect(() => { if (version === currentDocsVersion) return; - const href = window.location.href; + const { href } = window.location; const url = new URL(href); url.hostname = `v${version}.webpack.js.org`; @@ -37,12 +31,16 @@ export default function Sidebar({ className = '', pages, currentPage }) {