diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 0000000000000..f53ec0ae2900b --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,11 @@ +# Changesets + +Changesets in this directory describe user-facing changes to published packages. Add one by running: + +```shell +pnpm changeset +``` + +Commit the generated Markdown file with the package change. The publishing workflow consumes these files to update package versions and changelogs before publishing to npm. + +See the [package publishing guide](../docs/package-publishing.md) for the complete release process. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 0000000000000..19d462977d6da --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.4/schema.json", + "changelog": [ + "@changesets/changelog-github", + { "repo": "nodejs/nodejs.org" } + ], + "commit": false, + "fixed": [], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": [] +} diff --git a/.changeset/import-aliases-no-preconditions.md b/.changeset/import-aliases-no-preconditions.md new file mode 100644 index 0000000000000..252399d58c0f9 --- /dev/null +++ b/.changeset/import-aliases-no-preconditions.md @@ -0,0 +1,8 @@ +--- +'@node-core/ui-components': patch +--- + +Resolve the `#ui/*` import alias to the compiled output by default, so consumers +of the published package no longer need to opt into a bundler-specific +`rolldown` resolution condition. The uncompiled sources stay reachable through +the new `source` condition, which this repository's own tooling opts into. diff --git a/.changeset/shiki-more-more.md b/.changeset/shiki-more-more.md new file mode 100644 index 0000000000000..9f206d3fe804a --- /dev/null +++ b/.changeset/shiki-more-more.md @@ -0,0 +1,5 @@ +--- +'@node-core/rehype-shiki': minor +--- + +Add support for all Shiki languages when the `minimal` preset is not selected. diff --git a/.editorconfig b/.editorconfig index ac51662df4d97..5c165c38584da 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,13 +1,10 @@ -# http://editorconfig.org - root = true [*] indent_style = space indent_size = 2 end_of_line = lf -insert_final_newline = true +charset = utf-8 trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false +insert_final_newline = true +max_line_length = 80 diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index e3578aadfd3a9..0000000000000 --- a/.eslintrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "standard" -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000..205021e49dd1f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Enforce Unix newlines +* text=auto eol=lf diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000000000..158da6920f6c7 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,50 @@ +# Website Development +* @nodejs/nodejs-website + +# Infrastructure +.github @nodejs/web-infra +.husky @nodejs/web-infra +.nvmrc @nodejs/web-infra +codecov.yml @nodejs/web-infra +packages/ui-components/scripts/publish.mjs @nodejs/web-infra + +# Dependencies +pnpm-workspace.yaml @nodejs/nodejs-website @nodejs/web-infra +pnpm-lock.yaml @nodejs/web-infra + +# Framework +apps/site/next.config.mjs @nodejs/web-infra +apps/site/next.dynamic.mjs @nodejs/web-infra +apps/site/middleware.ts @nodejs/web-infra +apps/site/navigation.mjs @nodejs/web-infra +apps/site/playwright.config.mjs @nodejs/web-infra + +# Package Ecosystem +package.json @nodejs/nodejs-website +turbo.json @nodejs/nodejs-website @nodejs/web-infra + +# Web Infrastructure +crowdin.yml @nodejs/web-infra +apps/site/redirects.json @nodejs/web-infra +apps/site/site.json @nodejs/web-infra +platforms @nodejs/web-infra + +# Critical Documents +LICENSE @nodejs/tsc +CONTRIBUTING.md @nodejs/nodejs-website @nodejs/web-infra +docs @nodejs/nodejs-website @nodejs/web-infra +SECURITY.md @nodejs/security-wg +apps/site/public/.well-known/security.txt @nodejs/security-wg + +# Node.js Release Blog Posts +apps/site/pages/en/blog/release @nodejs/releasers +apps/site/pages/en/blog/announcements @nodejs/releasers +apps/site/pages/en/blog/vulnerability @nodejs/releasers + +# Marketing +apps/site/pages/en/about/partners.mdx @nodejs/marketing +apps/site/pages/en/about/branding.mdx @nodejs/marketing + +# The following users DO NOT have write access, and their review is requested +# via a GitHub action. +apps/site/pages/en/blog/migrations @nodejs/userland-migrations diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 249979ae08105..0000000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,22 +0,0 @@ - - -* **URL**: -* **Browser version**: -* **Operating system**: - - diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml new file mode 100644 index 0000000000000..281e5e0159968 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -0,0 +1,50 @@ +name: Report a Technical/Visual Issue on the Node.js Website +description: 'Is something not working as expected? Did you encounter a glitch or a bug with the Website?' +labels: [bug] +body: + - type: markdown + attributes: + value: | + Thanks for reporting an issue you've found on the nodejs.org website. + Please fill in the template below. If unsure about something, just do as best + as you're able. If you are reporting a visual glitch, it will be much easier + for us to fix it when you attach a screenshot as well. + - type: input + attributes: + label: 'URL:' + description: The URL of the page you are reporting an issue on. + placeholder: https://nodejs.org/en/ + validations: + required: true + - type: input + attributes: + label: 'Browser Name:' + description: What kind of browser are you using? + placeholder: Chrome + validations: + required: true + - type: input + attributes: + label: 'Browser Version:' + description: What version of browser are you using? + placeholder: '103.0.5060.134' + validations: + required: true + - type: input + attributes: + label: 'Operating System:' + description: What kind of operation system are you using + (Write it in full, with version number)? + placeholder: 'Windows 10, 21H2, 19044.1826' + validations: + required: true + - type: textarea + attributes: + label: 'How to reproduce the issue:' + placeholder: | + 1. What I did. + 2. What I expected to happen. + 3. What I actually got. + 4. If possible, images or videos are welcome. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/02-feature-request.yml b/.github/ISSUE_TEMPLATE/02-feature-request.yml new file mode 100644 index 0000000000000..da59bf9dbc09b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02-feature-request.yml @@ -0,0 +1,19 @@ +name: Suggest a new feature or improvement for the Node.js Website +description: 'Do you have an idea or a suggestion and you want to share?' +labels: [feature request] +body: + - type: markdown + attributes: + value: | + You have an idea how to improve the site? That's awesome! + Before submitting, please have a look at the existing issues if there's already + something related to your suggestion. + - type: textarea + attributes: + label: 'Enter your suggestions in details:' + placeholder: | + 1. What I expected to happen. + 2. Your reason (if possible, images or videos are welcome). + 3. What I plan to do (Optional but better). + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..ce6f540b49da4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,17 @@ +blank_issues_enabled: true +contact_links: + - name: Node.js Status Page + url: https://status.nodejs.org + about: 'Need to check if there is any ongoing incidents?' + - name: Report an API Docs Issue on the Node.js Website + url: https://github.com/nodejs/node/issues/new?assignees=&labels=doc&template=3-api-ref-docs-problem.yml + about: 'Is something wrong with the API Docs? Did you face a bug with the API Docs?' + - name: Report an issue with downloading Node.js + url: https://github.com/nodejs/release-cloudflare-worker/issues/new + about: 'Is something wrong with Node.js downloads?' + - name: Report a Translation Issue on the Node.js Website + url: https://crowdin.com/project/nodejs-web + about: 'Is something wrong in a specific translation? Do you believe a language can get improved? Do you have suggestions?' + - name: Need help with Node.js? + url: https://github.com/nodejs/help/issues/ + about: "Struggling with Node.js? You're not sure how to code?" diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000000..fc641b4b78268 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,32 @@ + + +## Description + + + +## Validation + + + +## Related Issues + + + +### Check List + + + +- [ ] I have read the [Contributing Guidelines](https://github.com/nodejs/nodejs.org/blob/main/CONTRIBUTING.md) and made commit messages that follow the guideline. +- [ ] I have run `pnpm format` to ensure the code follows the style guide. +- [ ] I have run `pnpm test` to check if all tests are passing. +- [ ] I have run `pnpm build` to check if the website builds without errors. +- [ ] I've covered new added functionality with unit tests if necessary. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000000..bb9dc3da2e04c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,106 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: '/' + schedule: + interval: monthly + labels: + - 'dependencies' + - 'github_actions' + - 'github_actions:pull-request' + - 'auto-merge' + commit-message: + prefix: chore(deps) + cooldown: + default-days: 7 + open-pull-requests-limit: 10 + + - package-ecosystem: npm + directory: '/' + versioning-strategy: increase + schedule: + interval: monthly + labels: + - 'dependencies' + - 'javascript' + - 'github_actions:pull-request' + - 'auto-merge' + commit-message: + prefix: chore(deps) + cooldown: + default-days: 7 + groups: + lint: + patterns: + - '@eslint/*' + - '@typescript-eslint/*' + - acorn + - eslint + - eslint-* + - lint-staged + - prettier + - prettier-* + - stylelint + - stylelint-* + - typescript-eslint + - unified + exclude-patterns: + - 'eslint-plugin-storybook' + - 'prettier-plugin-tailwindcss' + mdx: + patterns: + - '@vcarl/remark-headings' + - '@shikijs/*' + - '@mdx-js/*' + - hast-util-* + - rehype-* + - remark-* + - shiki + - sval + - unist-util-* + - vfile + - vfile-* + - reading-time + - twoslash + orama: + patterns: + - '@orama/*' + - '@oramacloud/*' + radix: + patterns: + - '@radix-ui/*' + react: + patterns: + - 'react' + - 'react-dom' + - '@types/react' + storybook: + patterns: + - 'storybook' + - '@storybook/*' + - 'eslint-plugin-storybook' + styling: + patterns: + - '@savvywombat/tailwindcss-grid-areas' + - '@tailwindcss/*' + - 'prettier-plugin-tailwindcss' + - 'tailwindcss' + testing: + patterns: + - '@testing-library/*' + - '@reporters/*' + - global-jsdom + - jsdom + - tsx + vercel: + patterns: + - '@next/*' + - '@opentelemetry/*' + - '@vercel/*' + - next + - next-* + - turbo + ignore: + - dependency-name: '@types/node' + update-types: ['version-update:semver-major'] + open-pull-requests-limit: 10 diff --git a/.github/scorecard.yml b/.github/scorecard.yml new file mode 100644 index 0000000000000..e461abd489a61 --- /dev/null +++ b/.github/scorecard.yml @@ -0,0 +1,8 @@ +# annotations tell scorecard that we have mitigated a concern. automation is only so good at establishing context +# https://github.com/ossf/scorecard/blob/main/config/README.md#annotating-your-project +annotations: + # our workflows only run when a maintainer allows it + - checks: + - dangerous-workflow + reasons: + - reason: remediated diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000000000..2fa6e29c31ab4 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,33 @@ +name: Auto-merge PRs + +on: + schedule: + - cron: '*/15 * * * *' + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: {} + +jobs: + auto-merge: + name: Auto-merge + if: github.repository == 'nodejs/nodejs.org' + runs-on: ubuntu-latest + permissions: + # Required to approve and merge pull requests + pull-requests: write + # Required to merge pull requests via merge queue + contents: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: nodejs/web-team/actions/auto-merge-prs@b087df186d25f8792fb85cc7794f68718726b8ee + with: + merge-method: queue diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000000000..d822f64cf115a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,157 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Build + +on: + push: + branches: + - main + pull_request: + branches: + - main + types: [opened, synchronize, reopened, ready_for_review] + merge_group: + +defaults: + run: + # This ensures that the working directory is the root of the repository + working-directory: ./ + +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + build: + name: Build on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + + - name: Use GNU tar instead BSD tar + # This ensures that we use GNU `tar` which is more efficient for extracting caches's + if: matrix.os == 'windows-latest' + shell: cmd + run: echo C:\Program Files\Git\usr\bin>>"%GITHUB_PATH%" + + - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ${{ github.workspace }}/apps/site/.next/cache + key: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }} + restore-keys: ${{ runner.os }}-nextjs-${{ hashFiles('**/pnpm-lock.yaml') }}- + + - name: Build Next.js + # We want a ISR build on CI to ensure that regular Next.js builds work as expected. + run: node_modules/.bin/turbo build --filter=@node-core/website --cache-dir=.turbo/cache + env: + # We want to ensure we have enough RAM allocated to the Node.js process + # this should be a last resort in case by any chances the build memory gets too high + # but in general this should never happen + NODE_OPTIONS: '--max_old_space_size=4096' + # See https://github.com/vercel/next.js/pull/90949 + TURBOPACK_STATS: ${{ matrix.os == 'ubuntu-latest' }} + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + if: matrix.os == 'ubuntu-latest' + with: + name: webpack-stats + path: apps/site/.next/diagnostics/route-bundle-stats.json + + - name: Build Next.js (Static Export) + # We want to generate a static build, as it is a requirement of our website. + run: node_modules/.bin/turbo deploy --filter=@node-core/website --cache-dir=.turbo/cache + env: + # We want to ensure we have enough RAM allocated to the Node.js process + # this should be a last resort in case by any chances the build memory gets too high + # but in general this should never happen + NODE_OPTIONS: '--max_old_space_size=4096' + # We want to ensure that static exports for all locales do not occur on `pull_request` events + # TODO: The output of this is too large, and it crashes the GitHub Runner + NEXT_PUBLIC_STATIC_EXPORT_LOCALE: false # ${{ github.event_name == 'push' }} + + compare-bundle-size: + name: Compare Bundle Size + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'pull_request' + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Git Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Download Stats (HEAD) + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: webpack-stats + path: head-stats + + - name: Get Run ID from BASE + id: base-run + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + GH_TOKEN: ${{ github.token }} + run: | + ID=$(gh run list -c "$BASE_SHA" -w build.yml -s success -L 1 --json databaseId --jq ".[].databaseId") + echo "run_id=$ID" >> "$GITHUB_OUTPUT" + + - name: Download Stats (BASE) + id: base-stats + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: webpack-stats + path: base-stats + run-id: ${{ steps.base-run.outputs.run_id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Compare Bundle Size + id: compare-bundle-size + if: steps.base-stats.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + HEAD_STATS_PATH: ./head-stats/route-bundle-stats.json + BASE_STATS_PATH: ./base-stats/route-bundle-stats.json + with: + script: | + const { compare } = await import('${{github.workspace}}/apps/site/scripts/compare-size/index.mjs') + await compare({core}) + + - name: Prepare Comment + if: steps.base-stats.outcome == 'success' + env: + COMMENT: ${{ steps.compare-bundle-size.outputs.comment }} + run: | + mkdir -p pr-comment + printf '%s' "$COMMENT" > pr-comment/comment.md + printf '%s' 'compare_bundle_size' > pr-comment/tag.txt + + - name: Upload Comment + if: steps.base-stats.outcome == 'success' + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-comment + path: pr-comment/ diff --git a/.github/workflows/bump-security-txt-expiry.yml b/.github/workflows/bump-security-txt-expiry.yml new file mode 100644 index 0000000000000..97cb139347bf1 --- /dev/null +++ b/.github/workflows/bump-security-txt-expiry.yml @@ -0,0 +1,65 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Bump security.txt Expiry + +on: + workflow_dispatch: + schedule: + - cron: '0 8 1 1 *' # once a year, Jan 1 at 08:00 UTC + +permissions: {} + +jobs: + bump-expiry: + runs-on: ubuntu-latest + if: github.repository_owner == 'nodejs' + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Bump the Expires year + id: update-expiry + env: + SECURITY_TXT: apps/site/public/.well-known/security.txt + run: | + set -euo pipefail + old=$(sed -n 's/^Expires: *//p' "$SECURITY_TXT") + year=${old%%-*} + new="$((year + 1))${old#"$year"}" + sed -i "s|^Expires: .*|Expires: ${new}|" "$SECURITY_TXT" + { + echo "old=${old}" + echo "new=${new}" + } >> "$GITHUB_OUTPUT" + + - name: Open pull request + uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + update-pull-request-title-and-body: true + branch: chore/update-security-txt + body: | + Updates the `Expiry` on our [`security.txt`](https://nodejs.org/.well-known/security.txt) from ${{ steps.update-expiry.outputs.old }} to ${{ steps.update-expiry.outputs.new }} + + Before merging, please explicitly confirm that: + - The security contact listed is still correct and actively monitored + - The linked security policy is still accurate + - The project's preferred vulnerability disclosure path has not changed + + cc @nodejs/security-wg + + Check this workflow's logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. + commit-message: 'meta: update security `Expiry` from ${{ steps.update-expiry.outputs.old }} to ${{ steps.update-expiry.outputs.new }}' + title: 'meta: update security `Expiry` from ${{ steps.update-expiry.outputs.old }} to ${{ steps.update-expiry.outputs.new }}' + draft: true + labels: security diff --git a/.github/workflows/chromatic.yml b/.github/workflows/chromatic.yml new file mode 100644 index 0000000000000..48af4d5ab7836 --- /dev/null +++ b/.github/workflows/chromatic.yml @@ -0,0 +1,72 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Chromatic + +on: + merge_group: + push: + branches: + - main + paths: + - packages/ui-components/** + - .github/workflows/chromatic.yml + pull_request: + branches: + - main + paths: + - packages/ui-components/** + - .github/workflows/chromatic.yml + workflow_dispatch: + +defaults: + run: + # This ensures that the working directory is the root of the repository + working-directory: ./ + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name != 'push' }} + +permissions: + contents: read + actions: read + +jobs: + chromatic: + # We only need to run Storybook Builds and Storybook Visual Regression Tests within Pull Requests that actually + # introduce changes to the Storybook. Hence, we skip running these on Crowdin PRs and Dependabot PRs + if: | + github.event_name != 'pull_request' || + ( + github.actor != 'dependabot[bot]' && + github.event.pull_request.head.ref != 'chore/crowdin' + ) + + name: Chromatic + runs-on: ubuntu-latest + + environment: + name: Storybook + url: ${{ steps.chromatic-deploy.outputs.storybookUrl }} + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + fetch-depth: 0 + + - name: Start Visual Regression Tests (Chromatic) + # This assigns the Environment Deployment for Storybook + id: chromatic-deploy + uses: chromaui/action@14cfaef73576e69f95f47f60058063f46ca38719 # v18.1.0 + with: + workingDir: packages/ui-components + buildScriptName: storybook:build + projectToken: ${{ vars.CHROMATIC_PROJECT_TOKEN }} + exitOnceUploaded: true + onlyChanged: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000000..d62bbce624f35 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,23 @@ +name: 'CodeQL' + +on: + push: + branches: ['main'] + pull_request: + # The branches below must be a subset of the branches above + branches: ['main'] + types: [opened, synchronize, reopened, ready_for_review] + schedule: + - cron: '0 0 * * 1' + +permissions: + contents: read + +jobs: + analyze: + name: Analyze + uses: nodejs/web-team/.github/workflows/codeql.yml@9f3c83af227d721768d9dbb63009a47ed4f4282f + permissions: + actions: read + contents: read + security-events: write diff --git a/.github/workflows/create-release-post.yml b/.github/workflows/create-release-post.yml new file mode 100644 index 0000000000000..a008351424834 --- /dev/null +++ b/.github/workflows/create-release-post.yml @@ -0,0 +1,67 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Create Release Blog Post + +on: + workflow_dispatch: + inputs: + version: + description: The version to generate a blog post for. + type: string + required: true + +defaults: + run: + # This ensures that the working directory is the root of the repository + working-directory: ./ + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ inputs.version }} + cancel-in-progress: false + +jobs: + create-post: + name: Create Release Blog Post + runs-on: ubuntu-latest + permissions: + # Required to push the release branch + contents: write + # Required to create the pull request + pull-requests: write + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + + - run: node --run scripts:release-post -- "$VERSION" + working-directory: apps/site + id: release-post + env: + VERSION: ${{ inputs.version }} + + - name: Open pull request + uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 + # Creates a PR or update the Action's existing PR, or + # no-op if the base branch is already up-to-date. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + update-pull-request-title-and-body: true + branch: release-${{ inputs.version }} + body: | + Creates a new blog post for ${{ inputs.version }} + + Check this workflow's logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. + commit-message: 'feat(blog): create post for ${{ inputs.version }}' + labels: fast-track + title: 'feat(blog): create post for ${{ inputs.version }}' + assignees: ${{ steps.release-post.outputs.author }} + draft: true diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000000..3b0d4f38d0255 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,18 @@ +name: Review Dependencies + +on: + pull_request: + branches: + - main + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-review: + uses: nodejs/web-team/.github/workflows/dependency-review.yml@9f3c83af227d721768d9dbb63009a47ed4f4282f diff --git a/.github/workflows/leave-comment.yml b/.github/workflows/leave-comment.yml new file mode 100644 index 0000000000000..d79ec042481d5 --- /dev/null +++ b/.github/workflows/leave-comment.yml @@ -0,0 +1,97 @@ +name: Leave Comment + +on: + workflow_run: # zizmor: ignore[dangerous-triggers] + # While it's true (as reported by Zizmor) that many `workflow_run` + # workflows are unsafe, we are using it purely to paste a code comment + # on a pull request. If an attacker were to attempt to modify the _only_ + # file of data we collect from their unsafe code, the worst they could do + # is leave a comment on their own PR, which has no harmful impact + # whatsover. + # + # Any Workflow that uploads a `pr-comment` artifact should be listed here + workflows: ['Build', 'Lighthouse'] + types: [completed] + +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.workflow_run.id }} + cancel-in-progress: true + +jobs: + leave-comment: + name: Leave Comment + runs-on: ubuntu-latest + permissions: + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Download Comment Artifact + # The Workflow may not have produced a comment (e.g. the comparison was skipped), so this is + # allowed to fail and every subsequent step is gated on it having succeeded. + id: download + continue-on-error: true + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: pr-comment + path: pr-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Resolve Pull Request Number + id: pr + if: steps.download.outcome == 'success' + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + with: + script: | + const run = context.payload.workflow_run; + + // 1. For same-repo Pull Requests the run is already linked to its PR(s). + if (run.pull_requests && run.pull_requests.length) { + core.setOutput('number', run.pull_requests[0].number); + return; + } + + // 2. For forks that list is empty, so find the open Pull Request who has the + // correct branch information + const match = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + head: `${context.payload.workflow_run.head_repository.owner.login}:${context.payload.workflow_run.head_branch}`, + sort: 'updated', + direction: 'desc', + per_page: 1, + }).then(r => r.data[0]); + + if (!match) { + core.info(`No open pull request found for HEAD ${run.head_sha}`); + return; + } + + core.setOutput('number', match.number); + + - name: Read Comment Tag + id: meta + if: steps.download.outcome == 'success' + run: | + tag="$(tr -cd 'A-Za-z0-9_-' < pr-comment/tag.txt)" + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Add Comment to PR + # The comment body is untrusted markdown, so it is passed as a file (data) rather than + # interpolated into an expression or shell command. + if: steps.download.outcome == 'success' && steps.pr.outputs.number != '' + uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + with: + file-path: pr-comment/comment.md + comment-tag: ${{ steps.meta.outputs.tag }} + pr-number: ${{ steps.pr.outputs.number }} diff --git a/.github/workflows/lighthouse.yml b/.github/workflows/lighthouse.yml new file mode 100644 index 0000000000000..b49f72e712bd9 --- /dev/null +++ b/.github/workflows/lighthouse.yml @@ -0,0 +1,109 @@ +# Security Notes +# This Workflow runs in the untrusted `pull_request` context and therefore must not rely on any +# repository secrets. It does not comment on the Pull Request itself; instead it uploads a +# `pr-comment` artifact which the trusted `Leave Comment` Workflow posts once this Workflow completes. +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. +# MERGE QUEUE NOTE: This Workflow does not run on `merge_group` trigger, as this Workflow is not required for Merge Queue's + +name: Lighthouse + +on: + pull_request: + branches: + - main + types: + - labeled + +defaults: + run: + # This ensures that the working directory is the root of the repository + working-directory: ./ + +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + lighthouse-ci: + # We want to skip our lighthouse analysis on Dependabot PRs + if: | + startsWith(github.event.pull_request.head.ref, 'dependabot/') == false && + github.event.label.name == 'github_actions:pull-request' + name: Lighthouse Report + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Capture Vercel Preview + id: deployment + uses: patrickedqvist/wait-for-vercel-preview@d7982701e6fcd3ae073bff929e408e004404d38d # v1.3.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + max_timeout: 300 # timeout after 5 minutes + check_interval: 10 # check every 10 seconds + + - name: Git Checkout + # Only needed for the Lighthouse formatting script; no credentials are persisted. + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Audit Preview URL with Lighthouse + # Conduct the lighthouse audit + id: lighthouse_audit + uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # v12.6.2 + with: + # Defines the settings and assertions to audit + configPath: './.lighthouserc.json' + # These URLS capture critical pages / site functionality. + urls: | + ${{ steps.deployment.outputs.url }}/en + ${{ steps.deployment.outputs.url }}/en/about + ${{ steps.deployment.outputs.url }}/en/about/previous-releases + ${{ steps.deployment.outputs.url }}/en/download + ${{ steps.deployment.outputs.url }}/en/download/archive/current + ${{ steps.deployment.outputs.url }}/en/blog + uploadArtifacts: true # save results as a action artifacts + temporaryPublicStorage: true # upload lighthouse report to the temporary storage + + - name: Format Lighthouse Score + # Transform the audit results into a single, friendlier output + id: format_lighthouse_score + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + env: + # using env as input to our script + # see https://github.com/actions/github-script#use-env-as-input + LIGHTHOUSE_RESULT: ${{ steps.lighthouse_audit.outputs.manifest }} + LIGHTHOUSE_LINKS: ${{ steps.lighthouse_audit.outputs.links }} + VERCEL_PREVIEW_URL: ${{ steps.deployment.outputs.url }} + with: + # Run as a separate file so we do not have to inline all of our formatting logic. + # See https://github.com/actions/github-script#run-a-separate-file for more info. + script: | + const { formatLighthouseResults } = await import('${{github.workspace}}/apps/site/scripts/lighthouse/index.mjs') + await formatLighthouseResults({core}) + + - name: Prepare Comment + env: + COMMENT: ${{ steps.format_lighthouse_score.outputs.comment }} + run: | + mkdir -p pr-comment + printf '%s' "$COMMENT" > pr-comment/comment.md + printf '%s' 'lighthouse_audit' > pr-comment/tag.txt + + - name: Upload Comment + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: pr-comment + path: pr-comment/ diff --git a/.github/workflows/lint-and-tests.yml b/.github/workflows/lint-and-tests.yml new file mode 100644 index 0000000000000..c61cfafcdb3fb --- /dev/null +++ b/.github/workflows/lint-and-tests.yml @@ -0,0 +1,124 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Linting and Tests + +# This workflow should run either on `merge_group`, `pull_request`, or `push` events +# since we want to run lint checks against any changes on pull requests, or the final patch on merge groups +# or if direct pushes happen to main (or when changes in general land on the `main` (default) branch) +# Note that the reason why we run this on pushes against `main` is that on rare cases, maintainers might do direct pushes against `main` + +on: + push: + branches: + - main + pull_request: + branches: + - main + types: [opened, synchronize, reopened, ready_for_review] + merge_group: + +# The permissions specified below apply to workflows triggered by `merge_group`, `push`, and `pull_request` events that originate from the same repository (non-fork). +# However, workflows triggered by `pull_request` events from forked repositories are treated differently for security reasons: +# - These workflows **do not** have access to any secrets configured in the repository. +# - They are also **not granted any permissions** to perform actions on the base repository. +# +# This is a deliberate security restriction designed to prevent potential abuse through malicious pull requests from forks. +# For a deeper explanation and best practices for securing your GitHub Actions workflows, particularly against so-called "pwn requests", +# refer to https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/ +permissions: + contents: read + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + lint: + name: Quality checks + runs-on: ubuntu-latest + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + + - name: Restore Lint Cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + .turbo/cache + .eslintmdcache + .stylelintcache + .prettiercache + # We want to restore Turborepo Cache and ESlint and Prettier Cache + # The ESLint and Prettier cache's are useful to reduce the overall runtime of ESLint and Prettier + # as they will only run on files that have changed since the last cached run + # this might of course lead to certain files not being checked against the linter, but the chances + # of such situation from happening are very slim as the checksums of both files would need to match + key: cache-lint-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('.turbo/cache/**') }} + restore-keys: | + cache-lint-${{ hashFiles('pnpm-lock.yaml') }}- + cache-lint- + + - name: Run quality checks with `turbo` + # We run the ESLint and Prettier commands on all Workflow triggers of the `Lint` job, besides if + # the Pull Request comes from a Crowdin Branch, as we don't want to run ESLint and Prettier on Crowdin PRs + # Note: Linting and Prettifying of files on Crowdin PRs is handled by the `translations-pr.yml` Workflow + if: | + (github.event_name == 'push' || github.event_name == 'merge_group') || + (github.event_name == 'pull_request' && github.event.pull_request.head.ref != 'chore/crowdin') + run: node_modules/.bin/turbo lint lint:types prettier --cache-dir=.turbo/cache + + - name: Save Lint Cache + # We only want to save caches on `push` events or `pull_request_target` events + # and if it is a `pull_request_target` event, we want to avoid saving the cache if the PR comes from Dependabot + # or if it comes from an automated Crowdin Pull Request + # The reason we save caches on `push` is because caches creates on `main` (default) branches can be reused within + # other Pull Requests and PRs coming from forks + if: | + github.event_name == 'push' || + (github.event_name == 'pull_request' && + startsWith(github.event.pull_request.head.ref, 'dependabot/') == false && + github.event.pull_request.head.ref != 'chore/crowdin') + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + .turbo/cache + .eslintmdcache + .stylelintcache + .prettiercache + key: cache-lint-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('.turbo/cache/**') }} + + tests: + name: Tests + runs-on: ubuntu-latest + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + + - name: Run Unit Tests + # We want to run Unit Tests in every circumstance, including Crowdin PRs and Dependabot PRs to ensure + # that changes to dependencies or translations don't break the Unit Tests + run: node --run test:ci -- --cache-dir=.turbo/cache + + - name: Upload test coverage to Codecov + if: ${{ !cancelled() && github.event_name != 'merge_group' }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + files: ./apps/site/lcov.info,./packages/*/lcov.info + + - name: Upload test results to Codecov + if: ${{ !cancelled() && github.event_name != 'merge_group' }} + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + report_type: test_results + files: ./apps/site/junit.xml,./packages/*/junit.xml diff --git a/.github/workflows/notify-on-push.yml b/.github/workflows/notify-on-push.yml new file mode 100644 index 0000000000000..9ac17633e30bd --- /dev/null +++ b/.github/workflows/notify-on-push.yml @@ -0,0 +1,24 @@ +on: + push: + branches: + - main + +name: Notify on Push +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: false + +jobs: + notify_on_push: + name: Notify on any direct push to `main` + if: > + github.repository == 'nodejs/nodejs.org' && + github.actor != 'github-merge-queue[bot]' + runs-on: ubuntu-latest + steps: + - uses: nodejs/web-team/actions/notify-on-push@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + webhook: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/playwright-cloudflare-open-next.yml b/.github/workflows/playwright-cloudflare-open-next.yml new file mode 100644 index 0000000000000..b049a3d08ddde --- /dev/null +++ b/.github/workflows/playwright-cloudflare-open-next.yml @@ -0,0 +1,64 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Playwright Tests on Cloudflare Open-Next + +on: + push: + branches: + - main + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + actions: read + +jobs: + playwright: + name: Playwright Tests + runs-on: ubuntu-latest + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + fetch-depth: 2 + + - name: Get Playwright version + id: playwright-version + working-directory: apps/site + run: echo "version=$(node_modules/.bin/playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright Browsers + working-directory: apps/site + run: node_modules/.bin/playwright install --with-deps + + - name: Build open-next site + run: node_modules/.bin/turbo build --filter=@node-core/platform-cloudflare --cache-dir=.turbo/cache + + - name: Run Playwright tests + run: pnpm --filter=@node-core/platform-cloudflare test:e2e + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-report + path: apps/site/playwright-report/ diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 0000000000000..f09764aafd418 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,72 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. +# MERGE QUEUE NOTE: This Workflow does not run on `merge_group` trigger, as this Workflow is not required for Merge Queue's + +name: Playwright Tests + +on: + pull_request: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + actions: read + +jobs: + playwright: + # We want to skip on Dependabot PRs + if: startsWith(github.event.pull_request.head.ref, 'dependabot/') == false + name: Playwright Tests + runs-on: ubuntu-latest + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + fetch-depth: 2 + + - name: Capture Vercel Preview + id: deployment + uses: patrickedqvist/wait-for-vercel-preview@d7982701e6fcd3ae073bff929e408e004404d38d # v1.3.3 + with: + token: ${{ secrets.GITHUB_TOKEN }} + max_timeout: 300 # timeout after 5 minutes + check_interval: 10 # check every 10 seconds + + - name: Get Playwright version + id: playwright-version + working-directory: apps/site + run: echo "version=$(node_modules/.bin/playwright --version | awk '{print $2}')" >> $GITHUB_OUTPUT + + - name: Cache Playwright browsers + id: playwright-cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ steps.playwright-version.outputs.version }} + + - name: Install Playwright Browsers + working-directory: apps/site + run: node_modules/.bin/playwright install --with-deps + + - name: Run Playwright tests + working-directory: apps/site + run: node --run playwright + env: + PLAYWRIGHT_BASE_URL: ${{ steps.deployment.outputs.url }} + + - name: Upload Playwright test results + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: playwright-report + path: apps/site/playwright-report/ diff --git a/.github/workflows/pnpm-updater.yml b/.github/workflows/pnpm-updater.yml new file mode 100644 index 0000000000000..6419dcd780c71 --- /dev/null +++ b/.github/workflows/pnpm-updater.yml @@ -0,0 +1,70 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: PNPM updater + +on: + workflow_dispatch: + schedule: + - cron: '0 0 1 * *' + +permissions: {} + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + update-pnpm: + name: Update pnpm + runs-on: ubuntu-latest + permissions: + # Required to push changes to the update branch + contents: write + # Required to create the pull request + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Git Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Update pnpm and package.json + id: update-pnpm + run: | + echo "old=$(pnpm --version)" >> $GITHUB_OUTPUT + pnpm self-update + echo "new=$(pnpm --version)" >> $GITHUB_OUTPUT + cat <<< $(jq '.devEngines.packageManager.version = (.packageManager | split("@")[1])' package.json) > package.json + + - name: Open pull request + if: steps.update-pnpm.outputs.old != steps.update-pnpm.outputs.new + uses: gr2m/create-or-update-pull-request-action@b65137ca591da0b9f43bad7b24df13050ea45d1b # v1.10.1 + # Creates a PR or update the Action's existing PR, or + # no-op if the base branch is already up-to-date. + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + update-pull-request-title-and-body: true + branch: chore/update-pnpm + body: | + Updates pnpm from ${{ steps.update-pnpm.outputs.old }} to ${{ steps.update-pnpm.outputs.new }} + + cc @nodejs/web-infra + + Check this workflow's logs at ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}. + commit-message: 'meta: update pnpm from ${{ steps.update-pnpm.outputs.old }} to ${{ steps.update-pnpm.outputs.new }}' + title: 'meta: update pnpm from ${{ steps.update-pnpm.outputs.old }} to ${{ steps.update-pnpm.outputs.new }}' + draft: true diff --git a/.github/workflows/publish-packages.yml b/.github/workflows/publish-packages.yml new file mode 100644 index 0000000000000..c12447643fd12 --- /dev/null +++ b/.github/workflows/publish-packages.yml @@ -0,0 +1,104 @@ +name: Publish Packages + +# Changesets opens or updates a version PR when release notes are present. Once that PR is merged, +# this workflow publishes the versioned packages to npm. + +on: + push: + # For security reasons, this should never be set to anything but `main` + branches: [main] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + COMMIT_SHA: ${{ github.sha }} + +jobs: + release: + name: Create Release PR or Publish + runs-on: ubuntu-latest + # Never attempt to publish from forks (no trusted-publisher match / secrets). + if: github.repository == 'nodejs/nodejs.org' + permissions: + contents: write + id-token: write + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Verify commit authenticity + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get commit data from GitHub API to verify its authenticity + COMMIT_DATA=$(gh api repos/${{ github.repository }}/commits/$COMMIT_SHA) + # Check if commit signature is verified (GPG signed) + VERIFIED=$(echo "$COMMIT_DATA" | jq -r '.commit.verification.verified') + # Check if commit was made through GitHub's web interface (merge queue) + COMMITTER=$(echo "$COMMIT_DATA" | jq -r '.commit.committer.email') + + # Security checks to ensure we only publish from verified and trusted sources + if [[ "$VERIFIED" != "true" ]]; then + echo "❌ Unverified commit! Aborting." + exit 1 + fi + + if [[ "$COMMITTER" != "noreply@github.com" ]]; then + echo "❌ Not merged with the merge queue! Aborting." + exit 1 + fi + + echo "✅ Commit is verified and trusted." + + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + registry-url: 'https://registry.npmjs.org' + fetch-depth: 0 + + - name: Create release pull request or publish packages + id: changesets + uses: changesets/action@a45c4d594aa4e2c509dc14a9f2b3b67ba3780d0d # v1.9.0 + with: + commit: 'chore: version packages' + title: 'chore: version packages' + version: node --run changeset:version + publish: node --run release + commitMode: github-api + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Format published packages + if: steps.changesets.outputs.published == 'true' + id: notification + env: + PUBLISHED_PACKAGES: ${{ steps.changesets.outputs.publishedPackages }} + run: | + { + echo 'packages<)"' <<< "$PUBLISHED_PACKAGES" + echo 'EOF' + } >> "$GITHUB_OUTPUT" + + - name: Notify + if: steps.changesets.outputs.published == 'true' + uses: rtCamp/action-slack-notify@33ca3be66c6f378fe1610fd1d5258632dbed5e58 # v2.4.0 + env: + SLACK_COLOR: '#43853D' + SLACK_ICON: https://github.com/nodejs.png?size=48 + SLACK_TITLE: ':rocket: Packages Published' + SLACK_MESSAGE: | + ${{ steps.notification.outputs.packages }} + :bust_in_silhouette: *Published by*: ${{ github.triggering_actor }} + :octocat: *Commit*: + SLACK_USERNAME: nodejs-bot + SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }} diff --git a/.github/workflows/pull-request-label.yml b/.github/workflows/pull-request-label.yml new file mode 100644 index 0000000000000..4642bc449cc78 --- /dev/null +++ b/.github/workflows/pull-request-label.yml @@ -0,0 +1,46 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Pull Request CI Label + +on: + pull_request_target: + branches: + - main + types: + - labeled + +defaults: + run: + # This ensures that the working directory is the root of the repository + working-directory: ./ + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + # This Job removes the `github_actions:pull-request` label after it got applied + # which allows people with write access to the repository to easily reapply the label if they need to trigger + # this Workflow again + remove_pull_request_label: + name: Remove Pull Request Label + runs-on: ubuntu-latest + permissions: + # Required by `actions-ecosystem/action-remove-label` to remove labels + pull-requests: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Remove GitHub Actions Label + uses: actions-ecosystem/action-remove-labels@2ce5d41b4b6aa8503e285553f75ed56e0a40bae0 # v1.3.0 + with: + labels: github_actions:pull-request diff --git a/.github/workflows/pull-request-policy.yml b/.github/workflows/pull-request-policy.yml new file mode 100644 index 0000000000000..2277df7ecdb70 --- /dev/null +++ b/.github/workflows/pull-request-policy.yml @@ -0,0 +1,54 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +name: Pull Request Policy + +on: + pull_request: + branches: + - main + paths: + - packages/** + types: [opened, edited, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + changesets: + name: Changesets + runs-on: ubuntu-latest + if: | + github.event_name == 'pull_request' && + !(github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'changeset-release/main') + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + fetch-depth: 0 + + - run: pnpm changeset status --since origin/main + + conventional-commits: + name: Conventional commits + runs-on: ubuntu-latest + steps: + - name: Check PR Title Format + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + REGEX="^[a-z][a-z0-9-]*(\([a-z0-9_.\/-]+\))?!?: .+" + + if [[ ! "$PR_TITLE" =~ $REGEX || "$PR_TITLE" == *. ]]; then + echo 'PR titles must use [optional scope][!]: and must not end with a period.' + exit 1 + fi diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000000000..026761dc3e4fd --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,26 @@ +name: OpenSSF Scorecard Review +on: + # For Branch-Protection check. Only the default branch is supported. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection + branch_protection_rule: + # To guarantee that the Maintained check is occasionally updated. See + # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained + schedule: + - cron: '20 7 * * 2' + push: + branches: + - main + +permissions: {} + +jobs: + analysis: + name: Scorecard analysis + uses: nodejs/web-team/.github/workflows/scorecard.yml@b62c434f5e530041c288a40280567849449e74be + permissions: + # Needed to upload the results to code-scanning dashboard. + security-events: write + # Needed to publish results and get a badge (see publish_results below). + id-token: write + contents: read + actions: read diff --git a/.github/workflows/tmp-cloudflare-open-next-deploy.yml b/.github/workflows/tmp-cloudflare-open-next-deploy.yml new file mode 100644 index 0000000000000..da8cba163dda9 --- /dev/null +++ b/.github/workflows/tmp-cloudflare-open-next-deploy.yml @@ -0,0 +1,63 @@ +# Security Notes +# Only selected Actions are allowed within this repository. Please refer to (https://github.com/nodejs/nodejs.org/settings/actions) +# for the full list of available actions. If you want to add a new one, please reach out a maintainer with Admin permissions. +# REVIEWERS, please always double-check security practices before merging a PR that contains Workflow changes!! +# AUTHORS, please only use actions with explicit SHA references, and avoid using `@master` or `@main` references or `@version` tags. + +# Note: this is a temporary workflow for deploying the OpenNext version of the site (hosted on https://node-test.org) +# this version of the site is just used for testing purposes and ideally we want to keep it in sync with the +#  official website + +name: Deploy the Cloudflare OpenNext test version of the site (https://node-test.org) + +on: + workflow_dispatch: + push: + branches: + - main + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + deploy: + name: Deploy Open-Next site + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Git Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Set up pnpm + uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 + + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + # We want to ensure that the Node.js version running here respects our supported versions + node-version-file: '.nvmrc' + cache: 'pnpm' + + - name: Install packages + run: pnpm install --frozen-lockfile + + - name: Build open-next site + run: node_modules/.bin/turbo build --filter=@node-core/platform-cloudflare --cache-dir=.turbo/cache + + - name: Deploy open-next site + run: pnpm --filter=@node-core/platform-cloudflare run deploy + env: + CF_WORKERS_SCRIPTS_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: 07be8d2fbc940503ca1be344714cb0d1 diff --git a/.github/workflows/translations-pr-lint.yml b/.github/workflows/translations-pr-lint.yml new file mode 100644 index 0000000000000..d6529c6f06e19 --- /dev/null +++ b/.github/workflows/translations-pr-lint.yml @@ -0,0 +1,58 @@ +# This Workflow is used to comment on PRs that have changes that touch Translated Files +# and then comments on their PRs mentioning that they should not do so + +name: Incoming Translation Checks + +on: + # run when someone tries to manually change localized content + pull_request_target: + branches: + - main + paths: + - 'apps/site/pages/**/*.md' + - 'apps/site/pages/**/*.mdx' + - '!apps/site/pages/en/**/*.md' + - '!apps/site/pages/en/**/*.mdx' + - 'packages/i18n/src/locales/*.json' + - '!packages/i18n/src/locales/en.json' + - 'apps/site/snippets/**/*.bash' + - '!apps/site/snippets/en/**/*.bash' + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: {} + +jobs: + comment_on_translation_pr: + # This comment should always be posted on forks, or from internal PRs not originating from Crowdin (which are direct branches) + if: | + (github.event.pull_request.head.repo.full_name != 'nodejs/nodejs.org') || + (github.event.pull_request.head.repo.full_name == 'nodejs/nodejs.org' && github.event.pull_request.head.ref != 'chore/crowdin') + + name: Comment on Translation PR + runs-on: ubuntu-latest + + permissions: + # This permission is required by `thollander/actions-comment-pull-request` + pull-requests: write + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - uses: thollander/actions-comment-pull-request@e2c37e53a7d2227b61585343765f73a9ca57eda9 # v3.0.0 + with: + message: | + > [!NOTE]\ + > Your Pull Request seems to be updating **Translations** of the Node.js Website. + > + > Whilst we appreciate your intent; Any Translation update should be done through our [Crowdin Project](https://crowdin.com/project/nodejs-web). + > We recommend giving a read on our [Translation Guidelines](https://github.com/nodejs/nodejs.org/blob/main/docs/translation.md). + > + > Thank you! + comment-tag: use_crowdin diff --git a/.github/workflows/translations-sync.yml b/.github/workflows/translations-sync.yml new file mode 100644 index 0000000000000..241455aafe1e6 --- /dev/null +++ b/.github/workflows/translations-sync.yml @@ -0,0 +1,128 @@ +# This action automates the synchronization of our crowdin translations, so that a human does not need to kick it off from the crowdin UI +# It also formats incoming content because it is often not adherent to our rules post-translation. + +# See translations-upload.yml for automation to upload our source content +# See translations-pr-lint.yml for quality control we conduct on ingress of new translations. +name: Crowdin Download + +on: + workflow_dispatch: # Allow running when we want to, for events such as urgent translation mistakes or 100% completed languages + schedule: + - cron: '0 5 * * 5' # At 05:00 on Fridays. This guarantees that we have the 72 hour weekend time to review translations. + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + BRANCH_NAME: chore/crowdin + +jobs: + synchronize-with-crowdin: + name: Synchronize with Crowdin + runs-on: ubuntu-latest + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + token: ${{ secrets.CROWDIN_GITHUB_BOT_TOKEN }} + persist-credentials: false + + # see all the options at https://github.com/crowdin/github-action + - name: Crowdin PR + uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + with: + # do not upload anything - this is a one-way operation download + upload_sources: false + upload_translations: false + # the rest of this controls how the PR comes in with new translations + download_translations: true + localization_branch_name: ${{ env.BRANCH_NAME }} + create_pull_request: true + pull_request_title: 'chore(i18n): sync translations from crowdin' + pull_request_body: 'New Crowdin translations from the [Node.js Crowdin project](https://crowdin.com/project/nodejs-web)' + commit_message: 'chore(i18n): sync translations from crowdin' + env: + GITHUB_TOKEN: ${{ secrets.CROWDIN_GITHUB_BOT_TOKEN }} + # A numeric ID, found at https://crowdin.com/project/nodejs-web/tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + # Created from https://crowdin.com/settings#api-key logged in using nodejs-crowdin-bot + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} + + format_crowdin_pull_request: + name: Format Crowdin Pull Request + needs: synchronize-with-crowdin + runs-on: ubuntu-latest + + permissions: + # This permission is required by `stefanzweifel/git-auto-commit-action` + contents: write + + steps: + - uses: nodejs/web-team/actions/setup-environment@9f3c83af227d721768d9dbb63009a47ed4f4282f + with: + pnpm: true + use-version-file: true + ref: ${{ env.BRANCH_NAME }} + token: ${{ secrets.CROWDIN_GITHUB_BOT_TOKEN }} + fetch-depth: 2 + + - name: Restore Lint Cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + apps/site/.eslintmdcache + apps/site/.prettiercache + # We want to restore Turborepo Cache and ESlint and Prettier Cache + # The ESLint and Prettier cache's are useful to reduce the overall runtime of ESLint and Prettier + # as they will only run on files that have changed since the last cached run + # this might of course lead to certain files not being checked against the linter, but the chances + # of such situation from happening are very slim as the checksums of both files would need to match + key: cache-lint-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('apps/site/.eslintmdcache') }} + restore-keys: | + cache-lint-${{ hashFiles('pnpm-lock.yaml') }}- + cache-lint- + + - name: Add changeset if the files changed + run: | + CHANGED_FILES=$(git diff --name-only HEAD^1 HEAD) + if [ -n "$CHANGED_FILES" ]; then + cat > .changeset/crowdin-translations.md <<'EOF' + --- + '@node-core/website-i18n': patch + --- + + Update translations from Crowdin. + EOF + fi + + - name: Run ESLint + working-directory: apps/site + run: node --run lint:md -- --fix + + - name: Run Prettier + run: node --run prettier:fix + + - name: Push Changes back to Pull Request + uses: stefanzweifel/git-auto-commit-action@4a55954c782fc1ea30b9056cd3e7a2b40ca8887d # v7.2.0 + with: + commit_options: '--no-verify --signoff' + commit_message: 'chore(i18n): format translated files' + branch: ${{ env.BRANCH_NAME }} + + - name: Save Lint Cache + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: | + apps/site/.eslintmdcache + apps/site/.prettiercache + key: cache-lint-${{ hashFiles('pnpm-lock.yaml') }}-${{ hashFiles('apps/site/.eslintmdcache') }} diff --git a/.github/workflows/translations-upload.yml b/.github/workflows/translations-upload.yml new file mode 100644 index 0000000000000..c561c7df98750 --- /dev/null +++ b/.github/workflows/translations-upload.yml @@ -0,0 +1,47 @@ +# This action automates the upload of our source content to crowdin. +# See translations-sync.yml for the automation to download new translations on a schedule +# See translations-pr-lint.yml for quality control we conduct on ingress of new translations. +name: Crowdin Upload + +on: + push: + branches: [main] + +# Cancel any runs on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + upload-to-crowdin: + name: Upload to Crowdin + runs-on: ubuntu-latest + + steps: + - name: Harden Runner + uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # see all the options at https://github.com/crowdin/github-action + - name: crowdin action + uses: crowdin/github-action@c7af9bc98b01694653031fef2a0dc6c7888ce9bc # v2.17.0 + with: + # only upload sources, ensuring this is a one-way operation + upload_sources: true + upload_translations: false + download_translations: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # A numeric ID, found at https://crowdin.com/project/nodejs-web/tools/api + CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }} + # Created from https://crowdin.com/settings#api-key logged in using nodejs-crowdin-bot + CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }} diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000000000..f2e1f2f2c7fb8 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,32 @@ +name: Zizmor + +on: + push: + branches: + - main + pull_request: + branches: + - main + +permissions: {} + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + zizmor: + name: Zizmor Security Analysis + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + security-events: write + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor 🌈 + uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1 diff --git a/.gitignore b/.gitignore index 646ad01542bfc..a756afeb5c8ef 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,58 @@ -# Generated HTML and other static files -build/ # Commonly ignored Node.js files -node_modules/ +node_modules +.env.* + +# npm Compatibility +# https://github.com/nodejs/nodejs.org/discussions/5334#discussioncomment-12827850 npm-debug.log -.npm/ +.npm +package-lock.json + +# Next.js Build Output +apps/site/.next +apps/site/build +apps/site/public/blog-data.json +apps/site/next-env.d.ts + +# Generated Build Artifacts +platforms/cloudflare/generated + +# Test Runner +junit.xml +lcov.info + +# Distributed Files +dist + +# Storybook +storybook-static +build-storybook.log +.nyc_output -# OSX system files, the bane of our existence +# Vercel Files +.vercel +.turbo +cache + +# Cache Files +.eslintmdcache +.stylelintcache +.prettiercache + +# TypeScript +tsconfig.tsbuildinfo +dist/ + +# Cloudflare Build Output +apps/site/.open-next +apps/site/.wrangler + +## Playwright +test-results +playwright-report + +## MacOS Ignored Files .DS_Store -.AppleDouble -.LSOverride + +## Other Files +.env diff --git a/.htmllintrc b/.htmllintrc deleted file mode 100644 index 0e1957f889f24..0000000000000 --- a/.htmllintrc +++ /dev/null @@ -1,14 +0,0 @@ -{ - "attr-bans": [], - "attr-name-style": false, - "attr-no-dup": false, - "attr-quote-style": false, - "attr-req-value": false, - "id-class-style": "dash", - "img-req-alt": false, - "indent-style": "spaces", - "indent-width": 2, - "lang-style": false, - "spec-char-escape": false, - "tag-bans": [] -} diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000000000..5f31f933a76bf --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,7 @@ +# lint and format staged files + +node --run lint:staged + +# verify typescript fully + +node --run lint:types diff --git a/.lighthouserc.json b/.lighthouserc.json new file mode 100644 index 0000000000000..be61cf8385d95 --- /dev/null +++ b/.lighthouserc.json @@ -0,0 +1,19 @@ +{ + "ci": { + "collect": { + "numberOfRuns": 1, + "settings": { + "preset": "desktop", + "skipAudits": ["is-crawlable"] + } + }, + "assert": { + "assertions": { + "categories:performance": ["warn", { "minScore": 0.9 }], + "categories:accessibility": ["warn", { "minScore": 0.9 }], + "categories:best-practices": ["warn", { "minScore": 0.9 }], + "categories:seo": ["warn", { "minScore": 0.9 }] + } + } + } +} diff --git a/.lintstagedrc.json b/.lintstagedrc.json new file mode 100644 index 0000000000000..91a0e156e4ab9 --- /dev/null +++ b/.lintstagedrc.json @@ -0,0 +1,3 @@ +{ + "**/*.{js,mjs,ts,tsx,md,mdx,json.yml}": ["prettier --check --write"] +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000000..54c65116f15a6 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v24 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000000..b0a2a81b9e2e2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,48 @@ +# Commonly ignored Node.js files +node_modules + +# npm Compatibility +# https://github.com/nodejs/nodejs.org/discussions/5334#discussioncomment-12827850 +npm-debug.log +.npm +package-lock.json + +# Next.js Build Output +.next +build +next-env.d.ts + +# Test Runner +junit.xml +lcov.info + +# Storybook +storybook-static +build-storybook.log +.nyc_output + +# Vercel Files +.vercel +.turbo +cache + +# Cache Files +.eslintmdcache +.stylelintcache +.prettiercache + +# TypeScript +tsconfig.tsbuildinfo + +# Metadata Files +CODEOWNERS + +# Public Folders +apps/site/public + +# Distributed Files +dist + +# Prettier's Handlebar parser is limited and chokes on some syntax features +# https://github.com/prettier/prettier/issues/11834 +scripts/release-post/template.hbs diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000000000..c69d55a94d950 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,13 @@ +{ + "tabWidth": 2, + "useTabs": false, + "semi": true, + "singleQuote": true, + "jsxSingleQuote": false, + "trailingComma": "es5", + "bracketSpacing": true, + "bracketSameLine": false, + "arrowParens": "avoid", + "plugins": ["prettier-plugin-tailwindcss"], + "tailwindPreserveWhitespace": true +} diff --git a/.snyk b/.snyk deleted file mode 100644 index c12cd532b4cc4..0000000000000 --- a/.snyk +++ /dev/null @@ -1,36 +0,0 @@ -version: v1.5.2 -ignore: {} -patch: - 'npm:marked:20150520': - - marked: - patched: '2016-07-09T09:33:23.222Z' - - metalsmith-markdown > marked: - patched: '2016-07-09T09:33:23.222Z' - 'npm:minimatch:20160620': - - metalsmith-stylus > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - metalsmith > recursive-readdir > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - metalsmith-prism > metalsmith > recursive-readdir > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - metalsmith-stylus > stylus > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar > fstream > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar-pack > fstream > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar-pack > fstream-ignore > fstream > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar-pack > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar-pack > tar > fstream > rimraf > glob > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - chokidar > fsevents > node-pre-gyp > tar-pack > fstream-ignore > minimatch: - patched: '2016-07-09T09:33:23.222Z' - - metalsmith-collections > minimatch: - patched: '2016-07-09T09:33:23.222Z' - 'npm:tough-cookie:20160722': - - chokidar > fsevents > node-pre-gyp > request > tough-cookie: - patched: '2016-07-26T18:26:02.202Z' diff --git a/.stylintrc b/.stylintrc deleted file mode 100644 index 05316154a9c46..0000000000000 --- a/.stylintrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "colons": { - "expect": "never", - "error": true - }, - "indentPref": { - "expect": 2, - "error": true - } -} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7e4ff2461e97b..0000000000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -sudo: false -language: node_js -node_js: - - 8 diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000000000..2d83c6ee3edef --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,10 @@ +{ + "recommendations": [ + "esbenp.prettier-vscode", + "bradlc.vscode-tailwindcss", + "stylelint.vscode-stylelint", + "unifiedjs.vscode-mdx", + "dbaeumer.vscode-eslint", + "editorconfig.editorconfig" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000..f45e0fc02c2f8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "css.validate": false, + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "javascript.updateImportsOnFileMove.enabled": "always", + "typescript.updateImportsOnFileMove.enabled": "always", + "typescript.tsdk": "node_modules/typescript/lib" +} diff --git a/COLLABORATOR_GUIDE.md b/COLLABORATOR_GUIDE.md deleted file mode 100644 index dbd0c89f6fa37..0000000000000 --- a/COLLABORATOR_GUIDE.md +++ /dev/null @@ -1,200 +0,0 @@ -# Node.js Collaborator Guide - -**Contents** - -* [Issues and Pull Requests](#issues-and-pull-requests) -* [Accepting Modifications](#accepting-modifications) - - [Involving the Website Admin Group](#involving-the-website-admin-group) - -This document contains information for Collaborators of the Node.js -website project regarding maintaining the code, documentation and issues. - -Collaborators should be familiar with the guidelines for new -contributors in [CONTRIBUTING.md](./CONTRIBUTING.md) and also -understand the project governance model as outlined in -[GOVERNANCE.md](./GOVERNANCE.md). - -## Issues and Pull Requests - -Courtesy should always be shown to individuals submitting issues and -pull requests to the Node.js website project. - -Collaborators should feel free to take full responsibility for -managing issues and pull requests they feel qualified to handle, as -long as this is done while being mindful of these guidelines, the -opinions of other Collaborators and guidance of the Website Admin Group. - -Collaborators may **close** any issue or pull request they believe is -not relevant for the future of the Node.js project. Where this is -unclear, the issue should be left open for several days to allow for -additional discussion. Where this does not yield input from Node.js -Collaborators or additional evidence that the issue has relevance, the -issue may be closed. Remember that issues can always be re-opened if -necessary. - -## Accepting Modifications - -All modifications to the Node.js code and documentation should be -performed via GitHub pull requests. Only the `Website Admin` group -can merge their own work and should do so with great care. - -All pull requests must be reviewed and accepted by a Collaborator with -sufficient expertise who is able to take full responsibility for the -change. In the case of pull requests proposed by an existing -Collaborator, an additional Collaborator is required for sign-off. - -In some cases, it may be necessary to summon a qualified Collaborator -to a pull request for review by @-mention. - -If you are unsure about the modification and are not prepared to take -full responsibility for the change, defer to another Collaborator. - -Before landing pull requests, sufficient time should be left for input -from other Collaborators. Leave at least 48 hours during the week and -72 hours over weekends to account for international time differences -and work schedules. Trivial changes (e.g. those which fix minor bugs -or improve performance without affecting API or causing other -wide-reaching impact) may be landed after a shorter delay. Any press -release can land with no time contstraints as long as the copy is -properly formatted, it is not the responsibility of the website group -to review the copy itself. - -Where there is no disagreement amongst Collaborators, a pull request -may be landed given appropriate review. Where there is discussion -amongst Collaborators, consensus should be sought if possible. The -lack of consensus may indicate the need to elevate discussion to the -Website Admin Group for resolution (see below). - -All bugfixes require a test case which demonstrates the defect. The -test should *fail* before the change, and *pass* after the change. - -All pull requests that modify executable code should be subjected to -continuous integration tests on the -[project CI server](https://ci.nodejs.org/). - -### Involving the Website Admin Group - -Collaborators may opt to elevate pull requests or issues to the admin group for -discussion by mentioning `@nodejs/website-admin`. This should be done -where a pull request: - -- has a significant impact on the codebase, -- is inherently controversial; or -- has failed to reach consensus amongst the Collaborators who are - actively participating in the discussion. - -The Website Admin group should serve as the final arbiter where required. - - -## Developer's Certificate of Origin 1.1 - -By making a contribution to this project, I certify that: - -* (a) The contribution was created in whole or in part by me and I - have the right to submit it under the open source license - indicated in the file; or - -* (b) The contribution is based upon previous work that, to the best - of my knowledge, is covered under an appropriate open source - license and I have the right under that license to submit that - work with modifications, whether created in whole or in part - by me, under the same open source license (unless I am - permitted to submit under a different license), as indicated - in the file; or - -* (c) The contribution was provided directly to me by some other - person who certified (a), (b) or (c) and I have not modified - it. - -* (d) I understand and agree that this project and the contribution - are public and that a record of the contribution (including all - personal information I submit with it, including my sign-off) is - maintained indefinitely and may be redistributed consistent with - this project or the open source license(s) involved. - -## Code of Conduct - -This Code of Conduct is adapted from [Rust's wonderful -CoC](https://github.com/rust-lang/rust/wiki/Note-development-policy#conduct). - -* We are committed to providing a friendly, safe and welcoming - environment for all, regardless of gender, sexual orientation, - disability, ethnicity, religion, or similar personal characteristic. -* Please avoid using overtly sexual nicknames or other nicknames that - might detract from a friendly, safe and welcoming environment for - all. -* Please be kind and courteous. There's no need to be mean or rude. -* Respect that people have differences of opinion and that every - design or implementation choice carries a trade-off and numerous - costs. There is seldom a right answer. -* Please keep unstructured critique to a minimum. If you have solid - ideas you want to experiment with, make a fork and see how it works. -* We will exclude you from interaction if you insult, demean or harass - anyone. That is not welcome behaviour. We interpret the term - "harassment" as including the definition in the [Citizen Code of - Conduct](http://citizencodeofconduct.org/); if you have any lack of - clarity about what might be included in that concept, please read - their definition. In particular, we don't tolerate behavior that - excludes people in socially marginalized groups. -* Private harassment is also unacceptable. No matter who you are, if - you feel you have been or are being harassed or made uncomfortable - by a community member, please contact one of the channel ops or any - of the TC members immediately with a capture (log, photo, email) of - the harassment if possible. Whether you're a regular contributor or - a newcomer, we care about making this community a safe place for you - and we've got your back. -* Likewise any spamming, trolling, flaming, baiting or other - attention-stealing behaviour is not welcome. -* Avoid the use of personal pronouns in code comments or - documentation. There is no need to address persons when explaining - code (e.g. "When the developer") - -## Code editing - -### Adding new pages - -1. Create new page content including the layout, title and copy. -2. Update ```/locale/en/site.json``` to provide page link attributes. -3. Update the relevant ```/layout``` to add a link to the new page. - -#### Create the page content - -Create a new markdown file in ```/local/en```. As specified in the -[README.md](./README.md#layout), initial development happens in English. - -At the top of the markdown file, set a page the title and layout. - -``` ---- -title: Events -layout: contribute.hbs ---- - -[Event copy goes here] -``` - -#### Update locale site.json to add link attributes - -Open ```local/en/site.json``` and find the appropriate page structure. -Add a new object defining the link attributes. - -``` -"event": { - "link": "get-involved/events", - "text": "Events" -} -``` - -#### Update the layout to add a link - -Using the example layout, open ```/layouts/contribute.hbs``` and add your new -link to the markup. It's essential to update the handlebars paths to site.json. - -``` -{{site.locale}}/{{site.getinvolved.events.link}} -``` - - -### Translating pages - -See [TRANSLATION.md](./TRANSLATION.md) for the website translation policy. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c902d09377b14..20fdec94a24b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,81 +1,143 @@ -# Node.js Community Contributing Guide 1.0 +# Node.js Website Contributing Guide + +## Table of Contents + +- [Quick Start](#quick-start) +- [Code of Conduct](#code-of-conduct) + - [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin-11) +- [Ways to Contribute](#ways-to-contribute) + - [For All Contributors](#for-all-contributors) + - [For Developers](#for-developers) +- [Development Workflow](#development-workflow) + - [1. Set Up Your Environment](#1-set-up-your-environment) + - [2. Make Your Changes](#2-make-your-changes) + - [3. Test Your Changes](#3-test-your-changes) + - [4. Submit Your Contribution](#4-submit-your-contribution) +- [Documentation Structure](#documentation-structure) +- [Getting Help](#getting-help) +- [Project Maintainers](#project-maintainers) +- [License](#license) + +--- + +Thank you for your interest in contributing to the Node.js Website! This guide will help you get started with contributing to our project. + +## Quick Start + +New to contributing? Start here: + +1. **[Getting Started](./docs/getting-started.md)** - Set up your development environment and make your first contribution +2. **[Code Style](./docs/code-style.md)** - Learn our coding standards and formatting guidelines +3. **[Adding Pages](./docs/adding-pages.md)** - Create new pages and content for the website ## Code of Conduct -The Code of Conduct explains the *bare minimum* behavior -expectations the Node Foundation requires of its contributors. -[Please read it before participating.](https://github.com/nodejs/node/blob/master/CODE_OF_CONDUCT.md) +Before contributing, please read and follow our [Code of Conduct](https://github.com/nodejs/node/blob/HEAD/CODE_OF_CONDUCT.md). + +### Developer's Certificate of Origin 1.1 + +``` +By contributing to this project, I certify that: + +- (a) The contribution was created in whole or in part by me and I have the right to + submit it under the open source license indicated in the file; or +- (b) The contribution is based upon previous work that, to the best of my knowledge, + is covered under an appropriate open source license and I have the right under that + license to submit that work with modifications, whether created in whole or in part + by me, under the same open source license (unless I am permitted to submit under a + different license), as indicated in the file; or +- (c) The contribution was provided directly to me by some other person who certified + (a), (b) or (c) and I have not modified it. +- (d) I understand and agree that this project and the contribution are public and that + a record of the contribution (including all personal information I submit with it, + including my sign-off) is maintained indefinitely and may be redistributed consistent + with this project or the open source license(s) involved. + +``` + +## Ways to Contribute + +### For All Contributors + +- **Report Issues**: Found a bug or have a feature request? [Open an issue](https://github.com/nodejs/nodejs.org/issues/new/choose) +- **Improve Documentation**: Help make our docs clearer and more comprehensive +- **Add Content**: Create new articles, blog posts, or improve existing content +- **Fix Bugs**: Look for issues labeled [`good first issue`](https://github.com/nodejs/nodejs.org/labels/good%20first%20issue) +- **Translate Content**: Help make Node.js documentation accessible worldwide + +### For Developers + +- **Create Components**: Build reusable React components following our [component guidelines](./docs/creating-components.md) +- **Write Tests**: Improve test coverage with our [testing guidelines](./docs/writing-tests.md) +- **Enhance Features**: Add new functionality to improve user experience + +## Development Workflow + +### 1. Set Up Your Environment + +```bash +# Fork and clone the repository +git clone https://github.com/YOUR_USERNAME/nodejs.org.git +cd nodejs.org + +# Install dependencies +pnpm install --frozen-lockfile + +# Start development server +node --run dev +``` -## Vocabulary +For detailed setup instructions, see [Getting Started](./docs/getting-started.md). -* A **Contributor** is any individual creating or commenting on an issue or pull request. -* A **Collaborator** is a subset of contributors who have been given write access to the repository. -* A **WG (Working Group)** is a group of collaborators representing the required technical -expertise to resolve rare disputes. +### 2. Make Your Changes -# Logging Issues +- **New Pages**: Follow our [page creation guide](./docs/adding-pages.md) +- **Components**: See [creating components](./docs/creating-components.md) +- **Styling**: Follow our [code style guidelines](./docs/code-style.md) -Log an issue for any question or problem you might have. When in doubt, log an issue, -any additional policies about what to include will be provided in the responses. The only -exception is security disclosures which should be sent privately. +### 3. Test Your Changes -Collaborators may direct you to another repository, ask for additional clarifications, and -add appropriate metadata before the issue is addressed. +```bash +# Format and lint code +node --run format -Please be courteous, respectful, and every participant is expected to follow the -project's Code of Conduct. +# Run tests +node --run test -# Contributions +# Test build +node --run build +``` -Any change to resources in this repository must be through pull requests. This applies to all changes -to documentation, code, binary files, etc. Even long term collaborators and WG members must use -pull requests. +### 4. Submit Your Contribution -No pull request can be merged without being reviewed. +1. **Create a branch**: `git checkout -b your-feature-branch` +2. **Commit changes**: Follow our [commit guidelines](./docs/code-style.md#commit-guidelines) +3. **Push to your fork**: `git push origin your-feature-branch` +4. **Open a Pull Request**: Use our [pull request template](.github/PULL_REQUEST_TEMPLATE.md) -For non-trivial contributions, pull requests should sit for at least 36 hours to ensure that -contributors in other timezones have time to review. Consideration should also be given to -weekends and other holiday periods to ensure active collaborators all have reasonable time to -become involved in the discussion and review process if they wish. +## Documentation Structure -The default for each contribution is that it is accepted once no collaborator has an objection. -During review collaborators may also request that a specific contributor who is most versed in a -particular area gives a "LGTM" before the PR can be merged. There is no additional "sign off" -process for contributions to land. Once all issues brought by collaborators are addressed it can -be landed by any collaborator. +Our documentation is organized in the [`docs/`](./docs/) directory, so check out it's **[README](./docs/README.md)** for navigation. -In the case of an objection being raised in a pull request by another collaborator, all involved -collaborators should seek to arrive at a consensus by way of addressing concerns being expressed -by discussion, compromise on the proposed change, or withdrawal of the proposed change. +## Getting Help -If a contribution is controversial and collaborators cannot agree about how to get it to land -or if it should land then it should be escalated to the WG. WG members should regularly -discuss pending contributions in order to find a resolution. It is expected that only a -small minority of issues be brought to the WG for resolution and that discussion and -compromise among collaborators be the default resolution mechanism. +- **Questions?** Start a [Discussion](https://github.com/nodejs/nodejs.org/discussions) +- **Found a bug?** [Open an issue](https://github.com/nodejs/nodejs.org/issues/new/choose) +- **Need clarification?** Comment on existing issues or PRs +- **Want to chat?** Join the Node.js community on [OpenJS Foundation Slack](https://openjs-foundation.slack.com/) -# Becoming a Collaborator +## Project Maintainers -All contributors who land a non-trivial contribution should be on-boarded in a timely manner, -and added as a collaborator, and be given write access to the repository. +This project is maintained by the [Node.js Website Team](https://github.com/nodejs/web-team/blob/main/MEMBERS.md#nodejs-website-team-nodejsnodejs-website). For questions about governance or high-level project direction, you can: -Collaborators are expected to follow this policy and continue to send pull requests, go through -proper review, and have other collaborators merge their pull requests. +- Mention `@nodejs/nodejs-website` in issues or PRs +- Contact team members directly for guidance +- Escalate to the [Node.js Technical Steering Committee](https://github.com/nodejs/TSC) if needed -# WG Process +## License -The WG uses a "consensus seeking" process for issues that are escalated to the WG. -The group tries to find a resolution that has no open objections among WG members. -If a consensus cannot be reached that has no objections then a majority wins vote -is called. It is also expected that the majority of decisions made by the WG are via -a consensus seeking process and that voting is only used as a last-resort. +By contributing to this project, you agree that your contributions will be licensed under the project's [MIT License](./LICENSE). -Resolution may involve returning the issue to collaborators with suggestions on how to -move forward towards a consensus. It is not expected that a meeting of the WG -will resolve all issues on its agenda during that meeting and may prefer to continue -the discussion happening among the collaborators. +--- -Members can be added to the WG at any time. Any collaborator can nominate another collaborator -to the WG and the WG uses its standard consensus seeking process to evaluate whether or -not to add this new member. Members who do not participate consistently at the level of -a majority of the other members are expected to resign. +**Ready to contribute?** Start with our [Getting Started guide](./docs/getting-started.md) and join the Node.js community in building better web experiences for developers worldwide! 🚀 diff --git a/GOVERNANCE.md b/GOVERNANCE.md deleted file mode 100644 index 17b761e265365..0000000000000 --- a/GOVERNANCE.md +++ /dev/null @@ -1,78 +0,0 @@ -# Project Governance - -## Working Group - -All of the Node.js Foundation websites, including this repo, are jointly governed -by a Working Group (WG). The WG is responsible for high-level guidance of this and -related projects which include: - -- [nodejs/iojs.org](https://github.com/nodejs/iojs.org) published to https://iojs.org -- [nodejs/nodejs.org](https://github.com/nodejs/iojs.org) published to https://nodejs.org -- [nodejs/nodejs.org-archive](https://github.com/nodejs/nodejs.org-archive) - -The Website WG has final authority over this project including: - -* Technical direction -* Project governance and process (including this policy) -* Contribution policy -* GitHub repository hosting -* Conduct guidelines -* Maintaining the list of additional Collaborators - -For the current list of WG members, see the project -[README.md](./README.md#current-project-team-members). - -## Collaborators - -The [nodejs/nodejs.org](https://github.com/nodejs/nodejs.org) -GitHub repository is maintained by the WG and additional Collaborators who are -added by the WG on an ongoing basis. - -Individuals making significant and valuable contributions are made -Collaborators and given commit-access to the project. These -individuals are identified by the WG. - -_Note:_ If you make a significant contribution and are not considered -for commit-access log an issue or contact a WG member directly. - -Modifications of the contents of the `nodejs/nodejs.org` repository are made on -a collaborative basis. Anybody with a GitHub account may propose a -modification via pull request and it will be considered by the project -Collaborators. All pull requests must be reviewed and accepted by a -Collaborator with sufficient expertise who is able to take full -responsibility for the change. In the case of pull requests proposed -by an existing Collaborator, an additional Collaborator is required -for sign-off. Consensus should be sought if additional Collaborators -participate and there is disagreement around a particular -modification. - -Collaborators may opt to elevate significant or controversial -modifications, or modifications that have not found consensus to the -WG for discussion by assigning the ***WG-agenda*** tag to a pull -request or issue. The WG should serve as the final arbiter where -required. - -For the current list of Collaborators, see the project -[README.md](./README.md#website-working-group-collaborators). - -## WG Membership - -WG seats are not time-limited. There is no fixed size of the WG. -However, the expected target is between 6 and 12, to ensure adequate -coverage of important areas of expertise, balanced with the ability to -make decisions efficiently. - -There is no specific set of requirements or qualifications for WG -membership beyond these rules. - -The WG may add additional members to the WG by unanimous consensus. - -A WG member may be removed from the WG by voluntary resignation, or by -unanimous consensus of all other WG members. - -No more than 1/3 of the WG members may be affiliated with the same -employer. If removal or resignation of a WG member, or a change of -employment by a WG member, creates a situation where more than 1/3 of -the WG membership shares an employer, then the situation must be -immediately remedied by the resignation or removal of one or more WG -members affiliated with the over-represented employer(s). diff --git a/LICENSE b/LICENSE index e2e6f65bc114f..896a53b840d2b 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,5 @@ -The original contents of the nodejs.org repo are licensed for use as follows: +MIT License -""" Copyright Node.js Website WG contributors. All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy @@ -20,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" diff --git a/README.md b/README.md index 6f0c0b1444e45..fa8ee3f021916 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,127 @@ -# [nodejs.org](https://nodejs.org/) - -[![Build Status](https://img.shields.io/travis/nodejs/nodejs.org/master.svg)](http://travis-ci.org/nodejs/nodejs.org) -[![Dependency Status](https://img.shields.io/david/nodejs/nodejs.org.svg)](https://david-dm.org/nodejs/nodejs.org) -[![MIT Licensed](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) +

+
+ + + + + + +

+ +

+ Node.js Website built using Next.js with TypeScript, CSS Modules/Tailwind, and MDXv3 +

+ +

+ + MIT License + + + Crowdin Badge + + + + + Powered by Vercel + + +
+ Build and Analysis Checks + + nodejs.org scorecard badge + +
+
+

## What is this repo? -[nodejs.org](https://nodejs.org) by the [Node.js Foundation](https://nodejs.org/foundation/) builds on the merged community's past website projects to form a self-publishing, community-managed version of the previous site. +[Nodejs.org](https://nodejs.org/), maintained by the [OpenJS Foundation](https://openjsf.org/), is the official website for the Node.js® JavaScript runtime. This repo is the source code for the website. It is built using [Next.js](https://nextjs.org), a React Framework. -On a technical level, inspiration has been taken from the `iojs.org` repo while design and content has been migrated from the old [nodejs.org repo](https://github.com/nodejs/nodejs.org-archive). These technical changes have helped to facilitate community involvement and empower the foundation's internationalization communities to provide alternative website content in other languages. +```bash +pnpm install --frozen-lockfile +pnpm dev -This repo's issues section has become the primary home for the Website WG's coordination efforts (meeting planning, minute approval, etc.). +# Listening at http://localhost:3000 +``` ## Contributing -Please contribute! There are plenty of [good first issues](https://github.com/nodejs/nodejs.org/labels/good%20first%20issue) to work on. To get started, you have to [fork](https://github.com/nodejs/nodejs.org/fork) this repo to your own GitHub account first. Then open up a terminal on your machine and enter the following commands: +This project adopts the Node.js [Code of Conduct][]. -```bash -$ git clone https://github.com//nodejs.org.git -$ cd nodejs.org -$ npm install -$ npm start -``` +Anyone who wants to contribute to the website is welcome! Please read [Contribution Guidelines][] and see the [Figma Design][] to understand better the structure of this repository. -This will start the development server on http://localhost:8080/en/ and should reload automatically when you make changes but it's all just code and no code is perfect so sometimes you may need to restart it. :) +> \[!IMPORTANT]\ +> Please read our [Translation Guidelines][] before contributing to Translation and Localization of the Website -Note: You'll need Node.js v4 or newer as the build system uses some native ES2015 features. +> \[!NOTE]\ +> We recommend a read of all Relevant Links below before doing code changes; Including Dependency changes, Content changes, and Code changes. -If you want to submit a new feature or a bugfix, the best way is to create the changes in a separate branch, e.g.: `git checkout -b feature/mycoolfeature`. This will make it easier for you to submit a pull request and get your feature merged. +### Deployment -### Layout +The Website is automatically deployed to [Vercel](https://vercel.com) through its GitHub App integration when new pushes happen on the `main` branch. -* Page templates are in `/layouts` -* Global styles are in `/layouts/css` -* Global static files are in `/static` -* All content and localization specific styles are in `/locale` - * Initial development usually happens in English: `/locale/en` - * `/locale/{{locale}}/site.json` is where global localization information lives. - * All content is in Markdown and is per locale. - * The top of each Markdown file is a block of YAML for page specific localization information that is passed to various templates. - * The bulk of the Markdown content for each page is referenced as `{{{content}}}` in the corresponding template. +Details regarding the deployment are only accessible to the maintainers of the Website Team due to certain limitations. -### Deployment +The current integration is owned by the OpenJS Foundation and managed by the Website Team. + +
+ Legacy Deployment + +The full setup is in minus secrets and certificates. + +The webhook is set up on GitHub for this project and talks to a small Node server on the host, which does the work. See the [github-webhook](https://github.com/rvagg/github-webhook) package for this. + +
+ +## Node.js Binaries & API Docs + +This repository does not contain the codebase or related infrastructure that serves `https://nodejs.org/api/`, `https://nodejs.org/docs/` or `https://nodejs.org/dist/`. + +These are maintained in different repositories and we urge users to open **issues in their respective repositories**, for bug reports, feature requests or any matter related to these endpoints. + +- [`release-cloudflare-worker`](https://github.com/nodejs/release-cloudflare-worker): The codebase responsible for serving the Node.js Distribution Binaries, API docs and any other assets from the links mentioned above. + - We use Cloudflare R2 Buckets for storing our Assets and Cloudflare Workers for serving these Assets to the Web. +- [`node/doc/api`](https://github.com/nodejs/node/tree/main/doc/api): The source code of our API docs, it contains all the Node.js API Documentation Markdown files +- [`doc-kit`](https://github.com/nodejs/doc-kit): The tooling that builds and compiles our API docs. Also responsible for generating what you see when accessing `https://nodejs.org/api/`. + +## Relevant Links + +[Code of Conduct][] + +[Contribution Guidelines][] + +[Collaborator Guide][] + +[Governance Document][] + +[Figma Design][] + +[Content vs Code][] + +[Dependency Pinning][] + +[Translation Guidelines][] + +[Status Page](https://status.nodejs.org/) of the Node.js web infrastructure. + +## Thanks + +- Thanks to all contributors and collaborators that make this project possible. +- Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions. +- Thanks to [Vercel](https://www.vercel.com/) for providing the infrastructure that serves and powers the Node.js Website +- Thanks to [Cloudflare](https://cloudflare.com) for providing the infrastructure that serves Node.js's Website, Node.js's CDN and more. + - A really warm thank you to Cloudflare as we would not be able to serve our community without their immense support. +- Thanks to [Sentry](https://sentry.io/welcome/) for providing an open source license for their error reporting, monitoring and diagnostic tools. +- Thanks to [Crowdin](https://crowdin.com/) for providing a platform that allows us to localize the Node.js Website and collaborate with translators. +- Thanks to [Orama](https://docs.oramasearch.com/) for providing a search platform that indexes our expansive content and provides lightning-fast results for our users. +- Thanks to [DigitalOcean](https://www.digitalocean.com/) for generously providing Node.js with credits as part of their open source program. -Full set up is in https://github.com/nodejs/build/tree/master/setup/www minus secrets and certificates. The webhook is setup on GitHub for this project and talks to a small Node server on the host which does the work. See the [github-webhook](https://github.com/rvagg/github-webhook) package for this. - -## Governance and Current Members - -All of the Node.js Foundation websites, including this repo, are jointly governed by the **Website Working Group**. See [GOVERNANCE.md](./GOVERNANCE.md) to learn more about the group's structure and [CONTRIBUTING.md](./CONTRIBUTING.md) for guidance about the expectations for all contributors to this project. - -### Content vs. Code - -The Website Working Group is primarily concerned with the code and overall structure of the website. - -The content of the website comes from a variety of working groups (Evangelism, Core, i18n, etc). -The Website WG defers to these WGs on matters of content and routinely adds collaborators from these -working groups as they add and improve content on the website. In other words, the Website WG is not -and *editorial* Working Group except when no other Working Group has taken responsibility for a -content area. - -### Website Working Group Members - -- Yuta Hiroto ([abouthiroppy](https://github.com/abouthiroppy)) -- Andy Gout ([andygout](https://github.com/andygout)) -- Austin Winstanley ([AustinWinstanley](https://github.com/AustinWinstanley)) -- Benget Nata ([bentinata](https://github.com/bentinata)) -- &! (bitandbang) ([bnb](https://github.com/bnb)) -- Divjot Singh ([bogas04](https://github.com/bogas04)) -- Bryce Baril ([brycebaril](https://github.com/brycebaril)) -- Csaba Palfi ([csabapalfi](https://github.com/csabapalfi)) -- Bruno Heridet ([Delapouite](https://github.com/Delapouite)) -- Steven Sinatra ([diagramatics](https://github.com/diagramatics)) -- Evan Lucas ([evanlucas](https://github.com/evanlucas)) -- Fábio Santos ([fabiosantoscode](https://github.com/fabiosantoscode)) -- Jeremiah Senkpiel ([Fishrock123](https://github.com/Fishrock123)) -- Frederic Hemberger ([fhemberger](https://github.com/fhemberger)) -- Wyatt Preul ([geek](https://github.com/geek)) -- Charlie Robbins ([indexzero](https://github.com/indexzero)) -- Minwoo Jung ([JungMinu](https://github.com/JungMinu)) -- Daniel Levy ([justsml](https://github.com/justsml)) -- Kohei TAKATA ([kohei-takata](https://github.com/kohei-takata)) -- Luigi Pinca ([lpinca](https://github.com/lpinca)) -- marocchino ([marocchino](https://github.com/marocchino)) -- Massimiliano Mura ([MassimilianoMura](https://github.com/MassimilianoMura)) -- Matthew Loring ([matthewloring](https://github.com/matthewloring)) -- Mikeal Rogers ([mikeal](https://github.com/mikeal)) -- Mike Dolan ([mkdolan](https://github.com/mkdolan)) -- Kiffie Liversage ([mrkiffie](https://github.com/mrkiffie)) -- Vladimir Varankin ([narqo](https://github.com/narqo)) -- Patrick Heneise ([PatrickHeneise](https://github.com/PatrickHeneise)) -- Paul Grock ([paulgrock](https://github.com/paulgrock)) -- Phillip Johnsen ([phillipj](https://github.com/phillipj)) -- rnsloan ([rnsloan](https://github.com/rnsloan)) -- Robert Kowalski ([robertkowalski](https://github.com/robertkowalski)) -- Rod Vagg ([rvagg](https://github.com/rvagg)) -- Seth Thompson ([s3ththompson](https://github.com/s3ththompson)) -- silverwind ([silverwind](https://github.com/silverwind)) -- Sean Ouimet ([snostorm](https://github.com/snostorm)) -- Steven R. Loomis ([srl295](https://github.com/srl295)) -- Steve Mao ([stevemao](https://github.com/stevemao)) -- Michaël Zasso ([targos](https://github.com/targos)) -- Myles Borins ([MylesBorins](https://github.com/MylesBorins)) -- Sakthipriyan Vairamani ([thefourtheye](https://github.com/thefourtheye)) -- Trent Oswald ([therebelrobot](https://github.com/therebelrobot)) -- Anton Wilhelm ([timaschew](https://github.com/timaschew)) -- Thomas Jensen ([tjconcept](https://github.com/tjconcept)) -- Rich Trott ([Trott](https://github.com/Trott)) -- wonderdogone ([wonderdogone](https://github.com/wonderdogone)) -- Xcat Liu ([xcatliu](https://github.com/xcatliu)) -- Francisco Baio Dias ([xicombd](https://github.com/xicombd)) -- Yosuke Furukawa ([yosuke-furukawa](https://github.com/yosuke-furukawa)) -- Chayoung You ([yous](https://github.com/yous)) -- Zeke Sikelianos ([zeke](https://github.com/zeke)) +[code of conduct]: https://github.com/nodejs/admin/blob/main/CODE_OF_CONDUCT.md +[contribution guidelines]: https://github.com/nodejs/nodejs.org/blob/main/CONTRIBUTING.md +[content vs code]: https://github.com/nodejs/nodejs.org/blob/main/docs/content-vs-code.md +[dependency pinning]: https://github.com/nodejs/nodejs.org/blob/main/docs/dependency-pinning.md +[collaborator guide]: https://github.com/nodejs/nodejs.org/blob/main/docs/collaborator-guide.md +[governance document]: https://github.com/nodejs/web-team/blob/main/GOVERNANCE.md +[figma design]: https://www.figma.com/file/a10cjjw3MzvRQMPT9FP3xz +[translation guidelines]: https://github.com/nodejs/nodejs.org/blob/main/docs/translation.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000000..d6a456a375a0f --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# Security + +## Reporting a vulnerability to Node.js Website + +Please report security issues **privately** using the **GitHub Security Advisory** +workflow ([Security → “Report a vulnerability”](https://github.com/nodejs/nodejs.org/security/advisories/new)). + +Do **not** open a public GitHub issue for security problems. + +We aim to acknowledge reports within **7 business days**. +If you do **not** receive an acknowledgement within **7 business days**, +forward your report to **[tsc@nodejs.org](mailto:tsc@nodejs.org)**. + +## Disclosure & advisories + +Confirmed vulnerabilities will be published as a **GitHub Security Advisory** +(and assigned a CVE when applicable). Notices are also shared via: + +- Node.js blog advisories: [https://nodejs.org/blog/vulnerability/](https://nodejs.org/blog/vulnerability/) + when necessary. diff --git a/TRANSLATION.md b/TRANSLATION.md deleted file mode 100644 index 7890110ca73b2..0000000000000 --- a/TRANSLATION.md +++ /dev/null @@ -1,73 +0,0 @@ -# Node.js Website Translation Policy - -Node.js is a global platform and so this site has many translations. The translation of the site into -languages other than English is handled by the localization working group of the language in question. If you -would like to contribute to the translation of nodejs.org, please refer to the following process: - - -## For Individuals wanting to contribute - -* Contact your appropriate localization group, and discuss with them the best possible way to contribute. A list of the localization groups can be found here (please keep in mind that some groups have not yet taken action upon the project name changes, hence the outdated repository names): - - * [`nodejs-ar`](https://github.com/nodejs/nodejs-ar) Arabic Community - * [`nodejs-bg`](https://github.com/nodejs/nodejs-bg) Bulgarian Community - * [`nodejs-bn`](https://github.com/nodejs/nodejs-bn) Bengali Community - * [`nodejs-zh-CN`](https://github.com/nodejs/nodejs-zh-CN) Chinese Community - * [`nodejs-cs`](https://github.com/nodejs/nodejs-cs) Czech Community - * [`nodejs-da`](https://github.com/nodejs/nodejs-da) Danish Community - * [`nodejs-de`](https://github.com/nodejs/nodejs-de) German Community - * [`nodejs-el`](https://github.com/nodejs/nodejs-el) Greek Community - * [`nodejs-es`](https://github.com/nodejs/nodejs-es) Spanish Community - * [`nodejs-fa`](https://github.com/nodejs/nodejs-fa) Persian Community - * [`nodejs-fi`](https://github.com/nodejs/nodejs-fi) Finnish Community - * [`nodejs-fr`](https://github.com/nodejs/nodejs-fr) French Community - * [`nodejs-he`](https://github.com/nodejs/nodejs-he) Hebrew Community - * [`nodejs-hi`](https://github.com/nodejs/nodejs-hi) Hindi Community - * [`nodejs-hu`](https://github.com/nodejs/nodejs-hu) Hungarian Community - * [`nodejs-id`](https://github.com/nodejs/nodejs-id) Indonesian Community - * [`nodejs-it`](https://github.com/nodejs/nodejs-it) Italian Community - * [`nodejs-ja`](https://github.com/nodejs/nodejs-ja) Japanese Community - * [`nodejs-ka`](https://github.com/nodejs/nodejs-ka) Georgian Community - * [`nodejs-ko`](https://github.com/nodejs/nodejs-ko) Korean Community - * [`nodejs-mk`](https://github.com/nodejs/nodejs-mk) Macedonian Community - * [`nodejs-ms`](https://github.com/nodejs/nodejs-ms) Malaysian Community - * [`nodejs-nl`](https://github.com/nodejs/nodejs-nl) Dutch Community - * [`nodejs-no`](https://github.com/nodejs/nodejs-no) Norwegian Community - * [`nodejs-pl`](https://github.com/nodejs/nodejs-pl) Polish Community - * [`nodejs-pt`](https://github.com/nodejs/nodejs-pt) Portuguese Community - * [`nodejs-ro`](https://github.com/nodejs/nodejs-ro) Romanian Community - * [`nodejs-ru`](https://github.com/nodejs/nodejs-ru) Russian Community - * [`nodejs-sv`](https://github.com/nodejs/nodejs-sv) Swedish Community - * [`nodejs-ta`](https://github.com/nodejs/nodejs-ta) Tamil Community - * [`nodejs-tr`](https://github.com/nodejs/nodejs-tr) Turkish Community - * [`nodejs-zh-TW`](https://github.com/nodejs/nodejs-zh-TW) Taiwan Community - * [`nodejs-uk`](https://github.com/nodejs/nodejs-uk) Ukrainian Community - * [`nodejs-vi`](https://github.com/nodejs/nodejs-vi) Vietnamese Community - - -## For Localization Groups - -* Ensure that any site translations are done as pull requests into the appropriate language folder in this repo. This will ensure the build process, layout, and styling, remain consistent across the different translations of the site. - -* You can find the appropriate language folder within `locale/`. If not, create one matching the two-letter [ISO code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) of your language. To specify a dialect for your language, [separate it with a dash](https://en.wikipedia.org/wiki/IETF_language_tag) (e.g. `pt-br` for Brazilian Portuguese, `zh-tw` for Taiwanese Mandarin). - -* The following files need to be in your language folder: - - * `site.json` (this contains the basic settings and navigation structure for the website) - * `index.md` (this contains the Markdown translation for the home page.) - * `styles.styl` (this imports the necessary Stylus files) - * All files and files in subfolders that end in `.md` are content pages and should be translated. - -* Prefix your PR with the localization group's name (e.g. `nodejs-no`). If you are only translating one of the above files, please mention them in your PR's subject as well, e.g.: - -``` - nodejs-de: Add files - index.md, faq.md - nodejs-ro: Add files - 15 files - - nodejs-fr: Update files - es6.md - nodejs-ja: Update files - all files -``` - -* Do not make language specific changes to layout or styling in a translation PR. If they are needed, make a separate styling/layout pr and talk with one of the website WG about the change. We want to make sure, for example, a Chinese layout change doesn't cascade failure to the German page. - -* To be merged, translation PR's require a Website WG +1 and a +1 from another native speaker in your language. Make sure whoever you have review the PR adds a +1 in the comments of it so we know it is translated properly. diff --git a/apps/site/.lintstagedrc.json b/apps/site/.lintstagedrc.json new file mode 100644 index 0000000000000..a1bc170c02b46 --- /dev/null +++ b/apps/site/.lintstagedrc.json @@ -0,0 +1,5 @@ +{ + "**/*.{js,mjs,ts,tsx,md,mdx}": ["prettier --check --write", "eslint --fix"], + "**/*.css": ["stylelint --allow-empty-input --fix", "prettier --write"], + "**/*.{json,yml}": ["prettier --check --write"] +} diff --git a/apps/site/.postcssrc.json b/apps/site/.postcssrc.json new file mode 100644 index 0000000000000..d750bab9d9e52 --- /dev/null +++ b/apps/site/.postcssrc.json @@ -0,0 +1,6 @@ +{ + "plugins": { + "postcss-calc": {}, + "@tailwindcss/postcss": {} + } +} diff --git a/apps/site/.remarkrc.json b/apps/site/.remarkrc.json new file mode 100644 index 0000000000000..721eda175ea09 --- /dev/null +++ b/apps/site/.remarkrc.json @@ -0,0 +1,13 @@ +{ + "plugins": [ + "remark-frontmatter", + "@node-core/remark-lint", + [ + "@node-core/remark-lint/rules/validate-links", + { + "ignoreFiles": ["pages/!(en)/**/*.{md,mdx}"], + "ignoreLinks": ["/learn", "/learn/**/*", "/static/**/*", "/feed/*"] + } + ] + ] +} diff --git a/apps/site/.stylelintignore b/apps/site/.stylelintignore new file mode 100644 index 0000000000000..bb4f97f4cd44d --- /dev/null +++ b/apps/site/.stylelintignore @@ -0,0 +1,23 @@ +# Next.js files +.next +.turbo +.swc +build + +# Public Folder +public + +# Test Runner +junit.xml +lcov.info + +# Old Styles +styles/old + +# Cloudflare Build Output +.open-next +.wrangler + +# Playwright +test-results +playwright-report diff --git a/apps/site/.stylelintrc.mjs b/apps/site/.stylelintrc.mjs new file mode 100644 index 0000000000000..cbb76b9e67ca4 --- /dev/null +++ b/apps/site/.stylelintrc.mjs @@ -0,0 +1,54 @@ +// These are all the custom `@` (at) rules that we use within our custom PostCSS plugins +const CUSTOM_AT_RULES = [ + // Tailwind-specific at-rules + 'apply', + 'layer', + 'responsive', + 'reference', + 'utility', + 'theme', + 'custom-variant', + 'screen', + 'source', + 'tailwind', + 'variants', +]; + +// Enforces certain selectors to be only in camelCase notation +// We use these for id selectors and classname selectors +const ONLY_ALLOW_CAMEL_CASE_SELECTORS = [ + /^(?:[a-z]+(?:[A-Z][a-z]*)*)$/, + { message: s => `Expected '${s}' to be in camelCase` }, +]; + +export default { + extends: ['stylelint-config-standard'], + plugins: [ + 'stylelint-order', + 'stylelint-selector-bem-pattern', + '@node-core/ui-components/src/stylelint/one-utility-class-per-line.mjs', + ], + rules: { + // Enforces Element Class Names to be camelCase + 'selector-class-pattern': ONLY_ALLOW_CAMEL_CASE_SELECTORS, + // Enforces Element IDs to be camelCase + 'selector-id-pattern': ONLY_ALLOW_CAMEL_CASE_SELECTORS, + // Allow Tailwind-based CSS Rules + 'at-rule-no-unknown': [true, { ignoreAtRules: CUSTOM_AT_RULES }], + // Allow the Global CSS Selector + 'selector-pseudo-class-no-unknown': [ + true, + { ignorePseudoClasses: ['global'] }, + ], + // Enforces the order of the CSS properties to be in alphabetical order + 'order/properties-alphabetical-order': true, + 'no-descending-specificity': null, + // Disables the Level-4 Media Queries; Since they're more exotic and less known + 'media-feature-range-notation': 'prefix', + // Adopts the import notation from `postcss-import` + 'import-notation': 'string', + // Allow the `@apply` at rule as its part of Tailwind + 'at-rule-no-deprecated': [true, { ignoreAtRules: CUSTOM_AT_RULES }], + 'nodejs/one-utility-class-per-line': true, + }, +}; diff --git a/apps/site/app/[locale]/[...path]/page.tsx b/apps/site/app/[locale]/[...path]/page.tsx new file mode 100644 index 0000000000000..aecb38f3da63f --- /dev/null +++ b/apps/site/app/[locale]/[...path]/page.tsx @@ -0,0 +1,97 @@ +/** + * This file extends on the `page.tsx` file, which is the default file that is used to render + * the entry points for each locale and then also reused within the [...path] route to render the + * and contains all logic for rendering our dynamic and static routes within the Node.js Website. + * + * Note: that each `page.tsx` should have its own `generateStaticParams` to prevent clash of + * dynamic params, which will lead on static export errors and other sort of issues. + */ + +import { availableLocaleCodes, defaultLocale } from '@node-core/website-i18n'; +import { notFound } from 'next/navigation'; + +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { ENABLE_STATIC_EXPORT_LOCALE } from '#site/next.constants.mjs'; +import { dynamicRouter } from '#site/next.dynamic.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; + +import type { DynamicParams } from '#site/types'; +import type { FC } from 'react'; + +type PageParams = DynamicParams<{ path: Array }>; + +// This is the default Viewport Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function +export const generateViewport = basePage.generateViewport; + +// This generates each page's HTML Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata +export const generateMetadata = basePage.generateMetadata; + +// Generates all possible static paths based on the locales and environment configuration +// - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false) +// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales +// - Otherwise, generates paths only for the default locale +// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +export const generateStaticParams = async () => { + // Return an empty array if static export is disabled + if (!ENABLE_STATIC_EXPORT) { + return []; + } + + const routes = await dynamicRouter.getAllRoutes(); + + // Helper function to fetch and map routes for a specific locale + const getRoutesForLocale = async (l: string) => + routes.map(pathname => dynamicRouter.mapPathToRoute(l, pathname)); + + // Determine which locales to include in the static export + const locales = ENABLE_STATIC_EXPORT_LOCALE + ? availableLocaleCodes + : [defaultLocale.code]; + + // Generates all possible routes for all available locales + const routesWithLocales = await Promise.all(locales.map(getRoutesForLocale)); + + return routesWithLocales.flat().sort(); +}; + +// This method parses the current pathname and does any sort of modifications needed on the route +// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component +// finally it returns (if the locale and route are valid) the React Component with the relevant context +// and attached context providers for rendering the current page +const getPage: FC = async props => { + const { path, locale: routeLocale } = await props.params; + + // Gets the current full pathname for a given path + const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale); + + // Gets the Markdown content and context + const [content, context] = await basePage.getMarkdownContext({ + locale, + pathname, + }); + + // If we have a filename and layout then we have a page + if (context.filename && context.frontmatter.layout) { + return basePage.renderPage({ + content, + layout: context.frontmatter.layout, + context, + }); + } + + return notFound(); +}; + +// Enforces that this route is used as static rendering +// Except whenever on the Development mode as we want instant-refresh when making changes +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = 300; + +export default getPage; diff --git a/apps/site/app/[locale]/blog/[...path]/page.tsx b/apps/site/app/[locale]/blog/[...path]/page.tsx new file mode 100644 index 0000000000000..2a11e65becc46 --- /dev/null +++ b/apps/site/app/[locale]/blog/[...path]/page.tsx @@ -0,0 +1,82 @@ +import { defaultLocale } from '@node-core/website-i18n'; +import { notFound } from 'next/navigation'; + +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import { BLOG_DYNAMIC_ROUTES } from '#site/next.dynamic.constants.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; + +import type { DynamicParams } from '#site/types'; +import type { FC } from 'react'; + +type PageParams = DynamicParams<{ path: Array }>; + +// This is the default Viewport Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function +export const generateViewport = basePage.generateViewport; + +// This generates each page's HTML Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata +export const generateMetadata = ({ params }: PageParams) => + basePage.generateMetadata({ params, prefix: 'blog' }); + +// Generates all possible static paths based on the locales and environment configuration +// - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false) +// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales +// - Otherwise, generates paths only for the default locale +// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +export const generateStaticParams = async () => { + // Return an empty array if static export is disabled + if (!ENABLE_STATIC_EXPORT) { + return []; + } + + return BLOG_DYNAMIC_ROUTES.map(pathname => ({ + locale: defaultLocale.code, + path: pathname.split('/'), + })); +}; + +// This method parses the current pathname and does any sort of modifications needed on the route +// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component +// finally it returns (if the locale and route are valid) the React Component with the relevant context +// and attached context providers for rendering the current page +const getPage: FC = async props => { + const { path, locale: routeLocale } = await props.params; + + // Gets the current full pathname for a given path + const [locale, pathname] = basePage.getLocaleAndPath(path, routeLocale); + + // Verifies if the current route is a dynamic route + const isDynamicRoute = BLOG_DYNAMIC_ROUTES.some(r => r.includes(pathname)); + + // Gets the Markdown content and context for Blog pages + // otherwise this is likely a blog-category or a blog post + const [content, context] = await basePage.getMarkdownContext({ + locale, + pathname: `blog/${pathname}`, + }); + + // If this isn't a valid dynamic route for blog post or there's no markdown file + // for this, then we fail as not found as there's nothing we can do. + if (isDynamicRoute || context.filename) { + return basePage.renderPage({ + content, + layout: context.frontmatter.layout ?? 'blog-category', + context: { ...context, pathname: `/blog/${pathname}` }, + }); + } + + return notFound(); +}; + +// Enforces that this route is used as static rendering +// Except whenever on the Development mode as we want instant-refresh when making changes +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = 300; + +export default getPage; diff --git a/apps/site/app/[locale]/download/archive/[version]/page.tsx b/apps/site/app/[locale]/download/archive/[version]/page.tsx new file mode 100644 index 0000000000000..1cc23837ccdf1 --- /dev/null +++ b/apps/site/app/[locale]/download/archive/[version]/page.tsx @@ -0,0 +1,93 @@ +import { defaultLocale } from '@node-core/website-i18n'; +import { notFound, redirect } from 'next/navigation'; + +import provideReleaseData from '#site/next-data/providers/releaseData'; +import provideReleaseVersions from '#site/next-data/providers/releaseVersions'; +import { ENABLE_STATIC_EXPORT } from '#site/next.constants.mjs'; +import * as basePage from '#site/next.dynamic.page.mjs'; + +import type { DynamicParams } from '#site/types'; +import type { FC } from 'react'; + +type PageParams = DynamicParams<{ version: string }>; + +// This is the default Viewport Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-viewport#generateviewport-function +export const generateViewport = basePage.generateViewport; + +// This generates each page's HTML Metadata +// @see https://nextjs.org/docs/app/api-reference/functions/generate-metadata +export const generateMetadata = basePage.generateMetadata; + +// Generates all possible static paths based on the locales and environment configuration +// - Returns an empty array if static export is disabled (`ENABLE_STATIC_EXPORT` is false) +// - If `ENABLE_STATIC_EXPORT_LOCALE` is true, generates paths for all available locales +// - Otherwise, generates paths only for the default locale +// @see https://nextjs.org/docs/app/api-reference/functions/generate-static-params +export const generateStaticParams = async () => { + // Return an empty array if static export is disabled + if (!ENABLE_STATIC_EXPORT) { + return []; + } + + const versions = await provideReleaseVersions(); + + return versions.map(version => ({ + locale: defaultLocale.code, + version, + })); +}; + +// This method parses the current pathname and does any sort of modifications needed on the route +// then it proceeds to retrieve the Markdown file and parse the MDX Content into a React Component +// finally it returns (if the locale and route are valid) the React Component with the relevant context +// and attached context providers for rendering the current page +const getPage: FC = async props => { + const { version, locale: routeLocale } = await props.params; + + // Gets the current full pathname for a given path + const [locale, pathname] = basePage.getLocaleAndPath(version, routeLocale); + + if (version === 'current') { + const releaseData = await provideReleaseData(); + + const release = releaseData.find(release => release.status === 'Current'); + + redirect(`/${locale}/download/archive/${release?.versionWithPrefix}`); + } + + const versions = await provideReleaseVersions(); + + // Verifies if the current route is a dynamic route + const isDynamicRoute = versions.some(r => r.includes(pathname)); + + // Gets the Markdown content and context for Download Archive pages + const [content, context] = await basePage.getMarkdownContext({ + locale, + pathname: 'download/archive', + }); + + // If this isn't a valid dynamic route for archive version or there's no markdown + // file for this, then we fail as not found as there's nothing we can do. + if (isDynamicRoute && context.filename) { + return basePage.renderPage({ + content, + layout: context.frontmatter.layout!, + context: { ...context, pathname: `/download/archive/${pathname}` }, + }); + } + + return notFound(); +}; + +// Enforces that this route is used as static rendering +// Except whenever on the Development mode as we want instant-refresh when making changes +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = 300; + +export default getPage; diff --git a/apps/site/app/[locale]/error.tsx b/apps/site/app/[locale]/error.tsx new file mode 100644 index 0000000000000..0c4bf7f827ec8 --- /dev/null +++ b/apps/site/app/[locale]/error.tsx @@ -0,0 +1,30 @@ +'use client'; + +import { useTranslations } from 'next-intl'; + +import Button from '#site/components/Common/Button'; +import GlowingBackdropLayout from '#site/layouts/GlowingBackdrop'; + +import type { FC } from 'react'; + +const ErrorPage: FC<{ error: Error }> = () => { + const t = useTranslations(); + + return ( + + 500 + +

+ {t('layouts.error.internalServerError.title')} +

+ +

+ {t('layouts.error.internalServerError.description')} +

+ + +
+ ); +}; + +export default ErrorPage; diff --git a/apps/site/app/[locale]/feed/[feed]/route.ts b/apps/site/app/[locale]/feed/[feed]/route.ts new file mode 100644 index 0000000000000..c70c0b63f9b3d --- /dev/null +++ b/apps/site/app/[locale]/feed/[feed]/route.ts @@ -0,0 +1,45 @@ +import { defaultLocale } from '@node-core/website-i18n'; +import { NextResponse } from 'next/server'; + +import { siteConfig } from '#site/next.json.mjs'; +import { getFeeds } from '#site/util/feeds'; + +type DynamicStaticPaths = { locale: string; feed: string }; +type StaticParams = { params: Promise }; + +// This is the Route Handler for the `GET` method which handles the request +// for the Node.js Website Blog Feeds (RSS) +// @see https://nextjs.org/docs/app/building-your-application/routing/router-handlers +export const GET = async (_: Request, props: StaticParams) => { + const params = await props.params; + + // Generate the Feed for the given feed type (blog, releases, etc) + const websiteFeed = getFeeds(params.feed); + + return new NextResponse(websiteFeed, { + headers: { 'Content-Type': 'application/xml' }, + status: websiteFeed !== undefined ? 200 : 404, + }); +}; + +// This function generates the static paths that come from the dynamic segments +// `[locale]/feeds/[feed]` and returns an array of all available static paths +// This is used for ISR static validation and generation +export const generateStaticParams = async () => + siteConfig.rssFeeds.map(feed => ({ + locale: defaultLocale.code, + feed: feed.file, + })); + +// Enforces that only the paths from `generateStaticParams` are allowed, giving 404 on the contrary +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamicparams +export const dynamicParams = false; + +// Enforces that this route is cached and static as much as possible +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic +export const dynamic = 'force-static'; + +// Ensures that this endpoint is invalidated and re-executed every X minutes +// so that when new deployments happen, the data is refreshed +// @see https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#revalidate +export const revalidate = 300; diff --git a/apps/site/app/[locale]/layout.tsx b/apps/site/app/[locale]/layout.tsx new file mode 100644 index 0000000000000..7b905a0fd03a3 --- /dev/null +++ b/apps/site/app/[locale]/layout.tsx @@ -0,0 +1,53 @@ +import PlatformAnalytics from '#platform/analytics'; +import { availableLocales, defaultLocale } from '@node-core/website-i18n'; +import classNames from 'classnames'; +import { NextIntlClientProvider } from 'next-intl'; + +import BaseLayout from '#site/layouts/Base'; +import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts'; +import { ThemeProvider } from '#site/providers/themeProvider'; + +import type { FC, PropsWithChildren } from 'react'; + +import '#site/styles/index.css'; + +const fontClasses = classNames(IBM_PLEX_MONO.variable, OPEN_SANS.variable); + +type RootLayoutProps = PropsWithChildren<{ + params: Promise<{ locale: string }>; +}>; + +const RootLayout: FC = async ({ children, params }) => { + const { locale } = await params; + + const { langDir, hrefLang } = + availableLocales.find(l => l.code === locale) || defaultLocale; + + return ( + + + + + {children} + + + + ; + } + + return ( + , 'href'>)} + /> + ); +}; + +export default Link; diff --git a/apps/site/components/MDX/Calendar/Event/index.module.css b/apps/site/components/MDX/Calendar/Event/index.module.css new file mode 100644 index 0000000000000..f192532d77c24 --- /dev/null +++ b/apps/site/components/MDX/Calendar/Event/index.module.css @@ -0,0 +1,23 @@ +@reference "../../../../styles/index.css"; + +.event { + @apply flex + w-fit + flex-col + gap-1; + + .title { + @apply flex + flex-row + gap-2; + + span { + @apply text-sm + font-bold; + } + } + + a { + @apply text-sm; + } +} diff --git a/apps/site/components/MDX/Calendar/Event/index.tsx b/apps/site/components/MDX/Calendar/Event/index.tsx new file mode 100644 index 0000000000000..63f63441e49ff --- /dev/null +++ b/apps/site/components/MDX/Calendar/Event/index.tsx @@ -0,0 +1,44 @@ +import FormattedTime from '#site/components/Common/FormattedTime'; +import Link from '#site/components/Link'; +import { getZoomLink, isZoned } from '#site/components/MDX/Calendar/utils'; + +import type { CalendarEvent } from '#site/types'; +import type { FC } from 'react'; + +import styles from './index.module.css'; + +type EventProps = Pick< + CalendarEvent, + 'start' | 'end' | 'summary' | 'location' | 'description' +>; + +const Event: FC = ({ + start, + end, + description, + summary, + location, +}) => ( +
+
+ + + + - + + + + (UTC) +
+ + {summary} +
+); + +export default Event; diff --git a/apps/site/components/MDX/Calendar/UpcomingMeetings.tsx b/apps/site/components/MDX/Calendar/UpcomingMeetings.tsx new file mode 100644 index 0000000000000..7a674286db712 --- /dev/null +++ b/apps/site/components/MDX/Calendar/UpcomingMeetings.tsx @@ -0,0 +1,54 @@ +import FormattedTime from '#site/components/Common/FormattedTime'; +import Event from '#site/components/MDX/Calendar/Event'; +import { getZoomLink, isZoned } from '#site/components/MDX/Calendar/utils'; +import { CALENDAR_NODEJS_ID } from '#site/next.calendar.constants.mjs'; +import { getCalendarEvents } from '#site/next.calendar.mjs'; + +import type { CalendarEvent } from '#site/types'; +import type { FC } from 'react'; + +import styles from './calendar.module.css'; + +type GroupedEntries = Record>; + +const UpcomingMeetings: FC = async () => { + const events = await getCalendarEvents(CALENDAR_NODEJS_ID); + + const groupedEntries = events.filter(getZoomLink).reduce((acc, event) => { + const startDate = new Date( + isZoned(event.start) ? event.start.dateTime : event.start.date + ); + + const datePerDay = startDate.toDateString(); + + acc[datePerDay] = acc[datePerDay] || []; + acc[datePerDay].push(event); + + return acc; + }, {} as GroupedEntries); + + const sortedGroupedEntries = Object.entries(groupedEntries).sort( + ([dateA], [dateB]) => new Date(dateA).getTime() - new Date(dateB).getTime() + ); + + return sortedGroupedEntries.map(([date, entries]) => ( +
+

+ +

+ + {entries.map(({ id, start, end, summary, location, description }) => ( + + ))} +
+ )); +}; + +export default UpcomingMeetings; diff --git a/apps/site/components/MDX/Calendar/calendar.module.css b/apps/site/components/MDX/Calendar/calendar.module.css new file mode 100644 index 0000000000000..ef0999b2b9d58 --- /dev/null +++ b/apps/site/components/MDX/Calendar/calendar.module.css @@ -0,0 +1,19 @@ +@reference "../../../styles/index.css"; + +.events { + @apply flex + flex-col + gap-2; + + h4 { + @apply text-xl + font-bold; + } +} + +.summits { + @apply flex + flex-col + gap-3 + md:flex-row; +} diff --git a/apps/site/components/MDX/Calendar/utils.ts b/apps/site/components/MDX/Calendar/utils.ts new file mode 100644 index 0000000000000..0c051431d969d --- /dev/null +++ b/apps/site/components/MDX/Calendar/utils.ts @@ -0,0 +1,10 @@ +import type { CalendarEvent, ZonedCalendarTime } from '#site/types'; + +export const isZoned = (d: object): d is ZonedCalendarTime => + 'dateTime' in d && 'timeZone' in d; + +export const getZoomLink = ( + event: Pick +) => + event.description?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0] || + event.location?.match(/https:\/\/zoom.us\/j\/\d+/)?.[0]; diff --git a/apps/site/components/MDX/CodeBox/index.tsx b/apps/site/components/MDX/CodeBox/index.tsx new file mode 100644 index 0000000000000..4ea12905881aa --- /dev/null +++ b/apps/site/components/MDX/CodeBox/index.tsx @@ -0,0 +1,21 @@ +import { getLanguageDisplayName } from '@node-core/rehype-shiki'; + +import CodeBox from '#site/components/Common/CodeBox'; + +import type { FC, HTMLAttributes } from 'react'; + +const MDXCodeBox: FC> = ({ + children: code, + className, +}) => { + const matches = className?.match(/language-(?[a-zA-Z]+)/); + const language = matches?.groups?.language ?? ''; + + return ( + + {code} + + ); +}; + +export default MDXCodeBox; diff --git a/apps/site/components/MDX/Image/index.tsx b/apps/site/components/MDX/Image/index.tsx new file mode 100644 index 0000000000000..b0da9f581e232 --- /dev/null +++ b/apps/site/components/MDX/Image/index.tsx @@ -0,0 +1,27 @@ +import Image from 'next/image'; + +import type { ImageProps } from 'next/image'; +import type { FC } from 'react'; + +const MDXImage: FC = ({ width, height, alt, src, ...props }) => { + if (!width || !height) { + // Since `width` and `height` are not provided in the Markdown image format, + // we provide the height and width automatically. + // @see https://nextjs.org/docs/pages/building-your-application/optimizing/images + return ( + {alt} + ); + } + + return {alt}; +}; + +export default MDXImage; diff --git a/apps/site/components/Releases/MinorReleasesTable/index.module.css b/apps/site/components/Releases/MinorReleasesTable/index.module.css new file mode 100644 index 0000000000000..9fc65821e7ff4 --- /dev/null +++ b/apps/site/components/Releases/MinorReleasesTable/index.module.css @@ -0,0 +1,43 @@ +@reference "../../../styles/index.css"; + +.additionalLinks { + @apply flex + h-4 + items-center + gap-2; +} + +.items { + @apply flex + h-9 + gap-2; +} + +.scrollable { + @apply scrollbar-thin + flex + max-h-[29rem] + overflow-y-auto; + + table { + @apply ml:border-t-0 + border-t; + } + + th { + @apply ml:border-t + border-neutral-200 + dark:border-neutral-800; + } +} + +.header { + @apply top-0 + z-10 + border-t + bg-white + text-left + font-semibold + sm:sticky + dark:bg-neutral-950; +} diff --git a/apps/site/components/Releases/MinorReleasesTable/index.tsx b/apps/site/components/Releases/MinorReleasesTable/index.tsx new file mode 100644 index 0000000000000..342d2720f200d --- /dev/null +++ b/apps/site/components/Releases/MinorReleasesTable/index.tsx @@ -0,0 +1,86 @@ +import { CodeBracketSquareIcon } from '@heroicons/react/24/outline'; +import Separator from '@node-core/ui-components/Common/Separator'; +import NpmIcon from '@node-core/ui-components/Icons/PackageManager/Npm'; +import { useTranslations } from 'next-intl'; + +import LinkWithArrow from '#site/components/Common/LinkWithArrow'; +import Link from '#site/components/Link'; +import ReleaseOverviewItem from '#site/components/Releases/ReleaseOverview/ReleaseOverviewItem'; +import { BASE_CHANGELOG_URL } from '#site/next.constants.mjs'; +import { getNodeApiUrl } from '#site/util/url'; + +import type { MinorVersion } from '#site/types'; +import type { FC } from 'react'; + +import styles from './index.module.css'; + +type MinorReleasesTableProps = { + releases: Array; +}; + +const MinorReleasesTable: FC = ({ releases }) => { + const t = useTranslations(); + + return ( + + + + + + + + + + + + {releases.map(release => ( + + + + + + + + ))} + +
{t('components.minorReleasesTable.version')}{t('components.minorReleasesTable.nApiVersion')}{t('components.minorReleasesTable.npmVersion')}{t('components.minorReleasesTable.v8Version')}{t('components.minorReleasesTable.links')}
+ + v{release.version} + + + {release.modules && ( + + )} + + {release.npm && ( + + )} + + + +
+ + {t('components.minorReleasesTable.actions.docs')} + + + + {t('components.minorReleasesTable.actions.changelog')} + +
+
+ ); +}; + +export default MinorReleasesTable; diff --git a/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx b/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx new file mode 100644 index 0000000000000..f7c047506aadb --- /dev/null +++ b/apps/site/components/Releases/PreviousReleasesTable/TableBody.tsx @@ -0,0 +1,80 @@ +'use client'; + +import Badge from '@node-core/ui-components/Common/Badge'; +import { useTranslations } from 'next-intl'; +import { Fragment, useState } from 'react'; + +import FormattedTime from '#site/components/Common/FormattedTime'; +import LinkWithArrow from '#site/components/Common/LinkWithArrow'; +import Link from '#site/components/Link'; +import { STATUS_KIND_MAP } from '#site/next.constants.mjs'; + +import type { NodeRelease } from '#site/types'; +import type { FC } from 'react'; + +import ReleaseModal from '../ReleaseModal'; + +type PreviousReleasesTableBodyProps = { + releaseData: Array; +}; + +const PreviousReleasesTableBody: FC = ({ + releaseData, +}) => { + const t = useTranslations(); + + const [currentModal, setCurrentModal] = useState(); + + return ( + + {releaseData.map(release => ( + + + + + v{release.major} + + + + + {release.codename || '-'} + + + + + + + + + + + + + {release.status} + + + + + setCurrentModal(release.version)} + > + {t('components.downloadReleasesTable.details')} + + + + + open || setCurrentModal(undefined)} + /> + + ))} + + ); +}; + +export default PreviousReleasesTableBody; diff --git a/apps/site/components/Releases/PreviousReleasesTable/index.tsx b/apps/site/components/Releases/PreviousReleasesTable/index.tsx new file mode 100644 index 0000000000000..a018f0114732f --- /dev/null +++ b/apps/site/components/Releases/PreviousReleasesTable/index.tsx @@ -0,0 +1,32 @@ +import { getTranslations } from 'next-intl/server'; + +import provideReleaseData from '#site/next-data/providers/releaseData'; + +import type { FC } from 'react'; + +import PreviousReleasesTableBody from './TableBody'; + +const PreviousReleasesTable: FC = async () => { + const releaseData = await provideReleaseData(); + + const t = await getTranslations(); + + return ( + + + + + + + + + + + + + +
{t('components.downloadReleasesTable.version')}{t('components.downloadReleasesTable.codename')}{t('components.downloadReleasesTable.firstReleased')}{t('components.downloadReleasesTable.lastUpdated')}{t('components.downloadReleasesTable.status')}
+ ); +}; + +export default PreviousReleasesTable; diff --git a/apps/site/components/Releases/ReleaseModal.tsx b/apps/site/components/Releases/ReleaseModal.tsx new file mode 100644 index 0000000000000..25036a777c243 --- /dev/null +++ b/apps/site/components/Releases/ReleaseModal.tsx @@ -0,0 +1,44 @@ +import { Modal, Title, Content } from '@node-core/ui-components/Common/Modal'; +import { useTranslations } from 'next-intl'; + +import MinorReleasesTable from '#site/components/Releases/MinorReleasesTable'; +import ReleaseOverview from '#site/components/Releases/ReleaseOverview'; +import WithReleaseAlertBox from '#site/components/withReleaseAlertBox'; + +import type { NodeRelease } from '#site/types'; +import type { ComponentProps, FC } from 'react'; + +type ReleaseModalProps = ComponentProps & { + release: NodeRelease; +}; + +const ReleaseModal: FC = ({ release, ...props }) => { + const t = useTranslations(); + + const modalHeadingKey = release.codename + ? 'components.releaseModal.title' + : 'components.releaseModal.titleWithoutCodename'; + + const modalHeading = t(modalHeadingKey, { + version: release.major, + codename: release.codename ?? '', + }); + + return ( + + + + {modalHeading} + + + + +
{t('components.releaseModal.minorVersions')}
+ + +
+
+ ); +}; + +export default ReleaseModal; diff --git a/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.module.css b/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.module.css new file mode 100644 index 0000000000000..99e446cdb856d --- /dev/null +++ b/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.module.css @@ -0,0 +1,21 @@ +@reference "../../../../styles/index.css"; + +.item { + @apply flex + items-center + gap-2; + + h1 { + @apply text-sm + font-semibold; + } + + h2 { + @apply text-xs + font-normal; + } + + svg { + @apply size-4; + } +} diff --git a/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.tsx b/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.tsx new file mode 100644 index 0000000000000..36e5d44e092aa --- /dev/null +++ b/apps/site/components/Releases/ReleaseOverview/ReleaseOverviewItem/index.tsx @@ -0,0 +1,31 @@ +import classNames from 'classnames'; + +import type { FC, ReactNode, SVGProps } from 'react'; + +import styles from './index.module.css'; + +type ReleaseOverviewItemProps = { + Icon: FC>; + title: ReactNode; + subtitle?: ReactNode; + className?: string; +}; + +const ReleaseOverviewItem: FC = ({ + Icon, + title, + subtitle, + className, +}) => { + return ( +
+ +
+ {subtitle &&

{subtitle}

} +

{title}

+
+
+ ); +}; + +export default ReleaseOverviewItem; diff --git a/apps/site/components/Releases/ReleaseOverview/index.module.css b/apps/site/components/Releases/ReleaseOverview/index.module.css new file mode 100644 index 0000000000000..7ad4ef58f3f47 --- /dev/null +++ b/apps/site/components/Releases/ReleaseOverview/index.module.css @@ -0,0 +1,18 @@ +@reference "../../../styles/index.css"; + +.root { + @apply rounded + border + border-neutral-200 + p-4 + text-neutral-900 + dark:border-neutral-800 + dark:text-white; + + .container { + @apply grid + grid-cols-2 + gap-4 + lg:grid-cols-3; + } +} diff --git a/apps/site/components/Releases/ReleaseOverview/index.tsx b/apps/site/components/Releases/ReleaseOverview/index.tsx new file mode 100644 index 0000000000000..481314102ead3 --- /dev/null +++ b/apps/site/components/Releases/ReleaseOverview/index.tsx @@ -0,0 +1,73 @@ +import { + CalendarIcon, + ClockIcon, + CodeBracketSquareIcon, + Square3Stack3DIcon, +} from '@heroicons/react/24/outline'; +import NpmIcon from '@node-core/ui-components/Icons/PackageManager/Npm'; +import { useTranslations } from 'next-intl'; + +import FormattedTime from '#site/components/Common/FormattedTime'; + +import type { NodeRelease } from '#site/types'; +import type { FC } from 'react'; + +import ReleaseOverviewItem from './ReleaseOverviewItem'; + +import styles from './index.module.css'; + +type ReleaseOverviewProps = { + release: NodeRelease; +}; + +const ReleaseOverview: FC = ({ release }) => { + const t = useTranslations(); + + return ( +
+
+ } + subtitle={t('components.releaseOverview.firstReleased')} + /> + + } + subtitle={t('components.releaseOverview.lastUpdated')} + /> + + + + {release.modules && ( + + )} + + {release.npm && ( + + )} + + +
+
+ ); +}; + +export default ReleaseOverview; diff --git a/apps/site/components/withAvatarGroup.tsx b/apps/site/components/withAvatarGroup.tsx new file mode 100644 index 0000000000000..f9c3491dd8c92 --- /dev/null +++ b/apps/site/components/withAvatarGroup.tsx @@ -0,0 +1,34 @@ +'use client'; + +import AvatarGroup from '@node-core/ui-components/Common/AvatarGroup'; + +import Link from '#site/components/Link'; +import { getAuthors } from '#site/util/author'; + +import type { AuthorProps } from '#site/types'; +import type { ComponentProps, FC } from 'react'; + +type WithAvatarGroupProps = Omit< + ComponentProps, + 'avatars' | 'as' +> & + AuthorProps; + +const WithAvatarGroup: FC = ({ + usernames, + names, + clickable = true, + ...props +}) => ( + +); + +export default WithAvatarGroup; diff --git a/apps/site/components/withBadgeGroup.tsx b/apps/site/components/withBadgeGroup.tsx new file mode 100644 index 0000000000000..be4263437700c --- /dev/null +++ b/apps/site/components/withBadgeGroup.tsx @@ -0,0 +1,31 @@ +import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; + +import Link from '#site/components/Link'; +import { siteConfig } from '#site/next.json.mjs'; +import { dateIsBetween } from '#site/util/date'; + +import type { FC } from 'react'; + +const WithBadgeGroup: FC<{ section: string }> = ({ section }) => { + const badge = siteConfig.websiteBadges[section]; + + if (badge && dateIsBetween(badge.startDate, badge.endDate)) { + return ( + + {badge.text} + + ); + } + + return null; +}; + +export default WithBadgeGroup; diff --git a/apps/site/components/withBanner.tsx b/apps/site/components/withBanner.tsx new file mode 100644 index 0000000000000..b8a186d01b23a --- /dev/null +++ b/apps/site/components/withBanner.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { ArrowUpRightIcon } from '@heroicons/react/24/outline'; +import Banner from '@node-core/ui-components/Common/Banner'; +import { useTranslations } from 'next-intl'; +import { useEffect, useState } from 'react'; + +import Link from '#site/components/Link'; +import { siteConfig } from '#site/next.json.mjs'; +import { dateIsBetween } from '#site/util/date'; + +import type { FC } from 'react'; + +const STORAGE_KEY = 'banner-dismissal'; + +const WithBanner: FC<{ section: string }> = ({ section }) => { + const banner = siteConfig.websiteBanners[section]; + const t = useTranslations(); + + const [open, setOpen] = useState(false); + + useEffect(() => { + if (banner) { + // eslint-disable-next-line @eslint-react/set-state-in-effect + setOpen(localStorage.getItem(STORAGE_KEY) !== banner.text); + } + }, [banner]); + + if (banner && open && dateIsBetween(banner.startDate, banner.endDate)) { + const bannerType = banner.type || 'default'; + + const onClose = () => { + localStorage.setItem(STORAGE_KEY, banner.text); + setOpen(false); + }; + + return ( + + {banner.link ? ( + {banner.text} + ) : ( + banner.text + )} + {banner.link && } + + ); + } + + return null; +}; + +export default WithBanner; diff --git a/apps/site/components/withBlogCategories.tsx b/apps/site/components/withBlogCategories.tsx new file mode 100644 index 0000000000000..e4b58cebf793c --- /dev/null +++ b/apps/site/components/withBlogCategories.tsx @@ -0,0 +1,61 @@ +import { useTranslations } from 'next-intl'; + +import BlogPostCard from '#site/components/Blog/BlogPostCard'; +import LinkTabs from '#site/components/Common/LinkTabs'; +import Pagination from '#site/components/Common/Pagination'; +import { mapAuthorToCardAuthors } from '#site/util/author'; + +import type { BlogPostsRSC } from '#site/types'; +import type { ComponentProps, FC } from 'react'; + +type WithBlogCategoriesProps = { + categories: ComponentProps['tabs']; + blogData: BlogPostsRSC & { category: string; page: number }; +}; + +const mapPaginationPages = (category: string, pages: number) => + [...Array(pages).keys()].map(page => ({ + url: `/blog/${category}/page/${page + 1}`, + })); + +const WithBlogCategories: FC = ({ + categories, + blogData, +}) => { + const t = useTranslations(); + + return ( + <> + +
+ {blogData.posts.map(post => ( + + ))} +
+
+ +
+ +
+ + ); +}; + +export default WithBlogCategories; diff --git a/apps/site/components/withBlogCrossLinks.tsx b/apps/site/components/withBlogCrossLinks.tsx new file mode 100644 index 0000000000000..468100521dddc --- /dev/null +++ b/apps/site/components/withBlogCrossLinks.tsx @@ -0,0 +1,51 @@ +import { getClientContext } from '#site/client-context'; +import CrossLink from '#site/components/Common/CrossLink'; +import { getBlogData } from '#site/util/blog'; + +import type { BlogCategory } from '#site/types'; +import type { FC } from 'react'; + +const WithBlogCrossLinks: FC = () => { + const { pathname } = getClientContext(); + + // Extracts from the static URL the components used for the Blog Post slug + const [, , category, postname] = pathname.split('/') as [ + unknown, + unknown, + BlogCategory, + string, + ]; + + const { posts } = getBlogData(category); + + const currentItem = posts.findIndex( + ({ slug }) => slug === `/blog/${category}/${postname}` + ); + + const [previousCrossLink, nextCrossLink] = [ + posts[currentItem - 1], + posts[currentItem + 1], + ]; + + return ( +
+ {(previousCrossLink && ( + + )) ||
} + + {nextCrossLink && ( + + )} +
+ ); +}; + +export default WithBlogCrossLinks; diff --git a/apps/site/components/withBreadcrumbs.tsx b/apps/site/components/withBreadcrumbs.tsx new file mode 100644 index 0000000000000..753387ad35abd --- /dev/null +++ b/apps/site/components/withBreadcrumbs.tsx @@ -0,0 +1,76 @@ +'use client'; + +import Breadcrumbs from '@node-core/ui-components/Common/Breadcrumbs'; +import { useTranslations } from 'next-intl'; + +import Link from '#site/components/Link'; +import useClientContext from '#site/hooks/useClientContext'; +import useMediaQuery from '#site/hooks/useMediaQuery'; +import useSiteNavigation from '#site/hooks/useSiteNavigation'; +import { dashToCamelCase } from '#site/util/string'; + +import type { NavigationKeys } from '#site/types'; +import type { BreadcrumbLink } from '@node-core/ui-components/Common/Breadcrumbs'; +import type { FC } from 'react'; + +type WithBreadcrumbsProps = { + navKeys?: Array; +}; + +const WithBreadcrumbs: FC = ({ navKeys = [] }) => { + const { getSideNavigation } = useSiteNavigation(); + const t = useTranslations(); + const { pathname } = useClientContext(); + const isMobileScreen = useMediaQuery('(max-width: 639px)'); + + const maxLength = isMobileScreen ? 2 : 4; + + const getBreadcrumbs = () => { + const navigationTree = getSideNavigation(navKeys); + + const pathList = pathname + .split('/') + .filter(item => item !== '') + .map(dashToCamelCase); + + let currentNode = navigationTree; + + // Reduce the pathList to a breadcrumbs array by finding each path in the current navigation layer, + // updating the currentNode to the found node's items(next layer) for the next iteration. + return pathList.reduce((breadcrumbs, path, index) => { + const nodeWithCurrentPath = currentNode.find( + ([nodePath, entry]) => + // Checking link in cases where nodePath cannot = path. Like 'discoverJavaScriptTimers' + (nodePath === path || entry.link === pathname) && + // Skip checking child path if it is the last path since there is no more child item inside + (index === pathList.length - 1 || + entry.items.some( + ([childPath, entry]) => + childPath === pathList[index + 1] || entry.link === pathname + )) + ); + + if (nodeWithCurrentPath) { + const [, { label, link = '', items = [] }] = nodeWithCurrentPath; + + // Goes deeper on the tree of items if there are any. + currentNode = items; + + return label ? [...breadcrumbs, { label, href: link }] : breadcrumbs; + } + + return breadcrumbs; + }, [] as Array); + }; + + return ( + + ); +}; + +export default WithBreadcrumbs; diff --git a/apps/site/components/withDownloadArchive.tsx b/apps/site/components/withDownloadArchive.tsx new file mode 100644 index 0000000000000..ec9f47b37fa95 --- /dev/null +++ b/apps/site/components/withDownloadArchive.tsx @@ -0,0 +1,46 @@ +import { notFound } from 'next/navigation'; + +import { getClientContext } from '#site/client-context'; +import provideReleaseData from '#site/next-data/providers/releaseData'; +import { + buildReleaseArtifacts, + extractVersionFromPath, +} from '#site/util/download/archive'; + +import type { FC } from 'react'; + +type DownloadArchive = ReturnType; + +type WithDownloadArchiveProps = { + children: FC; +}; + +/** + * Higher-order component that extracts version from pathname, + * fetches release data, and provides download artifacts to child component + */ +const WithDownloadArchive: FC = async ({ + children: Component, +}) => { + const { pathname } = getClientContext(); + + // Extract version from pathname + const version = extractVersionFromPath(pathname); + + // Find the release data for the given version + const releaseData = await provideReleaseData(); + const release = releaseData.find(release => + // Match major version only (e.g., v22.x.x for release.major v22) + version.startsWith(`v${release.major}`) + )!; + + if (!release) { + return notFound(); + } + + const releaseArtifacts = buildReleaseArtifacts(release, version); + + return ; +}; + +export default WithDownloadArchive; diff --git a/apps/site/components/withDownloadSection.tsx b/apps/site/components/withDownloadSection.tsx new file mode 100644 index 0000000000000..373eebaafb290 --- /dev/null +++ b/apps/site/components/withDownloadSection.tsx @@ -0,0 +1,55 @@ +import { defaultLocale } from '@node-core/website-i18n'; +import { getLocale } from 'next-intl/server'; + +import { getClientContext } from '#site/client-context'; +import WithNodeRelease from '#site/components/withNodeRelease'; +import provideDownloadSnippets from '#site/next-data/providers/downloadSnippets'; +import { + ReleaseProvider, + ReleasesProvider, +} from '#site/providers/releaseProvider'; + +import type { NodeRelease } from '../types'; +import type { FC, PropsWithChildren } from 'react'; + +type WithDownloadSectionProps = PropsWithChildren<{ + releases: Array; +}>; + +const WithDownloadSection: FC = async ({ + releases, + children, +}) => { + const locale = await getLocale(); + + const snippets = await provideDownloadSnippets(); + + const localeSnippets = snippets.get(locale) ?? []; + + // By default the translated languages do not contain all the download snippets + // Hence we always merge any translated snippet with the fallbacks for missing snippets + const fallbackSnippets = snippets.get(defaultLocale.code) ?? []; + + const { pathname } = getClientContext(); + + // Some available translations do not have download snippets translated or have them partially translated + // This aims to merge the available translated snippets with the fallback snippets + const memoizedSnippets = fallbackSnippets + .filter(snippet => !localeSnippets.some(s => s.name === snippet.name)) + .concat(localeSnippets); + + // Decides which initial release to use based on the current pathname + const initialRelease = pathname.endsWith('/current') ? 'Current' : 'LTS'; + + return ( + + {({ release }) => ( + + {children} + + )} + + ); +}; + +export default WithDownloadSection; diff --git a/apps/site/components/withFooter.tsx b/apps/site/components/withFooter.tsx new file mode 100644 index 0000000000000..148aa7e65e08f --- /dev/null +++ b/apps/site/components/withFooter.tsx @@ -0,0 +1,69 @@ +import BadgeGroup from '@node-core/ui-components/Common/BadgeGroup'; +import Footer from '@node-core/ui-components/Containers/Footer'; +import { useTranslations } from 'next-intl'; + +import { getClientContext } from '#site/client-context'; +import Link from '#site/components/Link'; +import { siteNavigation } from '#site/next.json.mjs'; + +import type { FC } from 'react'; + +import WithLegal from './withLegal'; +import WithNodeRelease from './withNodeRelease'; + +const WithFooter: FC = () => { + const t = useTranslations(); + const { pathname } = getClientContext(); + + const { socialLinks, footerLinks } = siteNavigation; + + const navigation = { + socialLinks, + footerLinks: footerLinks.map(link => ({ + ...link, + translation: t(link.text), + })), + }; + + const primary = ( +
+ + {({ release }) => ( + + {t('components.containers.footer.releasePills.latestLTS')} + + )} + + + + {({ release }) => ( + + {t('components.containers.footer.releasePills.latestRelease')} + + )} + +
+ ); + + const legal = ; + + return ( +