diff --git a/.devops/templates/variables.yml b/.devops/templates/variables.yml index a72a48c528ab8..31739629ffdaf 100644 --- a/.devops/templates/variables.yml +++ b/.devops/templates/variables.yml @@ -17,7 +17,6 @@ variables: BROWSERSLIST_IGNORE_OLD_DATA: true NX_PARALLEL: 8 - NX_PREFER_TS_NODE: true NX_VERBOSE_LOGGING: true # Also accessed as process.env.DEPLOYHOST diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 4036f2094b3c5..dcce4d07fdf32 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -409,7 +409,7 @@ packages/react-experiments/src/components/TileList @ThomasMichon **/tsconfig.json @microsoft/fluentui-react-build **/tsconfig.lib.json @microsoft/fluentui-react-build **/tsconfig.spec.json @microsoft/fluentui-react-build -**/cypress.config.ts @microsoft/fluentui-react-build +**/cypress.config.js @microsoft/fluentui-react-build **/api-extractor.json @microsoft/fluentui-react-build **/api-extractor.unstable.json @microsoft/fluentui-react-build **/.swcrc @microsoft/fluentui-react-build diff --git a/.github/workflows/bundle-size-base.yml b/.github/workflows/bundle-size-base.yml index b0f281c506696..86b5987a933a8 100644 --- a/.github/workflows/bundle-size-base.yml +++ b/.github/workflows/bundle-size-base.yml @@ -11,7 +11,6 @@ concurrency: env: NX_PARALLEL: 4 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true NX_VERBOSE_LOGGING: true BROWSERSLIST_IGNORE_OLD_DATA: true diff --git a/.github/workflows/bundle-size-comment.yml b/.github/workflows/bundle-size-comment.yml index 9efffc8fb4b7c..676d7228a1354 100644 --- a/.github/workflows/bundle-size-comment.yml +++ b/.github/workflows/bundle-size-comment.yml @@ -8,9 +8,14 @@ on: jobs: comment: runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'microsoft' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ (github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev') && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} permissions: pull-requests: write + # `actions/checkout` + `actions/download-artifact` (cross run) need these explicitly, since + # declaring a `permissions` block sets every unlisted scope to `none` + contents: read + actions: read steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/bundle-size.yml b/.github/workflows/bundle-size.yml index 186b59657f5e8..13ab2617dd36d 100644 --- a/.github/workflows/bundle-size.yml +++ b/.github/workflows/bundle-size.yml @@ -8,16 +8,18 @@ concurrency: cancel-in-progress: true env: - NX_PARALLEL: 6 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true + # Keep the upstream larger-runner tuning; leave one core free on the fork's Ubuntu runner. + NX_PARALLEL: ${{ github.repository_owner == 'mainframev' && '3' || '6' }} NX_VERBOSE_LOGGING: true BROWSERSLIST_IGNORE_OLD_DATA: true jobs: bundle-size: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} + timeout-minutes: 180 permissions: contents: 'read' actions: 'read' @@ -52,9 +54,33 @@ jobs: - name: Compare bundle size with base if: ${{ github.event.pull_request.base.ref == 'master' }} - run: npx monosize compare-reports --branch=${{ github.event.pull_request.base.ref }} --output=markdown --quiet > ./monosize-report.md env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # passed via env (not inlined into the script) so nothing from the event payload is + # evaluated by the shell + BASE_REF: ${{ github.event.pull_request.base.ref }} + REPOSITORY: ${{ github.repository }} + # base reports are read from the `microsoft/fluentui` "Bundle size Base" artifacts + # (see `storage` in monosize.config.mjs). A fork's GITHUB_TOKEN cannot read those, so a + # missing/unreachable baseline must not fail PR validation there - upstream keeps failing hard. + ALLOW_MISSING_BASE_REPORT: ${{ github.repository != 'microsoft/fluentui' }} + run: | + if npx monosize compare-reports --branch="$BASE_REF" --output=markdown --quiet > ./monosize-report.md; then + exit 0 + fi + + if [ "$ALLOW_MISSING_BASE_REPORT" != 'true' ]; then + echo "::error::'monosize compare-reports' failed." + exit 1 + fi + + echo "::warning::Bundle size comparison unavailable - the base report published by 'microsoft/fluentui' is not readable from '$REPOSITORY'. The bundle-size builds themselves passed." + { + printf '### 📦 Bundle size\n\n' + printf 'Bundle size **builds passed**, but no comparison could be produced.\n\n' + printf 'Baseline reports are published by the `Bundle size Base` workflow of ' + printf '`microsoft/fluentui` and are not readable from `%s`.\n' "$REPOSITORY" + } > ./monosize-report.md - name: Save PR number if: ${{ github.event.pull_request.base.ref == 'master' }} diff --git a/.github/workflows/check-packages.yml b/.github/workflows/check-packages.yml index 2736994f3b41a..5c8d61810406a 100644 --- a/.github/workflows/check-packages.yml +++ b/.github/workflows/check-packages.yml @@ -5,7 +5,8 @@ on: jobs: dependency-deduplication: runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'microsoft' }} + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} steps: - uses: actions/checkout@v6 with: @@ -38,7 +39,8 @@ jobs: dependency-mismatches: runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'microsoft' }} + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} steps: - uses: actions/checkout@v6 with: @@ -65,7 +67,8 @@ jobs: change-files: runs-on: ubuntu-latest - if: ${{ github.repository_owner == 'microsoft' }} + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} steps: - uses: actions/checkout@v6 with: diff --git a/.github/workflows/check-tooling.yml b/.github/workflows/check-tooling.yml index b7f0830b0897c..845e46758c8aa 100644 --- a/.github/workflows/check-tooling.yml +++ b/.github/workflows/check-tooling.yml @@ -14,11 +14,14 @@ env: jobs: check-tools: - if: ${{ github.repository_owner == 'microsoft' }} + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} strategy: matrix: + # both are GitHub hosted runners, no change needed for fork validation os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 60 steps: - uses: actions/checkout@v6 with: @@ -39,12 +42,16 @@ jobs: # pre-build react-jsx-runtime - as generate-api executor doesn't provide copy assets capability - run: yarn nx run react-jsx-runtime:build - - run: yarn nx g @fluentui/workspace-plugin:react-library --name hello-world --owner '@microsoft/fluentui-react-build' --kind standard --no-interactive + # The workflow validates generated files and follow-on generators after installing above. + # Avoid a redundant native dependency rebuild on the smaller Ubuntu runner. + - run: yarn nx g @fluentui/workspace-plugin:react-library --name hello-world --owner '@microsoft/fluentui-react-build' --kind standard --skip-install=${{ github.repository_owner == 'mainframev' }} --no-interactive - run: yarn nx g @fluentui/workspace-plugin:react-component --project hello-world-preview --name Aiur --no-interactive - run: yarn nx g @fluentui/workspace-plugin:cypress-component-configuration --project hello-world-preview --no-interactive - run: yarn nx g @fluentui/workspace-plugin:bundle-size-configuration --project hello-world-preview --no-interactive - run: yarn nx g @fluentui/workspace-plugin:prepare-initial-release --project hello-world-preview --phase=preview --no-interactive - - run: yarn nx g @fluentui/workspace-plugin:prepare-initial-release --project hello-world-preview --phase=stable --no-interactive + # API generation resolves the newly stable package through its workspace link, which is created + # by the install skipped on the fork. Upstream retains the complete post-generation validation. + - run: yarn nx g @fluentui/workspace-plugin:prepare-initial-release --project hello-world-preview --phase=stable --skip-install=${{ github.repository_owner == 'mainframev' }} --skip-generate-api=${{ github.repository_owner == 'mainframev' }} --no-interactive - run: yarn nx g @nx/workspace:remove --project hello-world --forceRemove --no-interactive - run: yarn nx g @nx/workspace:remove --project hello-world-stories --forceRemove --no-interactive - run: yarn nx g @fluentui/workspace-plugin:tsconfig-base-all --no-interactive diff --git a/.github/workflows/pr-vrt-comment.yml b/.github/workflows/pr-vrt-comment.yml index 4198336a10250..bb795a67cf0d3 100644 --- a/.github/workflows/pr-vrt-comment.yml +++ b/.github/workflows/pr-vrt-comment.yml @@ -6,13 +6,17 @@ on: - completed env: - NX_PARALLEL: 4 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true + # this workflow runs on `ubuntu-latest` = 4-core CPU / 16 GB of RAM + NX_PARALLEL: 4 NX_VERBOSE_LOGGING: true jobs: run_vr_diff: runs-on: ubuntu-latest + # NOTE: intentionally upstream only - the VR approval CLI authenticates against Azure via OIDC + # (`AZURE_VRT_CLIENT_ID`/`AZURE_TENANT_ID`/`AZURE_SUBSCRIPTION_ID`) and writes to the shared + # baseline storage. Forks do not have those secrets, so this stays skipped there instead of + # failing PR validation - screenshot *generation* is still validated by `pr-vrt.yml`. if: ${{ github.repository_owner == 'microsoft' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} permissions: # necessary to write comments to the PR from the vr-approval-cli @@ -28,7 +32,7 @@ jobs: # downloaded artifacts will contain screenshots from affected project including 'screenshots-report.json' which contains proper image mappings for affected project # - see @{link file://./../scripts/prepare-vr-screenshots-for-upload.js#45} - # - see @{link file://./pr-vrt.yml#56} + # - see @{link file://./pr-vrt.yml#80} - uses: actions/download-artifact@v7 with: diff --git a/.github/workflows/pr-vrt.yml b/.github/workflows/pr-vrt.yml index 4d7a717b33d31..6ccc52e87f198 100644 --- a/.github/workflows/pr-vrt.yml +++ b/.github/workflows/pr-vrt.yml @@ -11,8 +11,8 @@ concurrency: cancel-in-progress: true env: - NX_PARALLEL: 6 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true + # Keep the upstream larger-runner tuning; limit memory-heavy Storybook/Playwright work on the fork. + NX_PARALLEL: ${{ github.repository_owner == 'mainframev' && '2' || '6' }} NX_VERBOSE_LOGGING: true permissions: @@ -21,10 +21,12 @@ permissions: jobs: generate_vrt_screenshots: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + # The fork validates screenshot generation on Ubuntu. Upstream stays on macOS to match its baseline. + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} name: Generate screenshots - timeout-minutes: 60 + timeout-minutes: 120 steps: - uses: actions/checkout@v6 with: @@ -40,6 +42,14 @@ jobs: cache: 'yarn' node-version: '22' + # storybook builds + playwright browsers do not fit next to the preinstalled SDKs of the image + - name: Free up runner disk space + if: ${{ github.repository_owner == 'mainframev' }} + run: | + df -h / + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL || true + df -h / + - run: yarn install --frozen-lockfile - run: yarn playwright install --with-deps diff --git a/.github/workflows/pr-website-deploy-comment.yml b/.github/workflows/pr-website-deploy-comment.yml index 42f808fe0dcd9..481c95cfeae36 100644 --- a/.github/workflows/pr-website-deploy-comment.yml +++ b/.github/workflows/pr-website-deploy-comment.yml @@ -17,6 +17,11 @@ env: jobs: deploy: runs-on: ubuntu-latest + # NOTE: intentionally upstream only - uploading to the `fluentuipr` storage account requires the + # Azure OIDC credentials (`AZURE_CLIENT_ID`/`AZURE_TENANT_ID`/`AZURE_SUBSCRIPTION_ID`), which do + # not exist in forks. Keeping it skipped there (the dependent `comment` job is skipped with it) + # means PR validation is not failed by a missing deployment target - the website artifact itself + # is still built and validated by `pr-website-deploy.yml`. if: ${{ github.repository_owner == 'microsoft' && github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }} outputs: pr_number: ${{ steps.pr_number.outputs.result }} diff --git a/.github/workflows/pr-website-deploy.yml b/.github/workflows/pr-website-deploy.yml index 2b7c54f0d7fab..609d9612980b9 100644 --- a/.github/workflows/pr-website-deploy.yml +++ b/.github/workflows/pr-website-deploy.yml @@ -8,16 +8,20 @@ concurrency: cancel-in-progress: true env: - NX_PARALLEL: 6 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true + # Keep the upstream larger-runner tuning; limit memory-heavy webpack work on the fork. + NX_PARALLEL: ${{ github.repository_owner == 'mainframev' && '2' || '6' }} NX_VERBOSE_LOGGING: true BROWSERSLIST_IGNORE_OLD_DATA: true jobs: bundle: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream. + # NOTE: this job only *builds* the PR website artifact - uploading it to Azure happens in + # `pr-website-deploy-comment.yml`, which stays upstream only (needs Azure credentials). + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} + timeout-minutes: 240 permissions: contents: 'read' actions: 'read' @@ -41,6 +45,14 @@ jobs: - name: NodeJS heap default size run: node -e 'console.log(v8.getHeapStatistics().heap_size_limit / 1024 / 1024 + " MB");' + # bundles + storybook builds of the whole workspace do not fit next to the preinstalled SDKs + - name: Free up runner disk space + if: ${{ github.repository_owner == 'mainframev' }} + run: | + df -h / + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL || true + df -h / + - run: yarn install --frozen-lockfile - name: Install Playwright Browsers diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index bfe29d543e43f..4bb6aba82ded9 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -11,10 +11,13 @@ concurrency: cancel-in-progress: true env: - NX_PARALLEL: 6 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true + # Keep the upstream larger-runner tuning; use one Nx task per core on the fork's Ubuntu runner. + NX_PARALLEL: ${{ github.repository_owner == 'mainframev' && '4' || '6' }} NX_VERBOSE_LOGGING: true + # Nx already runs tasks concurrently, so use one Jest worker per task on the 4-core fork runner. + FLUENT_JEST_WORKER: ${{ github.repository_owner == 'mainframev' && '1' || '2' }} + BROWSERSLIST_IGNORE_OLD_DATA: true DEPLOY_HOST: fluentuipr.z22.web.core.windows.net @@ -23,8 +26,10 @@ env: jobs: main: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} + timeout-minutes: 300 permissions: contents: 'read' actions: 'read' @@ -74,16 +79,62 @@ jobs: - name: build, test, lint, test-ssr (affected) run: | - FLUENT_JEST_WORKER=2 yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging --nxBail + yarn nx affected -t build test lint type-check test-ssr test-integration verify-packaging --nxBail - name: 'Check for unstaged changes' run: | git status --porcelain git diff-index --quiet HEAD -- || exit 1 + type-check-benchmark: + name: TypeScript benchmark + if: ${{ github.repository_owner == 'mainframev' }} + runs-on: ubuntu-latest + timeout-minutes: 180 + env: + NX_PARALLEL: 2 + NX_VERBOSE_LOGGING: true + + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + cache: yarn + node-version: 22 + + - run: yarn install --frozen-lockfile + + - name: Report TypeScript version + run: yarn tsc --version + + - name: Run uncached workspace type-check + shell: bash + run: | + started_at=$(date +%s) + set +e + yarn nx run-many -t type-check --all --parallel="$NX_PARALLEL" --skip-nx-cache + status=$? + set -e + duration_seconds=$(($(date +%s) - started_at)) + + { + echo "## TypeScript type-check benchmark" + echo + echo "- Duration: ${duration_seconds} seconds" + echo "- Parallelism: ${NX_PARALLEL}" + echo "- Nx cache: disabled" + } >> "$GITHUB_STEP_SUMMARY" + + exit "$status" + react-compiler-analyzer: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} + timeout-minutes: 120 permissions: contents: 'read' actions: 'read' @@ -110,8 +161,15 @@ jobs: yarn nx affected -t react-compiler-analyzer--lint --nxBail react-major-versions-integration: - if: ${{ github.repository_owner == 'microsoft' }} - runs-on: macos-14-xlarge + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + runs-on: ${{ github.repository_owner == 'mainframev' && 'ubuntu-latest' || 'macos-14-xlarge' }} + timeout-minutes: 300 + permissions: + contents: 'read' + # required by `nrwl/nx-set-shas` to look up the last successful workflow run + actions: 'read' + steps: - uses: actions/checkout@v6 with: @@ -129,6 +187,15 @@ jobs: - run: echo number of CPUs "$(getconf _NPROCESSORS_ONLN)" + # this job installs 2 additional React workspaces (+ 2 Cypress binaries) next to the + # workspace `node_modules`, which does not fit next to the preinstalled SDKs of the image + - name: Free up runner disk space + if: ${{ github.repository_owner == 'mainframev' }} + run: | + df -h / + sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup /opt/hostedtoolcache/CodeQL || true + df -h / + - run: | yarn install --frozen-lockfile yarn rit --react 17 --install-deps @@ -156,15 +223,18 @@ jobs: - name: React Versions Integration Tests (17,18) - Type-check & Test env: NODE_OPTIONS: --max-old-space-size=4096 + # Each task gets a 4 GB heap, so limit the 16 GB fork runner while preserving upstream tuning. run: | - FLUENT_JEST_WORKER=2 yarn nx affected -t test-rit--17--type-check,test-rit--18--type-check,test-rit--17--test,test-rit--18--test --exclude='react-19-tests-v9' + yarn nx affected -t test-rit--17--type-check,test-rit--18--type-check,test-rit--17--test,test-rit--18--test --exclude='react-19-tests-v9' --parallel=${{ github.repository_owner == 'mainframev' && '2' || '6' }} e2e: - if: ${{ github.repository_owner == 'microsoft' }} - # TODO: switch to macos once problematic tests are fixed + # upstream repo + the fork used to validate large migrations before they are opened upstream + if: ${{ github.repository_owner == 'microsoft' || github.repository_owner == 'mainframev' }} + # NOTE: this job was already running on ubuntu # https://github.com/microsoft/fluentui/issues/33173 # https://github.com/microsoft/fluentui/issues/33172 runs-on: ubuntu-latest + timeout-minutes: 120 permissions: contents: 'read' actions: 'read' diff --git a/.github/workflows/vrt-baseline.yml b/.github/workflows/vrt-baseline.yml index 23ecdd85fb60c..3bbc64cc31e12 100644 --- a/.github/workflows/vrt-baseline.yml +++ b/.github/workflows/vrt-baseline.yml @@ -7,7 +7,6 @@ on: env: NX_PARALLEL: 6 # ubuntu-latest = 4-core CPU / 16 GB of RAM | macos-14-xlarge (arm) = 6-core CPU / 14 GB of RAM - NX_PREFER_TS_NODE: true NX_VERBOSE_LOGGING: true permissions: diff --git a/.gitignore b/.gitignore index bfea28a63f0c7..0331d2bea4a49 100644 --- a/.gitignore +++ b/.gitignore @@ -135,6 +135,9 @@ package-lock.json # Copied monaco-editor files /packages/monaco-editor/esm +# transient tsconfigs created by build/type-check tasks to opt out of TS path aliases +tsconfig.__generated-no-path-aliases-*.json + # tsdoc tsdoc-metadata.json diff --git a/AGENTS.md b/AGENTS.md index 5f280a17cdf02..7955c98143efa 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -68,11 +68,12 @@ state.root.className = mergeClasses( ## Architecture (deep dives) -| Topic | Location | -| --------------------------------------------- | ---------------------------------------------------------------------------------- | -| V9 component patterns (hooks, slots, Griffel) | [docs/architecture/component-patterns.md](docs/architecture/component-patterns.md) | -| Design tokens and theming | [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md) | -| Package dependency layers | [docs/architecture/layers.md](docs/architecture/layers.md) | +| Topic | Location | +| --------------------------------------------- | ------------------------------------------------------------------------------------------ | +| V9 component patterns (hooks, slots, Griffel) | [docs/architecture/component-patterns.md](docs/architecture/component-patterns.md) | +| Design tokens and theming | [docs/architecture/design-tokens.md](docs/architecture/design-tokens.md) | +| Package dependency layers | [docs/architecture/layers.md](docs/architecture/layers.md) | +| v8 published artifacts (ES5/AMD, interop) | [docs/architecture/v8-published-artifacts.md](docs/architecture/v8-published-artifacts.md) | ## Workflows diff --git a/apps/chart-docsite/tsconfig.json b/apps/chart-docsite/tsconfig.json index a85a689b30243..61ecb7408fd81 100644 --- a/apps/chart-docsite/tsconfig.json +++ b/apps/chart-docsite/tsconfig.json @@ -3,7 +3,6 @@ "compilerOptions": { "jsx": "react-jsx", "allowJs": false, - "esModuleInterop": false, "allowSyntheticDefaultImports": true, "strict": true }, diff --git a/apps/perf-test/tsconfig.app.json b/apps/perf-test/tsconfig.app.json index f7b76a07fffe5..c95538693bce4 100644 --- a/apps/perf-test/tsconfig.app.json +++ b/apps/perf-test/tsconfig.app.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "module": "commonjs", - "target": "ES5", + "target": "ES2015", "outDir": "lib", "jsx": "react", "lib": ["ES2015", "DOM"], diff --git a/apps/perf-test/tsconfig.json b/apps/perf-test/tsconfig.json index 0b5b0de619497..f93f07337a184 100644 --- a/apps/perf-test/tsconfig.json +++ b/apps/perf-test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.v8.json", "compilerOptions": { - "target": "es5", + "target": "es2015", "module": "commonjs", "experimentalDecorators": true, "preserveConstEnums": true diff --git a/apps/public-docsite-resources/package.json b/apps/public-docsite-resources/package.json index 362cb5c11f9a6..9629404cad53d 100644 --- a/apps/public-docsite-resources/package.json +++ b/apps/public-docsite-resources/package.json @@ -30,7 +30,6 @@ "license": "MIT", "scripts": { "build": "just-scripts build", - "prebundle": "yarn build", "bundle": "just-scripts bundle", "lint": "eslint --ext .js,.ts,.tsx ./src", "just": "just-scripts", diff --git a/apps/public-docsite-resources/project.json b/apps/public-docsite-resources/project.json index 3e2fd9316813c..c6fc9bf8fa719 100644 --- a/apps/public-docsite-resources/project.json +++ b/apps/public-docsite-resources/project.json @@ -3,5 +3,10 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "implicitDependencies": [], - "tags": ["v8"] + "tags": ["v8", "ships-es5"], + "targets": { + "bundle": { + "dependsOn": ["build", "^build"] + } + } } diff --git a/apps/public-docsite-resources/tsconfig.json b/apps/public-docsite-resources/tsconfig.json index 15242005deed6..80584c21f1828 100644 --- a/apps/public-docsite-resources/tsconfig.json +++ b/apps/public-docsite-resources/tsconfig.json @@ -1,9 +1,8 @@ { "extends": "../../tsconfig.base.v8.json", "compilerOptions": { - "baseUrl": ".", "outDir": "lib", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -11,7 +10,7 @@ "importHelpers": true, "noUnusedLocals": true, "preserveConstEnums": true, - "lib": ["es5", "dom", "es2015.promise"], + "lib": ["es2019", "dom"], "types": ["webpack-env", "custom-global"] }, "include": ["src"] diff --git a/apps/public-docsite-resources/tsconfig.webpack.json b/apps/public-docsite-resources/tsconfig.webpack.json new file mode 100644 index 0000000000000..cf994799aee5b --- /dev/null +++ b/apps/public-docsite-resources/tsconfig.webpack.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "module": "esnext", + "rootDir": "../..", + "types": ["webpack-env", "custom-global", "node"] + } +} diff --git a/apps/public-docsite-resources/webpack.serve.config.js b/apps/public-docsite-resources/webpack.serve.config.js index ecd8dce354cab..da0107b6f4b91 100644 --- a/apps/public-docsite-resources/webpack.serve.config.js +++ b/apps/public-docsite-resources/webpack.serve.config.js @@ -26,4 +26,5 @@ module.exports = resources.createServeConfig( { outDir }, ), outDirRelative, + { typescriptConfigFile: path.join(__dirname, 'tsconfig.webpack.json') }, ); diff --git a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts index f7bba7ae64ee5..32affd37d1268 100644 --- a/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts +++ b/apps/public-docsite-v9/src/Concepts/Migration/FromV0/Components/IconCatalog/useDebounce.ts @@ -1,10 +1,10 @@ import * as React from 'react'; -export const useDebounce = (fn: (...args: unknown[]) => void, duration: number) => { +export const useDebounce = (fn: (...args: TArgs) => void, duration: number) => { const timeoutRef = React.useRef(0); return React.useCallback( - (...args: unknown[]) => { + (...args: TArgs) => { // eslint-disable-next-line @nx/workspace-no-restricted-globals window.clearTimeout(timeoutRef.current); // eslint-disable-next-line @nx/workspace-no-restricted-globals diff --git a/apps/public-docsite/package.json b/apps/public-docsite/package.json index 279bc0e695ef6..46395aacd3e78 100644 --- a/apps/public-docsite/package.json +++ b/apps/public-docsite/package.json @@ -19,7 +19,7 @@ "clean": "just-scripts clean", "code-style": "just-scripts code-style", "start": "just-scripts dev", - "type-check": "tsc -p . --noEmit --baseUrl ." + "type-check": "node ../../scripts/tasks/bin/type-check-project.js" }, "license": "MIT", "devDependencies": { diff --git a/apps/public-docsite/project.json b/apps/public-docsite/project.json index 111d006669b1b..d8869579bf32c 100644 --- a/apps/public-docsite/project.json +++ b/apps/public-docsite/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "implicitDependencies": [], - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "targets": { "type-check": { "dependsOn": ["prebundle"] diff --git a/apps/public-docsite/src/pages/PageTemplates/TemplatePage/TemplatePage.tsx b/apps/public-docsite/src/pages/PageTemplates/TemplatePage/TemplatePage.tsx index f8580f42723e9..bf6777d8144fc 100644 --- a/apps/public-docsite/src/pages/PageTemplates/TemplatePage/TemplatePage.tsx +++ b/apps/public-docsite/src/pages/PageTemplates/TemplatePage/TemplatePage.tsx @@ -103,8 +103,8 @@ function _otherSections(platform: Platforms): IPageSectionProps[] { ), // Optionally wrap the section with a className. Use the `css` utility from Fluent UI to concatenate - // classNames that may be falsey. - className: css(styles.customSection, 'customGlobalClassName', platform === 'web' && 'falseyGlobalClassName'), + // classNames. + className: css(styles.customSection, 'customGlobalClassName'), }, ]; } diff --git a/apps/public-docsite/tsconfig.json b/apps/public-docsite/tsconfig.json index a602c87e693f2..ab46c3274c523 100644 --- a/apps/public-docsite/tsconfig.json +++ b/apps/public-docsite/tsconfig.json @@ -2,7 +2,7 @@ "extends": "../../tsconfig.base.v8.json", "compilerOptions": { "outDir": "lib", - "target": "es5", + "target": "es2015", // ensure webpack can split async imports into separate chunks "module": "esnext", "jsx": "react", diff --git a/apps/rit-tests-v8/cypress.config.js b/apps/rit-tests-v8/cypress.config.js new file mode 100644 index 0000000000000..d409a5b4c908c --- /dev/null +++ b/apps/rit-tests-v8/cypress.config.js @@ -0,0 +1,22 @@ +// @ts-check + +const path = require('node:path'); + +const { baseConfig, readWorkspacePathAliases } = require('@fluentui/scripts-cypress'); +const { registerTsPaths } = require('@fluentui/scripts-storybook'); + +const tsConfigPath = path.resolve(__dirname, '../../tsconfig.base.v8.json'); + +const config = { ...baseConfig }; + +registerTsPaths({ + config: config.component.devServer.webpackConfig, + configFile: tsConfigPath, + // explicit, because the aliases are `pathsBasePath` relative - see `@fluentui/scripts-cypress`'s + // `ts-paths.js`. Without it `TsconfigPathsPlugin` falls back to anchoring `paths` at the directory of + // `tsConfigPath` itself, which is only correct here as long as `tsconfig.base.v8.json` declares its + // `paths` directly rather than through an `extends` chain. + baseUrl: readWorkspacePathAliases(tsConfigPath).absoluteBaseUrl, +}); + +module.exports = config; diff --git a/apps/rit-tests-v8/cypress.config.ts b/apps/rit-tests-v8/cypress.config.ts deleted file mode 100644 index 204ea0334b4b8..0000000000000 --- a/apps/rit-tests-v8/cypress.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as path from 'node:path'; - -import { baseConfig } from '@fluentui/scripts-cypress'; -import { registerTsPaths } from '@fluentui/scripts-storybook'; - -const tsConfigPath = path.resolve(__dirname, '../../tsconfig.base.v8.json'); - -const config = { ...baseConfig }; - -registerTsPaths({ config: config.component.devServer.webpackConfig, configFile: tsConfigPath }); - -export default config; diff --git a/apps/ssr-tests/package.json b/apps/ssr-tests/package.json index 60f603150402e..b0ededcc645e2 100644 --- a/apps/ssr-tests/package.json +++ b/apps/ssr-tests/package.json @@ -4,7 +4,7 @@ "description": "Server-side rendering tests for @fluentui/react.", "private": true, "scripts": { - "type-check": "tsc -p . --noEmit --baseUrl .", + "type-check": "tsc -p . --noEmit", "bundle": "just-scripts bundle", "test-ssr": "just-scripts test" }, diff --git a/apps/ssr-tests/tsconfig.json b/apps/ssr-tests/tsconfig.json index ff1bc9bb938d3..d1757a0af3017 100644 --- a/apps/ssr-tests/tsconfig.json +++ b/apps/ssr-tests/tsconfig.json @@ -1,9 +1,8 @@ { "extends": "@tsconfig/node20/tsconfig.json", "compilerOptions": { - "target": "ES5", - "module": "commonjs", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "noEmit": true, "noUnusedLocals": true, "preserveConstEnums": true, diff --git a/apps/theming-designer/project.json b/apps/theming-designer/project.json index 876c7d22633b2..b6dc348be98d9 100644 --- a/apps/theming-designer/project.json +++ b/apps/theming-designer/project.json @@ -3,5 +3,5 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "application", "implicitDependencies": [], - "tags": ["v8"] + "tags": ["v8", "ships-es5"] } diff --git a/apps/theming-designer/tsconfig.json b/apps/theming-designer/tsconfig.json index 78c5173940ff7..c16dae86babcb 100644 --- a/apps/theming-designer/tsconfig.json +++ b/apps/theming-designer/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.v8.json", "compilerOptions": { - "target": "es5", + "target": "es2015", "outDir": "lib", "module": "commonjs", "jsx": "react", diff --git a/apps/ts-minbar-test-react-components/tsconfig.json b/apps/ts-minbar-test-react-components/tsconfig.json index 94b462a672152..1480cc99e96ab 100644 --- a/apps/ts-minbar-test-react-components/tsconfig.json +++ b/apps/ts-minbar-test-react-components/tsconfig.json @@ -1,10 +1,11 @@ { "compilerOptions": { "noEmit": true, + "rootDir": "./src", "lib": ["ES2019"], "target": "ES2019", - "module": "CommonJS", - "moduleResolution": "Node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/apps/ts-minbar-test-react/tsconfig.json b/apps/ts-minbar-test-react/tsconfig.json index 94b462a672152..1480cc99e96ab 100644 --- a/apps/ts-minbar-test-react/tsconfig.json +++ b/apps/ts-minbar-test-react/tsconfig.json @@ -1,10 +1,11 @@ { "compilerOptions": { "noEmit": true, + "rootDir": "./src", "lib": ["ES2019"], "target": "ES2019", - "module": "CommonJS", - "moduleResolution": "Node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "strict": true, "esModuleInterop": true, "skipLibCheck": true, diff --git a/apps/vr-tests-react-components/package.json b/apps/vr-tests-react-components/package.json index 947b718394ca5..de18523a513ed 100644 --- a/apps/vr-tests-react-components/package.json +++ b/apps/vr-tests-react-components/package.json @@ -10,7 +10,7 @@ "lint": "just-scripts lint", "start": "storybook dev", "test": "just-scripts test", - "type-check": "tsc -p . --noEmit --baseUrl .", + "type-check": "node ../../scripts/tasks/bin/type-check-project.js", "test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --bailOnStoriesError --stepsApi parameters" }, "dependencies": { diff --git a/apps/vr-tests-web-components/package.json b/apps/vr-tests-web-components/package.json index 2333cf739af3b..fb49017284433 100644 --- a/apps/vr-tests-web-components/package.json +++ b/apps/vr-tests-web-components/package.json @@ -9,7 +9,7 @@ "format": "prettier . -w --ignore-path ../../.prettierignore", "lint": "eslint src --ext .ts,.tsx", "start": "storybook dev", - "type-check": "tsc -p . --baseUrl . --noEmit", + "type-check": "node ../../scripts/tasks/bin/type-check-project.js", "test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --stepsApi parameters --bailOnStoriesError" }, "dependencies": { diff --git a/apps/vr-tests/package.json b/apps/vr-tests/package.json index 4eea7019bc059..9e4af1ed04827 100644 --- a/apps/vr-tests/package.json +++ b/apps/vr-tests/package.json @@ -10,7 +10,7 @@ "just": "just-scripts", "lint": "just-scripts lint", "start": "storybook dev -p 3000", - "type-check": "tsc -p . --noEmit --baseUrl .", + "type-check": "node ../../scripts/tasks/bin/type-check-project.js", "test-vr": "storywright --browsers chromium --url dist/storybook --destpath dist/screenshots --waitTimeScreenshot 500 --concurrency 4 --headless true --stepsApi parameters --bailOnStoriesError" }, "dependencies": { diff --git a/apps/vr-tests/tsconfig.json b/apps/vr-tests/tsconfig.json index e7d70fca355f3..7bd5d6e851c8a 100644 --- a/apps/vr-tests/tsconfig.json +++ b/apps/vr-tests/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.v8.json", "compilerOptions": { - "target": "es5", + "target": "es2015", "outDir": "lib", "module": "commonjs", "jsx": "react", diff --git a/change/@fluentui-chart-web-components-74d6f8b4-f789-4cee-a77b-40fb3da65339.json b/change/@fluentui-chart-web-components-74d6f8b4-f789-4cee-a77b-40fb3da65339.json new file mode 100644 index 0000000000000..382d263e74f75 --- /dev/null +++ b/change/@fluentui-chart-web-components-74d6f8b4-f789-4cee-a77b-40fb3da65339.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update TypeScript build scripts and configuration for TypeScript 6 compatibility", + "packageName": "@fluentui/chart-web-components", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@fluentui-codemods-eade6a9a-452a-4bac-b06f-ddeb54503afe.json b/change/@fluentui-codemods-eade6a9a-452a-4bac-b06f-ddeb54503afe.json new file mode 100644 index 0000000000000..f72d29e01c86f --- /dev/null +++ b/change/@fluentui-codemods-eade6a9a-452a-4bac-b06f-ddeb54503afe.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update TypeScript configuration for TypeScript 6 compatibility", + "packageName": "@fluentui/codemods", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/@fluentui-eslint-plugin-react-components-54d0b200-9d0c-4a78-9eb1-88375bb59771.json b/change/@fluentui-eslint-plugin-react-components-54d0b200-9d0c-4a78-9eb1-88375bb59771.json new file mode 100644 index 0000000000000..da36f3aea5b92 --- /dev/null +++ b/change/@fluentui-eslint-plugin-react-components-54d0b200-9d0c-4a78-9eb1-88375bb59771.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "bound the \"typescript\" peerDependency to \">=5.0.0 <7.0.0\": TypeScript 5.x and 6.x are verified against the published rule types, while the unbounded \">= 5.0.0\" range also advertised support for the unreleased TypeScript 7 compiler", + "packageName": "@fluentui/eslint-plugin-react-components", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-eslint-plugin-react-components-6e0a31fc-93ba-4ed0-b92e-7d68a70e958f.json b/change/@fluentui-eslint-plugin-react-components-6e0a31fc-93ba-4ed0-b92e-7d68a70e958f.json new file mode 100644 index 0000000000000..241224f78b81c --- /dev/null +++ b/change/@fluentui-eslint-plugin-react-components-6e0a31fc-93ba-4ed0-b92e-7d68a70e958f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "export the `RuleOptions` type from the package entry point and give the flat config `plugins` map an explicit index signature, so the generated type declarations stop referencing `../package.json` and unpublished module paths", + "packageName": "@fluentui/eslint-plugin-react-components", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-eslint-plugin-react-components-7959dc2c-ad05-45e8-98a4-34edb71330b6.json b/change/@fluentui-eslint-plugin-react-components-7959dc2c-ad05-45e8-98a4-34edb71330b6.json new file mode 100644 index 0000000000000..404560ec43d08 --- /dev/null +++ b/change/@fluentui-eslint-plugin-react-components-7959dc2c-ad05-45e8-98a4-34edb71330b6.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "chore: bump @typescript-eslint/utils to 8.64.0 for TypeScript 6.0 support", + "packageName": "@fluentui/eslint-plugin-react-components", + "email": "copilot@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-breadcrumb-ae9310f5-0e6c-43fc-9554-5048658edcb5.json b/change/@fluentui-react-breadcrumb-ae9310f5-0e6c-43fc-9554-5048658edcb5.json new file mode 100644 index 0000000000000..1f1689fef4eff --- /dev/null +++ b/change/@fluentui-react-breadcrumb-ae9310f5-0e6c-43fc-9554-5048658edcb5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "annotate `BreadcrumbProvider` with `React.Provider` so the published type declaration rollup no longer imports the unpublished `./Breadcrumb.types` module", + "packageName": "@fluentui/react-breadcrumb", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-calendar-compat-1dde0083-ef4d-4a50-b59c-ba2255d0bd0f.json b/change/@fluentui-react-calendar-compat-1dde0083-ef4d-4a50-b59c-ba2255d0bd0f.json new file mode 100644 index 0000000000000..639cec08d97f3 --- /dev/null +++ b/change/@fluentui-react-calendar-compat-1dde0083-ef4d-4a50-b59c-ba2255d0bd0f.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "use `RefAttributes` from `@fluentui/react-utilities` instead of `React.RefAttributes`, which leaks string refs into the public `CalendarProps` contract", + "packageName": "@fluentui/react-calendar-compat", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-charts-989a6829-9d97-4246-b910-665d8f4c43b5.json b/change/@fluentui-react-charts-989a6829-9d97-4246-b910-665d8f4c43b5.json new file mode 100644 index 0000000000000..f29e6d2bad4fb --- /dev/null +++ b/change/@fluentui-react-charts-989a6829-9d97-4246-b910-665d8f4c43b5.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "import `SlotClassNames`/`JSXElement` from the `@fluentui/react-utilities` package entry instead of its internal `/src/index` path, and use its `RefAttributes` type instead of `React.RefAttributes`, which leaks string refs", + "packageName": "@fluentui/react-charts", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-conformance-4ca98534-6b2f-40bc-b375-d237dc796e9d.json b/change/@fluentui-react-conformance-4ca98534-6b2f-40bc-b375-d237dc796e9d.json new file mode 100644 index 0000000000000..4ef01add832b0 --- /dev/null +++ b/change/@fluentui-react-conformance-4ca98534-6b2f-40bc-b375-d237dc796e9d.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "widen the \"typescript\" peerDependency to \">=4.3.0 <7.0.0\" so TypeScript 5.x and 6.x consumers satisfy the peer; the previous \"^4.3.0\" range excluded every TypeScript release after 4.9", + "packageName": "@fluentui/react-conformance", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-conformance-griffel-09437a3c-3d24-4cf6-b2c9-159b22e81e9b.json b/change/@fluentui-react-conformance-griffel-09437a3c-3d24-4cf6-b2c9-159b22e81e9b.json new file mode 100644 index 0000000000000..415505a876b77 --- /dev/null +++ b/change/@fluentui-react-conformance-griffel-09437a3c-3d24-4cf6-b2c9-159b22e81e9b.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "widen the \"typescript\" peerDependency to \">=4.3.0 <7.0.0\" so TypeScript 5.x and 6.x consumers satisfy the peer; the previous \"^4.3.0\" range excluded every TypeScript release after 4.9", + "packageName": "@fluentui/react-conformance-griffel", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-field-7e801ac4-697a-4256-8bbe-bce339679eaa.json b/change/@fluentui-react-field-7e801ac4-697a-4256-8bbe-bce339679eaa.json new file mode 100644 index 0000000000000..7a9c0e3ac30d8 --- /dev/null +++ b/change/@fluentui-react-field-7e801ac4-697a-4256-8bbe-bce339679eaa.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "annotate `FieldContextProvider` with `React.Provider` so the published type declaration rollup no longer imports an unpublished relative module", + "packageName": "@fluentui/react-field", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-headless-components-preview-888747fe-0f00-4823-9a04-022103184d80.json b/change/@fluentui-react-headless-components-preview-888747fe-0f00-4823-9a04-022103184d80.json new file mode 100644 index 0000000000000..cbaced4121a5f --- /dev/null +++ b/change/@fluentui-react-headless-components-preview-888747fe-0f00-4823-9a04-022103184d80.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "annotate `Provider` with its public forward-ref type, using `RefAttributes` from `@fluentui/react-utilities` to avoid leaking string refs and unpublished modules into the declaration rollup", + "packageName": "@fluentui/react-headless-components-preview", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-overflow-42c1dc7b-faa9-4fe9-ad25-adedb6bc11d7.json b/change/@fluentui-react-overflow-42c1dc7b-faa9-4fe9-ad25-adedb6bc11d7.json new file mode 100644 index 0000000000000..6b8c762d8da88 --- /dev/null +++ b/change/@fluentui-react-overflow-42c1dc7b-faa9-4fe9-ad25-adedb6bc11d7.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "annotate `Overflow` with its public forward-ref type, using `RefAttributes` from `@fluentui/react-utilities` to avoid leaking string refs and unpublished modules into the declaration rollup", + "packageName": "@fluentui/react-overflow", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-storybook-addon-export-to-sandbox-23272e6e-3138-4fe9-ad93-5cea45062d85.json b/change/@fluentui-react-storybook-addon-export-to-sandbox-23272e6e-3138-4fe9-ad93-5cea45062d85.json new file mode 100644 index 0000000000000..683660c57e79c --- /dev/null +++ b/change/@fluentui-react-storybook-addon-export-to-sandbox-23272e6e-3138-4fe9-ad93-5cea45062d85.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "bump the generated sandbox template's \"typescript\" devDependency from \"~4.7.0\" to \"~6.0.0\": the scaffolded tsconfig sets \"moduleResolution\": \"bundler\", which TypeScript 4.7 does not support", + "packageName": "@fluentui/react-storybook-addon-export-to-sandbox", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-react-tag-picker-ee9d1591-918a-495f-81c6-cd5d4ac5e0ee.json b/change/@fluentui-react-tag-picker-ee9d1591-918a-495f-81c6-cd5d4ac5e0ee.json new file mode 100644 index 0000000000000..f563515bb7c6b --- /dev/null +++ b/change/@fluentui-react-tag-picker-ee9d1591-918a-495f-81c6-cd5d4ac5e0ee.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "import `TagAppearance`/`TagSize` from the `@fluentui/react-tags` package entry instead of its internal `/src/index` path, which is not resolvable through the package exports map", + "packageName": "@fluentui/react-tag-picker", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} diff --git a/change/@fluentui-web-components-2ea77ecc-6b4f-4d41-b51d-df1b020472c3.json b/change/@fluentui-web-components-2ea77ecc-6b4f-4d41-b51d-df1b020472c3.json new file mode 100644 index 0000000000000..360ad68db6e58 --- /dev/null +++ b/change/@fluentui-web-components-2ea77ecc-6b4f-4d41-b51d-df1b020472c3.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "update TypeScript build scripts and configuration for TypeScript 6 compatibility", + "packageName": "@fluentui/web-components", + "email": "vgenaev@gmail.com", + "dependentChangeType": "patch" +} \ No newline at end of file diff --git a/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json b/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json deleted file mode 100644 index c6f73ed4930d9..0000000000000 --- a/change/fluentui-eslint-plugin-a417f93b-a140-4c40-a4e9-074fab196688.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "chore: enforce consistent type imports in react packages", - "packageName": "@fluentui/eslint-plugin", - "email": "copilot@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json b/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json deleted file mode 100644 index 6c9c934700c16..0000000000000 --- a/change/fluentui-theme-designer-2a88231d-0358-4d1a-bb01-9f602e9306ec.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "none", - "comment": "chore: enforce consistent type imports", - "packageName": "@fluentui/theme-designer", - "email": "copilot@microsoft.com", - "dependentChangeType": "none" -} diff --git a/docs/architecture/typescript-6-upstreaming.md b/docs/architecture/typescript-6-upstreaming.md new file mode 100644 index 0000000000000..273f15ba7643b --- /dev/null +++ b/docs/architecture/typescript-6-upstreaming.md @@ -0,0 +1,47 @@ +# Upstreaming this TypeScript 6 migration + +This branch doubles as the **fork validation vehicle**: it runs the fork's CI on GitHub-hosted +Ubuntu runners so the migration can be exercised end-to-end before an upstream +`microsoft/fluentui` PR exists. As a result the workflow files mix two kinds of change that must be +**separated** when the upstream PR is opened. + +## Keep (real TypeScript 6 fixes) + +These are genuine migration fixes and belong upstream unchanged: + +- **Remove `NX_PREFER_TS_NODE: true`** from all 8 workflow files and `.devops/templates/variables.yml`. + `NX_PREFER_TS_NODE` forces Nx onto its `ts-node` fallback, which hardcodes `moduleResolution: + node10` and fails under TypeScript 6 with `TS5107`. With it removed, Nx uses the + `@swc-node/register` path this branch installs. +- Any `@swc-node/register` install / setup steps added for the above. + +## Drop (fork-only plumbing) + +Everything gated on `github.repository_owner == 'mainframev'` is fork-only and must be reverted to +the upstream `microsoft`-owner behavior: + +| File | Fork-only change to drop | +| --------------------------------- | ------------------------------------------------------------------------- | +| `pr.yml` | Ubuntu runner routing, `NX_PARALLEL`/`FLUENT_JEST_WORKER` tuning, fork job enablement (12 conditionals) | +| `pr-vrt.yml` | Ubuntu routing + fork enablement (4) | +| `pr-website-deploy.yml` | Ubuntu routing + fork enablement (4) | +| `check-packages.yml` | Ubuntu routing (3) | +| `check-tooling.yml` | Ubuntu routing + skip git-hook setup (3) | +| `bundle-size.yml` | Ubuntu routing + inaccessible-baseline warning (3) | +| `bundle-size-comment.yml` | fork gate (1) | + +Restore each `runs-on` to `macos-14-xlarge` and each `if:` to `github.repository_owner == +'microsoft'`, and restore `NX_PARALLEL: 6` (macOS larger runner) where the fork lowered it to 4. + +## Also fork-specific + +- Azure-dependent VRT comparison and website deploy remain upstream-only (fork lacks credentials); + no upstream change needed there. +- The `TypeScript benchmark` workflow and pinned `benchmark/typescript-6-base` base exist only for + the fork's 5.7.3-vs-6.0.3 comparison and should not be part of the upstream PR. + +## Split the code diff, too + +For a reviewable upstream PR, split into: (a) compiler + tsconfig upgrade (the resolver split, +inline helpers, peer ranges), (b) the Cypress `.ts -> .js` conversion, (c) the dts-rollup / +api-extractor rework, (d) the source-contract fixes (entry-point imports, `RefAttributes`). diff --git a/docs/architecture/v8-published-artifacts.md b/docs/architecture/v8-published-artifacts.md new file mode 100644 index 0000000000000..70e14b363c71b --- /dev/null +++ b/docs/architecture/v8-published-artifacts.md @@ -0,0 +1,70 @@ +# v8 published artifacts (post TypeScript 6) + +TypeScript 6 removed `target: es5` and `module: amd`. v8 packages (`packages/react` & +friends) publish exactly those artifacts, so the way they are produced changed even though the +published contract did not. This page documents the contract and what consumers can observe. + +## Module configuration + +v8 packages emit their published JavaScript with `tsc` directly (via `just-scripts build`), so the +`module` compiler option controls the shipped artifact. To keep the CommonJS output byte-stable, +v8 packages keep `module: commonjs`. + +TypeScript 6 deprecates the classic Node resolver (`moduleResolution: node`/`node10`) with +`TS5107`, and the modern resolvers (`node16`/`nodenext`) cannot be paired with `module: commonjs` +(`TS5110`). Because changing `module` would change the published artifact, v8 stays on +`module: commonjs` + `moduleResolution: node10` and silences the deprecation with +`ignoreDeprecations: "6.0"` in `tsconfig.base.v8.json`. This is a scoped, maintenance-only stopgap: +`node10` is removed in TypeScript 7, at which point v8 (or its resolver) must be revisited. + +v9 packages differ: they emit JS via SWC (`.swcrc`), so `tsc` only type-checks and emits +declarations. They use the non-deprecated `module: nodenext` + `moduleResolution: nodenext`, which +does not affect their shipped JS. + +## What each artifact is and how it is produced + +| Artifact | Module format | ECMAScript baseline | Produced by | +| -------------- | ------------- | --------------------------------------------------- | --------------------------------------- | +| `lib` | ESM | `ships-es5` -> ES5, otherwise the tsconfig `target` | `tsc` (+ SWC downlevel for `ships-es5`) | +| `lib-commonjs` | CommonJS | `ships-es5` -> ES5, otherwise the tsconfig `target` | `tsc` (+ SWC downlevel for `ships-es5`) | +| `lib-amd` | AMD | always ES5 | SWC, from the emitted `lib` | +| `dist` | bundle / dts | n/a | webpack / API Extractor | + +- `tsc` type checks, emits declarations and modern JS. +- SWC downlevels/re-modularizes that output (`scripts/tasks/src/swc/transpile.ts`), which is the + same split converged packages shipping AMD (`@fluentui/react-portal-compat`) already used. +- `lib-amd` is a fully derived directory: it always mirrors `lib` and stale files are pruned. + +### `ships-es5` is explicit project metadata + +Whether a package publishes ES5 **cannot** be derived from any other metadata - eg +`@fluentui/react-icons-mdl2` ships AMD but its `lib`/`lib-commonjs` were emitted as ES2019. Every +project whose tsconfig declared `target: es5` before the migration therefore carries the +`ships-es5` tag in its `project.json`, and that tag is the only thing which enables the downlevel +step (`ts:downlevel`) - see `scripts/tasks/src/metadata-utils.ts#shipsES5`. + +`verify-packaging` asserts the published files actually match this contract (module shape, +ECMAScript baseline, declaration counterparts, `lib-amd`/`lib` parity). + +## Behaviour changes for consumers + +### 1. SWC downlevel helpers are inlined (no new runtime dependency) + +TypeScript 6 removed `target: es5`, so the ES5/AMD downlevel moved out of the compiler into SWC. +SWC's downlevel and module-interop helpers are **inlined** into each emitted file rather than +imported from `@swc/helpers`. + +Inlining is deliberate: external helpers would add `@swc/helpers` as a new runtime dependency to +every `ships-es5` v8 package. v8 is maintenance-only, and adding a runtime dependency to ~40 +published maintenance packages is a contract change we avoid. The cost is a small size increase +(~13% of `lib`, ~21% of `lib-amd`), which is immaterial for frozen legacy artifacts. The v8 +dependency graph is therefore unchanged from before the migration. + +### 2. `esModuleInterop` is unchanged for v8 + +`esModuleInterop` is **not** implied by `module: commonjs`, so v8's CommonJS emit is unchanged: v8 +packages never set `esModuleInterop` and still do not, so `import * as React from 'react'` keeps +emitting the plain `var React = require('react')` form it always did. + +(Note: `module: nodenext` *does* imply `esModuleInterop: true`, so the v9 base sets it explicitly to +match its own module mode; that only affects v9 declaration/type-checking, not the v8 CJS emit.) diff --git a/docs/workflows/testing.md b/docs/workflows/testing.md index 63e2df0050e15..31f365be67465 100644 --- a/docs/workflows/testing.md +++ b/docs/workflows/testing.md @@ -41,6 +41,29 @@ yarn nx run :test -u Review the snapshot diff to verify the change is correct before committing. +## E2E (Cypress Component Testing) + +`cypress.config.js` files are **JavaScript on purpose** - do not convert them to TypeScript. + +Cypress loads the config file in a child process where it registers its own bundled `ts-node` with +a hardcoded `moduleResolution: 'node'` (node10). TypeScript 6 rejects that with TS5107, so every +TypeScript file reachable from a `cypress.config.*` fails to load. The whole Node side of the +Cypress setup - the per project configs and the shared `@fluentui/scripts-cypress` entry point +(`scripts/cypress/src/base.config.js`) - is therefore plain CommonJS checked with `// @ts-check`. +The browser side (`scripts/cypress/src/browser`, `*.cy.tsx` specs) stays TypeScript, it is bundled +by webpack/esbuild-loader and never touched by Cypress' `ts-node`. + +Path aliases for the bundler are wired explicitly by `scripts/cypress/src/ts-paths.js`, which +resolves `paths` against the compiler reported `pathsBasePath` of `tsconfig.base.json` and hands +that base to `tsconfig-paths-webpack-plugin`. `pathsBasePath` is only populated correctly through +`extends` chains when the config file's identity is passed to TypeScript's `parseJsonConfigFileContent` +(as `configFileName`) - without it, `paths` declared in a shared base config resolve against the +wrong directory. Cypress' own `tsconfig-paths` registration (Node side) does _not_ skip resolution +just because `baseUrl` is missing - as of `tsconfig-paths@4.2` it only gives up when no +`tsconfig.json` can be found at all, and otherwise anchors `paths` at the directory of the _nearest_ +config file instead of the one that declares them. That is inconsequential here - the Cypress Node +process resolves every workspace package through its Yarn workspace `node_modules` symlink. + ## Conformance Tests Every component package has a `testing/isConformant.ts` file that validates: diff --git a/nx.json b/nx.json index ec7986898dad7..d3e70e3647c1b 100644 --- a/nx.json +++ b/nx.json @@ -62,7 +62,7 @@ "e2e": { "dependsOn": [], "cache": true, - "inputs": ["default", "{projectRoot}/cypress.config.ts", "!{projectRoot}/**/?(*.)+cy.[jt]s?(x)?"] + "inputs": ["default", "{projectRoot}/cypress.config.js", "!{projectRoot}/**/?(*.)+cy.[jt]s?(x)?"] }, "lint": { "executor": "nx:run-commands", diff --git a/package.json b/package.json index cb4ed19196e56..d49013776b301 100644 --- a/package.json +++ b/package.json @@ -100,6 +100,7 @@ "@storybook/icons": "1.3.2", "@storybook/react": "9.1.17", "@storybook/react-webpack5": "9.1.17", + "@swc-node/register": "1.9.2", "@swc/cli": "0.7.7", "@swc/core": "1.11.24", "@swc/helpers": "0.5.1", @@ -161,8 +162,8 @@ "@types/webpack-hot-middleware": "2.25.9", "@types/yargs": "13.0.11", "@types/yargs-unparser": "2.0.1", - "@typescript-eslint/eslint-plugin": "^8.46.2", - "@typescript-eslint/rule-tester": "8.46.2", + "@typescript-eslint/eslint-plugin": "^8.64.0", + "@typescript-eslint/rule-tester": "8.64.0", "autoprefixer": "10.2.1", "babel-jest": "30.4.1", "babel-loader": "9.1.3", @@ -307,16 +308,15 @@ "terser-webpack-plugin": "5.3.10", "through2": "4.0.2", "tmp": "0.2.1", - "ts-jest": "29.4.5", + "ts-jest": "29.4.11", "ts-loader": "9.4.2", "ts-node": "10.9.2", - "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "tslib": "2.8.1", "turndown": "7.2.0", "turndown-plugin-gfm": "1.0.2", - "typescript": "5.7.3", - "typescript-eslint": "8.46.2", + "typescript": "6.0.3", + "typescript-eslint": "8.64.0", "vite": "6.4.2", "webpack": "5.108.4", "webpack-bundle-analyzer": "4.10.1", @@ -351,6 +351,7 @@ ] }, "resolutions": { + "@swc-node/core": "1.13.3", "@types/jest-axe/axe-core": "4.7.2", "@phenomnomnominal/tsquery": "6.1.3", "esbuild": "0.25.0", diff --git a/packages/a11y-testing/tsconfig.json b/packages/a11y-testing/tsconfig.json index fa841b9ffea5a..d095d0d1d1e52 100644 --- a/packages/a11y-testing/tsconfig.json +++ b/packages/a11y-testing/tsconfig.json @@ -1,5 +1,6 @@ { "compilerOptions": { + "rootDir": "./src", "outDir": "lib", "target": "es6", "module": "commonjs", @@ -9,7 +10,8 @@ "sourceMap": true, "importHelpers": true, "noUnusedLocals": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["ES2019", "DOM"], "types": ["jest"], diff --git a/packages/api-docs/project.json b/packages/api-docs/project.json index e01d74d0cce28..d012eb335b174 100644 --- a/packages/api-docs/project.json +++ b/packages/api-docs/project.json @@ -3,6 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "implicitDependencies": [], - "tags": ["v8", "platform:node"], + "tags": ["v8", "platform:node", "ships-es5"], "sourceRoot": "packages/api-docs/src" } diff --git a/packages/api-docs/tsconfig.json b/packages/api-docs/tsconfig.json index e4f2f79574d59..09108232a60ee 100644 --- a/packages/api-docs/tsconfig.json +++ b/packages/api-docs/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,13 +10,14 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, "noImplicitThis": true, "skipLibCheck": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, - "downlevelIteration": true, "lib": ["es2017"], "types": [], "isolatedModules": true diff --git a/packages/azure-themes/project.json b/packages/azure-themes/project.json index 86771adb6e347..6366c95a18162 100644 --- a/packages/azure-themes/project.json +++ b/packages/azure-themes/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/azure-themes/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/azure-themes/tsconfig.json b/packages/azure-themes/tsconfig.json index 845a5902ebefa..7722592db4117 100644 --- a/packages/azure-themes/tsconfig.json +++ b/packages/azure-themes/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,10 +10,12 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, "skipLibCheck": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom"], "isolatedModules": true diff --git a/packages/charts/chart-web-components/.storybook/tsconfig.json b/packages/charts/chart-web-components/.storybook/tsconfig.json index 78905f4f65971..baca6b5cce357 100644 --- a/packages/charts/chart-web-components/.storybook/tsconfig.json +++ b/packages/charts/chart-web-components/.storybook/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "checkJs": true, "noEmit": true, - "types": ["node"] + "types": ["node", "static-assets"] }, "include": ["*", "../public", "../src/**/*.stories.*"] } diff --git a/packages/charts/chart-web-components/scripts/compile.js b/packages/charts/chart-web-components/scripts/compile.js index 990e52e51dcdc..90627fc5c5821 100644 --- a/packages/charts/chart-web-components/scripts/compile.js +++ b/packages/charts/chart-web-components/scripts/compile.js @@ -6,6 +6,8 @@ import { execSync } from 'child_process'; import chalk from 'chalk'; +import { createTsConfigWithoutPathAliases } from './tsconfig-utils.js'; + main(); function compile() { @@ -13,7 +15,12 @@ function compile() { console.log(chalk.bold(`🎬 compile:start`)); console.log(chalk.blueBright(`compile: running tsc`)); - execSync(`tsc -p tsconfig.lib.json --rootDir ./src --baseUrl .`, { stdio: 'inherit' }); + const noPathAliasesConfig = createTsConfigWithoutPathAliases('tsconfig.lib.json', 'compile'); + try { + execSync(`tsc -p ${noPathAliasesConfig.path} --rootDir ./src`, { stdio: 'inherit' }); + } finally { + noPathAliasesConfig.cleanup(); + } console.log(chalk.bold(`🏁 compile:end`)); } catch (err) { diff --git a/packages/charts/chart-web-components/scripts/tsconfig-utils.js b/packages/charts/chart-web-components/scripts/tsconfig-utils.js new file mode 100644 index 0000000000000..bfb46e854a2be --- /dev/null +++ b/packages/charts/chart-web-components/scripts/tsconfig-utils.js @@ -0,0 +1,106 @@ +// @ts-check + +/** + * This script should be shared for all web-component packages. + * Tracking issue - https://github.com/microsoft/fluentui/issues/33576 + */ + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * All transient configs created by this module which have not been cleaned up yet. + * + * Registering them in one place keeps the number of process listeners constant - one listener per + * module - no matter how many `tsc` invocations a script performs. + * + * NOTE: behaviourally aligned with `scripts/tasks/src/utils.ts#createTsConfigWithoutPathAliases`. + * The duplication is intentional - web-components packages must not depend on the `just` based + * v8 build tooling. + * + * @type {Set} + */ +const pendingTransientTsConfigs = new Set(); +let transientTsConfigsCounter = 0; +let processListenersRegistered = false; + +/** + * @param {string} generatedPath + */ +function removeTransientTsConfig(generatedPath) { + pendingTransientTsConfigs.delete(generatedPath); + fs.rmSync(generatedPath, { force: true }); +} + +function cleanupTransientTsConfigs() { + for (const generatedPath of [...pendingTransientTsConfigs]) { + removeTransientTsConfig(generatedPath); + } +} + +function registerProcessListeners() { + if (processListenersRegistered) { + return; + } + + processListenersRegistered = true; + + process.on('exit', cleanupTransientTsConfigs); + + // node does not run `exit` listeners when a process is terminated by a signal, + // so clean up explicitly and re-raise to keep the default termination semantics + for (const signal of /** @type {const} */ (['SIGINT', 'SIGTERM'])) { + process.once(signal, cleanupTransientTsConfigsOnSignal); + } +} + +/** + * @param {NodeJS.Signals} signal + */ +function cleanupTransientTsConfigsOnSignal(signal) { + cleanupTransientTsConfigs(); + process.kill(process.pid, signal); +} + +/** + * Creates a transient tsconfig, next to `tsConfigPath`, which turns TS path aliases off + * (`"paths": null`) for a single `tsc` invocation and returns its path. + * + * TypeScript 6 deprecates `baseUrl`, which used to be (ab)used as `tsc --baseUrl .` to make the + * workspace root relative `paths` entries unresolvable. TypeScript 6 resolves `paths` relative to + * the config file that declares them, so nulling `paths` is now the only supported way to opt a + * compilation out of path aliases - and it cannot be expressed via CLI flags, only via a config file. + * + * NOTES: + * - the generated config lives next to the original one, so every relative path + * (`extends`/`include`/`outDir`/`rootDir`/`references`) keeps resolving identically + * - the file name is unique per process and invocation, so the concurrent `tsc` runs these + * scripts spawn can never delete each other's config + * + * @param {string} tsConfigPath + * @param {string} purpose + */ +export function createTsConfigWithoutPathAliases(tsConfigPath, purpose) { + if (!fs.existsSync(tsConfigPath)) { + throw new Error(`Cannot disable TS path aliases for "${tsConfigPath}", because the file doesn't exist.`); + } + + const configFileName = path.basename(tsConfigPath); + const uniqueId = `${process.pid}-${transientTsConfigsCounter++}-${crypto.randomBytes(4).toString('hex')}`; + const generatedPath = path.join( + path.dirname(tsConfigPath), + `tsconfig.__generated-no-path-aliases-${purpose}-${uniqueId}-${configFileName}`, + ); + + fs.writeFileSync( + generatedPath, + JSON.stringify({ extends: `./${configFileName}`, compilerOptions: { paths: null } }, null, 2), + 'utf-8', + ); + + pendingTransientTsConfigs.add(generatedPath); + registerProcessListeners(); + + return { path: generatedPath, cleanup: () => removeTransientTsConfig(generatedPath) }; +} diff --git a/packages/charts/chart-web-components/scripts/type-check.js b/packages/charts/chart-web-components/scripts/type-check.js index 9d8a4b6c2f419..8d308822a2ab6 100644 --- a/packages/charts/chart-web-components/scripts/type-check.js +++ b/packages/charts/chart-web-components/scripts/type-check.js @@ -11,6 +11,8 @@ import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { exit } from 'node:process'; +import { createTsConfigWithoutPathAliases } from './tsconfig-utils.js'; + const asyncExec = promisify(exec); main().catch(err => { @@ -28,15 +30,24 @@ async function main() { const asyncQueue = []; + const cleanupQueue = []; + for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --noEmit --baseUrl .`; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(ref, 'type-check'); + cleanupQueue.push(noPathAliasesConfig.cleanup); + + const program = `tsc -p ${noPathAliasesConfig.path} --pretty --noEmit`; asyncQueue.push(asyncExec(program)); } - return Promise.all(asyncQueue).catch(err => { - console.error(err.stdout); - exit(1); - }); + return Promise.all(asyncQueue) + .catch(err => { + console.error(err.stdout); + exit(1); + }) + .finally(() => { + cleanupQueue.forEach(cleanup => cleanup()); + }); } /** diff --git a/packages/charts/chart-web-components/tsconfig.api-extractor.json b/packages/charts/chart-web-components/tsconfig.api-extractor.json index e245193e1fb3d..2640248432cb8 100644 --- a/packages/charts/chart-web-components/tsconfig.api-extractor.json +++ b/packages/charts/chart-web-components/tsconfig.api-extractor.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.lib.json", "compilerOptions": { - "paths": null, - "baseUrl": "." + "paths": null } } diff --git a/packages/charts/react-charting/project.json b/packages/charts/react-charting/project.json index 047d9ab2457fb..c671ed553e6a3 100644 --- a/packages/charts/react-charting/project.json +++ b/packages/charts/react-charting/project.json @@ -2,7 +2,7 @@ "name": "react-charting", "$schema": "../../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8", "ships-bundle", "charting"], + "tags": ["v8", "ships-bundle", "charting", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/charts/react-charting/src/components/LineChart/LineChartRTL.test.tsx b/packages/charts/react-charting/src/components/LineChart/LineChartRTL.test.tsx index 064cbe4758206..a70d710797ebe 100644 --- a/packages/charts/react-charting/src/components/LineChart/LineChartRTL.test.tsx +++ b/packages/charts/react-charting/src/components/LineChart/LineChartRTL.test.tsx @@ -276,6 +276,16 @@ const chartPointsWithGaps = { lineChartData: pointsWithGaps, }; +const tickValues = [ + new Date('2020-03-03T00:00:00.000Z'), + new Date('2020-03-04T00:00:00.000Z'), + new Date('2020-03-05T00:00:00.000Z'), + new Date('2020-03-06T00:00:00.000Z'), + new Date('2020-03-07T00:00:00.000Z'), + new Date('2020-03-08T00:00:00.000Z'), + new Date('2020-03-09T00:00:00.000Z'), +]; + const secondaryYScalePoints = [{ yMaxValue: 50000, yMinValue: 10000 }]; describe('Line chart rendering', () => { @@ -451,16 +461,6 @@ const simplePoints = { ], }; -const tickValues = [ - new Date('2020-03-03T00:00:00.000Z'), - new Date('2020-03-04T00:00:00.000Z'), - new Date('2020-03-05T00:00:00.000Z'), - new Date('2020-03-06T00:00:00.000Z'), - new Date('2020-03-07T00:00:00.000Z'), - new Date('2020-03-08T00:00:00.000Z'), - new Date('2020-03-09T00:00:00.000Z'), -]; - const eventAnnotationProps = { events: [ { diff --git a/packages/charts/react-charting/tsconfig.json b/packages/charts/react-charting/tsconfig.json index ae47a5d033758..a4caf68b51ef8 100644 --- a/packages/charts/react-charting/tsconfig.json +++ b/packages/charts/react-charting/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,10 +10,12 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "strictBindCallApply": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "lib": ["es5", "dom", "ES2015.Symbol.WellKnown"], diff --git a/packages/charts/react-charts/library/etc/react-charts.api.md b/packages/charts/react-charts/library/etc/react-charts.api.md index b841ad9d64148..5eb44d734cdb8 100644 --- a/packages/charts/react-charts/library/etc/react-charts.api.md +++ b/packages/charts/react-charts/library/etc/react-charts.api.md @@ -11,6 +11,7 @@ import type { Margin } from '@fluentui/chart-utilities'; import type { PositioningShorthand } from '@fluentui/react-positioning'; import * as React_2 from 'react'; import type { Ref } from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import type { SankeyGraph } from 'd3-sankey'; import type { SankeyLayout } from 'd3-sankey'; import type { SankeyLink } from 'd3-sankey'; @@ -713,7 +714,7 @@ export const DataVizPalette: { export const DeclarativeChart: React_2.FunctionComponent; // @public -export interface DeclarativeChartProps extends React_2.RefAttributes { +export interface DeclarativeChartProps extends RefAttributes { chartSchema: Schema; colorwayType?: ColorwayType; componentRef?: React_2.Ref; @@ -1109,7 +1110,7 @@ export interface HeatMapChartStyles extends CartesianChartStyles { export const HorizontalBarChart: React_2.FunctionComponent; // @public -export interface HorizontalBarChartProps extends React_2.RefAttributes { +export interface HorizontalBarChartProps extends RefAttributes { barHeight?: number; calloutProps?: ChartPopoverProps; calloutPropsPerDataPoint?: (dataPointCalloutProps: ChartDataPoint) => ChartPopoverProps; @@ -1809,7 +1810,7 @@ export type SNode = SankeyNode; export const Sparkline: React_2.FunctionComponent; // @public -export interface SparklineProps extends React.RefAttributes { +export interface SparklineProps extends RefAttributes { className?: string; culture?: string; data?: ChartProps; diff --git a/packages/charts/react-charts/library/src/components/CommonComponents/useCartesianChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/CommonComponents/useCartesianChartStyles.styles.ts index ab56008ec291d..cdad47180d5dc 100644 --- a/packages/charts/react-charts/library/src/components/CommonComponents/useCartesianChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/CommonComponents/useCartesianChartStyles.styles.ts @@ -3,7 +3,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { CartesianChartProps, CartesianChartStyles } from './CartesianChart.types'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens, typographyStyles } from '@fluentui/react-theme'; import { CARTESIAN_XAXIS_CLASSNAME, HighContrastSelector, useRtl } from '../../utilities/utilities'; import { getAxisTitleStyle, getTooltipStyle } from '../../utilities/index'; diff --git a/packages/charts/react-charts/library/src/components/CommonComponents/useChartPopoverStyles.styles.ts b/packages/charts/react-charts/library/src/components/CommonComponents/useChartPopoverStyles.styles.ts index fe5de5f2d5f15..ec4efca2c7973 100644 --- a/packages/charts/react-charts/library/src/components/CommonComponents/useChartPopoverStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/CommonComponents/useChartPopoverStyles.styles.ts @@ -1,7 +1,7 @@ 'use client'; import { makeStyles, mergeClasses } from '@griffel/react'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens, typographyStyles } from '@fluentui/react-theme'; import type { ChartPopoverProps, PopoverComponentStyles } from './ChartPopover.types'; diff --git a/packages/charts/react-charts/library/src/components/DeclarativeChart/DeclarativeChart.tsx b/packages/charts/react-charts/library/src/components/DeclarativeChart/DeclarativeChart.tsx index c2d5f4b392f50..84ac5a130dbe5 100644 --- a/packages/charts/react-charts/library/src/components/DeclarativeChart/DeclarativeChart.tsx +++ b/packages/charts/react-charts/library/src/components/DeclarativeChart/DeclarativeChart.tsx @@ -65,7 +65,7 @@ import { withResponsiveContainer } from '../ResponsiveContainer/withResponsiveCo import { ChartTable } from '../ChartTable/index'; import type { LegendsProps, LegendContainer } from '../Legends/index'; import { Legends } from '../Legends/index'; -import type { JSXElement } from '@fluentui/react-utilities/src/index'; +import type { JSXElement, RefAttributes } from '@fluentui/react-utilities'; import { resolveCSSVariables, useRtl } from '../../utilities/index'; import { exportChartsAsImage } from '../../utilities/image-export-utils'; @@ -105,7 +105,7 @@ export interface Schema { * DeclarativeChart props. * {@docCategory DeclarativeChart} */ -export interface DeclarativeChartProps extends React.RefAttributes { +export interface DeclarativeChartProps extends RefAttributes { /** * The schema representing the chart data, layout and configuration */ diff --git a/packages/charts/react-charts/library/src/components/GaugeChart/useGaugeChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/GaugeChart/useGaugeChartStyles.styles.ts index 36c4f6081aff8..b6efd4f1569ee 100644 --- a/packages/charts/react-charts/library/src/components/GaugeChart/useGaugeChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/GaugeChart/useGaugeChartStyles.styles.ts @@ -1,7 +1,7 @@ 'use client'; import { tokens, typographyStyles } from '@fluentui/react-theme'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { GaugeChartProps, GaugeChartStyles } from './GaugeChart.types'; diff --git a/packages/charts/react-charts/library/src/components/GroupedVerticalBarChart/useGroupedVerticalBarChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/GroupedVerticalBarChart/useGroupedVerticalBarChartStyles.styles.ts index 2ab030d5a8af0..76dd2c5c34a9e 100644 --- a/packages/charts/react-charts/library/src/components/GroupedVerticalBarChart/useGroupedVerticalBarChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/GroupedVerticalBarChart/useGroupedVerticalBarChartStyles.styles.ts @@ -3,7 +3,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { GroupedVerticalBarChartProps, GroupedVerticalBarChartStyles } from '../../index'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { getBarLabelStyle, getTooltipStyle } from '../../utilities/index'; export const groupedVerticalBarChartClassNames: SlotClassNames = { diff --git a/packages/charts/react-charts/library/src/components/HorizontalBarChart/HorizontalBarChart.types.ts b/packages/charts/react-charts/library/src/components/HorizontalBarChart/HorizontalBarChart.types.ts index 9ad65955b721d..d982fe2e8d70e 100644 --- a/packages/charts/react-charts/library/src/components/HorizontalBarChart/HorizontalBarChart.types.ts +++ b/packages/charts/react-charts/library/src/components/HorizontalBarChart/HorizontalBarChart.types.ts @@ -1,5 +1,4 @@ -import type * as React from 'react'; -import type { JSXElement } from '@fluentui/react-utilities'; +import type { JSXElement, RefAttributes } from '@fluentui/react-utilities'; import type { ChartPopoverProps } from '../CommonComponents/ChartPopover.types'; import type { ChartDataPoint, ChartProps } from './index'; import type { LegendsProps } from '../Legends/index'; @@ -8,7 +7,7 @@ import type { LegendsProps } from '../Legends/index'; * Horizontal Bar Chart properties * {@docCategory HorizontalBarChart} */ -export interface HorizontalBarChartProps extends React.RefAttributes { +export interface HorizontalBarChartProps extends RefAttributes { /** * An array of chart data points for the Horizontal bar chart */ diff --git a/packages/charts/react-charts/library/src/components/LineChart/useLineChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/LineChart/useLineChartStyles.styles.ts index 3fb093710d092..dd066db87e706 100644 --- a/packages/charts/react-charts/library/src/components/LineChart/useLineChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/LineChart/useLineChartStyles.styles.ts @@ -4,7 +4,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import { tokens } from '@fluentui/react-theme'; import type { LineChartProps, LineChartStyles } from './LineChart.types'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { HighContrastSelector } from '../../utilities/index'; import { getMarkerLabelStyle, getTooltipStyle } from '../../utilities/index'; diff --git a/packages/charts/react-charts/library/src/components/ScatterChart/useScatterChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/ScatterChart/useScatterChartStyles.styles.ts index e8df8fe04b7fa..ff7bc47d25bfd 100644 --- a/packages/charts/react-charts/library/src/components/ScatterChart/useScatterChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/ScatterChart/useScatterChartStyles.styles.ts @@ -3,7 +3,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { ScatterChartProps, ScatterChartStyles } from './ScatterChart.types'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { getMarkerLabelStyle, getTooltipStyle } from '../../utilities/index'; /** diff --git a/packages/charts/react-charts/library/src/components/Sparkline/Sparkline.types.ts b/packages/charts/react-charts/library/src/components/Sparkline/Sparkline.types.ts index 1591314939f40..baff2ee6a429e 100644 --- a/packages/charts/react-charts/library/src/components/Sparkline/Sparkline.types.ts +++ b/packages/charts/react-charts/library/src/components/Sparkline/Sparkline.types.ts @@ -1,3 +1,4 @@ +import type { RefAttributes } from '@fluentui/react-utilities'; import type { ChartProps } from './index'; import type { CartesianChartStyleProps } from '../CommonComponents/index'; @@ -7,7 +8,7 @@ export interface SparklineStyleProps extends CartesianChartStyleProps {} * Sparkline properties * {@docCategory SparklineChart} */ -export interface SparklineProps extends React.RefAttributes { +export interface SparklineProps extends RefAttributes { /** * An array of chart data points for the Sparkline chart */ diff --git a/packages/charts/react-charts/library/src/components/VerticalBarChart/useVerticalBarChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/VerticalBarChart/useVerticalBarChartStyles.styles.ts index e9e19d850647a..10e9e9fe86b8c 100644 --- a/packages/charts/react-charts/library/src/components/VerticalBarChart/useVerticalBarChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/VerticalBarChart/useVerticalBarChartStyles.styles.ts @@ -3,7 +3,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { VerticalBarChartProps, VerticalBarChartStyles } from '../../index'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { tokens } from '@fluentui/react-theme'; import { HighContrastSelector } from '../../utilities/utilities'; import { getBarLabelStyle, getTooltipStyle } from '../../utilities/index'; diff --git a/packages/charts/react-charts/library/src/components/VerticalStackedBarChart/useVerticalStackedBarChartStyles.styles.ts b/packages/charts/react-charts/library/src/components/VerticalStackedBarChart/useVerticalStackedBarChartStyles.styles.ts index f69f5b192d555..68a665c36fee2 100644 --- a/packages/charts/react-charts/library/src/components/VerticalStackedBarChart/useVerticalStackedBarChartStyles.styles.ts +++ b/packages/charts/react-charts/library/src/components/VerticalStackedBarChart/useVerticalStackedBarChartStyles.styles.ts @@ -3,7 +3,7 @@ import type { GriffelStyle } from '@griffel/react'; import { makeStyles, mergeClasses } from '@griffel/react'; import type { VerticalStackedBarChartProps, VerticalStackedBarChartStyles } from './VerticalStackedBarChart.types'; -import type { SlotClassNames } from '@fluentui/react-utilities/src/index'; +import type { SlotClassNames } from '@fluentui/react-utilities'; import { getBarLabelStyle, getTooltipStyle } from '../../utilities/index'; export const verticalstackedbarchartClassNames: SlotClassNames = { diff --git a/packages/codemods/src/modRunner/tests/mocks/MockProject/projects/subProject/tsconfig.json b/packages/codemods/src/modRunner/tests/mocks/MockProject/projects/subProject/tsconfig.json index ef22c72804390..756a89837fc50 100644 --- a/packages/codemods/src/modRunner/tests/mocks/MockProject/projects/subProject/tsconfig.json +++ b/packages/codemods/src/modRunner/tests/mocks/MockProject/projects/subProject/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { + "rootDir": "./src", "target": "esnext", - "module": "commonjs", + "module": "nodenext", "jsx": "react", "declaration": true, "outDir": "./lib", "strict": true, - "moduleResolution": "node", + "moduleResolution": "nodenext", "esModuleInterop": true, "typeRoots": ["node_modules/@types", "node_modules/just-stack-single-lib/node_modules/@types"] }, diff --git a/packages/codemods/src/modRunner/tests/mocks/MockProject/tsconfig.json b/packages/codemods/src/modRunner/tests/mocks/MockProject/tsconfig.json index ef22c72804390..756a89837fc50 100644 --- a/packages/codemods/src/modRunner/tests/mocks/MockProject/tsconfig.json +++ b/packages/codemods/src/modRunner/tests/mocks/MockProject/tsconfig.json @@ -1,12 +1,13 @@ { "compilerOptions": { + "rootDir": "./src", "target": "esnext", - "module": "commonjs", + "module": "nodenext", "jsx": "react", "declaration": true, "outDir": "./lib", "strict": true, - "moduleResolution": "node", + "moduleResolution": "nodenext", "esModuleInterop": true, "typeRoots": ["node_modules/@types", "node_modules/just-stack-single-lib/node_modules/@types"] }, diff --git a/packages/codemods/tsconfig.json b/packages/codemods/tsconfig.json index cf6c3ca4ff178..2c26f3e7eac26 100644 --- a/packages/codemods/tsconfig.json +++ b/packages/codemods/tsconfig.json @@ -1,6 +1,6 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "lib", "target": "es6", "module": "commonjs", @@ -11,9 +11,11 @@ "allowSyntheticDefaultImports": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "esModuleInterop": true, "lib": ["dom", "es2015.promise", "es2017"], diff --git a/packages/date-time-utilities/project.json b/packages/date-time-utilities/project.json index 440176442f91e..dec90cbd99d69 100644 --- a/packages/date-time-utilities/project.json +++ b/packages/date-time-utilities/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/date-time-utilities/src", - "tags": ["v8"] + "tags": ["v8", "ships-es5"] } diff --git a/packages/date-time-utilities/src/timeFormatting/timeFormatting.test.ts b/packages/date-time-utilities/src/timeFormatting/timeFormatting.test.ts index c6e1290462e70..a7749dc1beaaf 100644 --- a/packages/date-time-utilities/src/timeFormatting/timeFormatting.test.ts +++ b/packages/date-time-utilities/src/timeFormatting/timeFormatting.test.ts @@ -11,7 +11,7 @@ describe('timeFormatting', () => { * thus it will be determined on users physical location - making the test non-deterministic * */ - const toLocaleTimeStringMock: (locales?: string | string[], options?: Intl.DateTimeFormatOptions) => string = ( + const toLocaleTimeStringMock: (locales?: Intl.LocalesArgument, options?: Intl.DateTimeFormatOptions) => string = ( locales, options, ) => { diff --git a/packages/date-time-utilities/tsconfig.json b/packages/date-time-utilities/tsconfig.json index aa10c628691bc..be9697ba1d610 100644 --- a/packages/date-time-utilities/tsconfig.json +++ b/packages/date-time-utilities/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es5", "es2015.promise", "dom", "ES2015.Symbol.WellKnown"], @@ -12,8 +13,10 @@ "importHelpers": true, "noImplicitAny": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/dom-utilities/project.json b/packages/dom-utilities/project.json index 8a74780c99344..a99fc834704eb 100644 --- a/packages/dom-utilities/project.json +++ b/packages/dom-utilities/project.json @@ -2,6 +2,6 @@ "name": "dom-utilities", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [] } diff --git a/packages/dom-utilities/tsconfig.json b/packages/dom-utilities/tsconfig.json index d0b3e091fae55..2bdcde59b2dae 100644 --- a/packages/dom-utilities/tsconfig.json +++ b/packages/dom-utilities/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -11,7 +11,8 @@ "importHelpers": true, "noUnusedLocals": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index e510c117fabbd..50e27e52a89f6 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -13,8 +13,8 @@ "@eslint/compat": "1.3.0", "@griffel/eslint-plugin": "^2.0.0", "@rnx-kit/eslint-plugin": "^0.8.4", - "@typescript-eslint/type-utils": "^8.46.2", - "@typescript-eslint/utils": "^8.46.2", + "@typescript-eslint/type-utils": "^8.64.0", + "@typescript-eslint/utils": "^8.64.0", "@nx/eslint-plugin": "21.6.10", "eslint-config-prettier": "^10.1.8", "eslint-config-airbnb-extended": "2.1.2", @@ -30,12 +30,12 @@ "globals": "13.24.0", "jju": "^1.4.0", "minimatch": "^3.1.2", - "typescript-eslint": "^8.46.2", - "@typescript-eslint/eslint-plugin": "^8.46.2" + "typescript-eslint": "^8.64.0", + "@typescript-eslint/eslint-plugin": "^8.64.0" }, "peerDependencies": { "eslint": "^9.0.0", - "typescript": "^5.0.0" + "typescript": ">=5.0.0 <7.0.0" }, "files": [ "src" diff --git a/packages/eslint-plugin/src/rules/ban-context-export/fixtures/context-selector/tsconfig.json b/packages/eslint-plugin/src/rules/ban-context-export/fixtures/context-selector/tsconfig.json index acbde2e6a1a5c..174a296c31e67 100644 --- a/packages/eslint-plugin/src/rules/ban-context-export/fixtures/context-selector/tsconfig.json +++ b/packages/eslint-plugin/src/rules/ban-context-export/fixtures/context-selector/tsconfig.json @@ -7,7 +7,6 @@ "declaration": true, "declarationDir": "dist/types", "types": ["static-assets", "environment"], - "baseUrl": ".", "paths": { "@proj/react-context-selector": ["../react-context-selector-pkg/index.ts"] } diff --git a/packages/eslint-plugin/src/rules/ban-context-export/fixtures/named-export/tsconfig.json b/packages/eslint-plugin/src/rules/ban-context-export/fixtures/named-export/tsconfig.json index acbde2e6a1a5c..174a296c31e67 100644 --- a/packages/eslint-plugin/src/rules/ban-context-export/fixtures/named-export/tsconfig.json +++ b/packages/eslint-plugin/src/rules/ban-context-export/fixtures/named-export/tsconfig.json @@ -7,7 +7,6 @@ "declaration": true, "declarationDir": "dist/types", "types": ["static-assets", "environment"], - "baseUrl": ".", "paths": { "@proj/react-context-selector": ["../react-context-selector-pkg/index.ts"] } diff --git a/packages/eslint-plugin/src/utils/configHelpers.js b/packages/eslint-plugin/src/utils/configHelpers.js index 37789c7e9a150..384ed70ddecb1 100644 --- a/packages/eslint-plugin/src/utils/configHelpers.js +++ b/packages/eslint-plugin/src/utils/configHelpers.js @@ -44,8 +44,10 @@ const storyFiles = ['**/*.stories.tsx', '**/*.stories.ts']; const configFiles = [ './just.config.ts', - './cypress.config.ts', './gulpfile.ts', + // `cypress.config.*` is always plain JavaScript (Cypress loads it through a bundled `ts-node` that + // rejects TypeScript 6 projects), so it's already covered by `./*.js` below rather than needing its + // own `.ts` entry here. './*.js', './.*.js', './config/**', diff --git a/packages/example-data/project.json b/packages/example-data/project.json index 59cc8f14b5c63..f4b494dc6d2a1 100644 --- a/packages/example-data/project.json +++ b/packages/example-data/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/example-data/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/example-data/tsconfig.json b/packages/example-data/tsconfig.json index 5544ec08ccc03..69ef7061165ab 100644 --- a/packages/example-data/tsconfig.json +++ b/packages/example-data/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom"], "types": [], diff --git a/packages/fluent2-theme/project.json b/packages/fluent2-theme/project.json index ddedc06d98ec4..77f573615b2d7 100644 --- a/packages/fluent2-theme/project.json +++ b/packages/fluent2-theme/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/fluent2-theme/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/fluent2-theme/tsconfig.json b/packages/fluent2-theme/tsconfig.json index 845a5902ebefa..7722592db4117 100644 --- a/packages/fluent2-theme/tsconfig.json +++ b/packages/fluent2-theme/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,10 +10,12 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, "skipLibCheck": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom"], "isolatedModules": true diff --git a/packages/font-icons-mdl2/project.json b/packages/font-icons-mdl2/project.json index 8ddfd3f58ca48..2da991ef124be 100644 --- a/packages/font-icons-mdl2/project.json +++ b/packages/font-icons-mdl2/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/font-icons-mdl2/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/font-icons-mdl2/tsconfig.json b/packages/font-icons-mdl2/tsconfig.json index 7792941ba2a04..c474dc75aa943 100644 --- a/packages/font-icons-mdl2/tsconfig.json +++ b/packages/font-icons-mdl2/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "module": "commonjs", "lib": ["es5", "dom"], "jsx": "react", @@ -9,7 +10,8 @@ "experimentalDecorators": true, "importHelpers": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "skipLibCheck": true, "preserveConstEnums": false, "types": [] diff --git a/packages/foundation-legacy/project.json b/packages/foundation-legacy/project.json index d4bc7f9d164d9..b6d4340c7109f 100644 --- a/packages/foundation-legacy/project.json +++ b/packages/foundation-legacy/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/foundation-legacy/src", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/foundation-legacy/src/slots.test.tsx b/packages/foundation-legacy/src/slots.test.tsx index 869a4f8e9dbc2..76d0c0b73cdc8 100644 --- a/packages/foundation-legacy/src/slots.test.tsx +++ b/packages/foundation-legacy/src/slots.test.tsx @@ -142,8 +142,12 @@ describe('typings', () => { describe('withSlots', () => { let reactCalls: number; + // `import * as React` compiles to a namespace object whose members are non-configurable accessors, so it + // cannot be spied on directly. Those accessors read through to the underlying CommonJS module, which can. + const reactModule: typeof React = jest.requireActual('react'); + beforeEach(() => { - jest.spyOn(React, 'createElement').mockImplementation((() => { + jest.spyOn(reactModule, 'createElement').mockImplementation((() => { reactCalls += 1; }) as any); reactCalls = 0; diff --git a/packages/foundation-legacy/tsconfig.json b/packages/foundation-legacy/tsconfig.json index 8b450c327d70d..124541139bdca 100644 --- a/packages/foundation-legacy/tsconfig.json +++ b/packages/foundation-legacy/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "isolatedModules": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Core", "ES2015.Symbol.WellKnown"], diff --git a/packages/jest-serializer-merge-styles/project.json b/packages/jest-serializer-merge-styles/project.json index 08a28991a40e8..f966f52eb0733 100644 --- a/packages/jest-serializer-merge-styles/project.json +++ b/packages/jest-serializer-merge-styles/project.json @@ -3,6 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "implicitDependencies": [], - "tags": ["v8", "platform:node"], + "tags": ["v8", "platform:node", "ships-es5"], "sourceRoot": "packages/jest-serializer-merge-styles/src" } diff --git a/packages/jest-serializer-merge-styles/tsconfig.json b/packages/jest-serializer-merge-styles/tsconfig.json index 0422ae654d5f4..e73819f3080b2 100644 --- a/packages/jest-serializer-merge-styles/tsconfig.json +++ b/packages/jest-serializer-merge-styles/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es2017", "dom"], @@ -10,7 +11,8 @@ "experimentalDecorators": true, "importHelpers": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "types": ["jest"], "isolatedModules": true diff --git a/packages/keyboard-key/project.json b/packages/keyboard-key/project.json index e3829ee4d4af8..ab5771af6dfdb 100644 --- a/packages/keyboard-key/project.json +++ b/packages/keyboard-key/project.json @@ -2,6 +2,6 @@ "name": "keyboard-key", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [] } diff --git a/packages/keyboard-key/tsconfig.json b/packages/keyboard-key/tsconfig.json index 0089e0abe2032..4c166c7aa4d3d 100644 --- a/packages/keyboard-key/tsconfig.json +++ b/packages/keyboard-key/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "lib": ["ES5", "DOM", "ES2015.Collection", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "jsx": "react", @@ -11,9 +11,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "types": ["jest"], "isolatedModules": true diff --git a/packages/merge-styles/project.json b/packages/merge-styles/project.json index 2dcdc90faf56b..7e1aface70a4f 100644 --- a/packages/merge-styles/project.json +++ b/packages/merge-styles/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/merge-styles/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/merge-styles/tsconfig.json b/packages/merge-styles/tsconfig.json index ab623f4eba190..98451ae847738 100644 --- a/packages/merge-styles/tsconfig.json +++ b/packages/merge-styles/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["ES5", "ES2015.Collection", "ES2015.Iterable", "ES2015.Symbol.WellKnown", "dom"], @@ -10,7 +11,8 @@ "experimentalDecorators": true, "importHelpers": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "isolatedModules": true, "types": ["jest"] diff --git a/packages/monaco-editor/just.config.ts b/packages/monaco-editor/just.config.ts index ec4614eb7abec..b9e6296e303de 100644 --- a/packages/monaco-editor/just.config.ts +++ b/packages/monaco-editor/just.config.ts @@ -8,7 +8,8 @@ preset(); task('clean', cleanTask({ paths: ['esm', 'lib', 'lib-commonjs'].map(p => path.join(process.cwd(), p)) })); task('transform-css', transformCssTask); task('ts:postprocess', postprocessTask([...postprocessTask.defaultLibPaths, 'esm/**/*.d.ts'])); -task('ts', series(parallel('ts:esm', 'ts:commonjs'), 'ts:postprocess')); +// this package publishes ES5 (`ships-es5`), which since TypeScript 6 is downleveled by swc - see `ts:downlevel` +task('ts', series(parallel('ts:esm', 'ts:commonjs'), 'ts:downlevel', 'ts:postprocess')); task('build', series('clean', 'copy', 'transform-css', 'ts', 'lint-imports:all')).cached!(); diff --git a/packages/monaco-editor/project.json b/packages/monaco-editor/project.json index bfa3f4251ef20..d6ddc85d99d44 100644 --- a/packages/monaco-editor/project.json +++ b/packages/monaco-editor/project.json @@ -2,6 +2,6 @@ "name": "monaco-editor", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [] } diff --git a/packages/monaco-editor/tsconfig.json b/packages/monaco-editor/tsconfig.json index 4bd51015a2ed8..c38d14be64a34 100644 --- a/packages/monaco-editor/tsconfig.json +++ b/packages/monaco-editor/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom"], "types": [] diff --git a/packages/public-docsite-setup/project.json b/packages/public-docsite-setup/project.json index 50c99f1508f89..7031f45b56d99 100644 --- a/packages/public-docsite-setup/project.json +++ b/packages/public-docsite-setup/project.json @@ -2,6 +2,6 @@ "name": "public-docsite-setup", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [] } diff --git a/packages/public-docsite-setup/tsconfig.json b/packages/public-docsite-setup/tsconfig.json index 154bc136fdc16..d3ea0effc6429 100644 --- a/packages/public-docsite-setup/tsconfig.json +++ b/packages/public-docsite-setup/tsconfig.json @@ -1,12 +1,14 @@ { "compilerOptions": { + "rootDir": "./src", "outDir": "lib", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, "sourceMap": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "strict": true, "noUnusedLocals": true, diff --git a/packages/react-cards/project.json b/packages/react-cards/project.json index d909901d2917c..1031700d13e6f 100644 --- a/packages/react-cards/project.json +++ b/packages/react-cards/project.json @@ -2,7 +2,7 @@ "name": "react-cards", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/react-cards/tsconfig.json b/packages/react-cards/tsconfig.json index bce86629c30db..447b2a0fed4ff 100644 --- a/packages/react-cards/tsconfig.json +++ b/packages/react-cards/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,7 +10,9 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, - "moduleResolution": "node", + "strict": false, + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-components/babel-preset-global-context/cypress.config.ts b/packages/react-components/babel-preset-global-context/cypress.config.js similarity index 81% rename from packages/react-components/babel-preset-global-context/cypress.config.ts rename to packages/react-components/babel-preset-global-context/cypress.config.js index 17d33ed8c6b6c..55847f3c12ab1 100644 --- a/packages/react-components/babel-preset-global-context/cypress.config.ts +++ b/packages/react-components/babel-preset-global-context/cypress.config.js @@ -1,5 +1,8 @@ -import { defineConfig } from 'cypress'; -import { baseConfig } from '@fluentui/scripts-cypress'; +// @ts-check + +const { defineConfig } = require('cypress'); + +const { baseConfig } = require('@fluentui/scripts-cypress'); /** * Extends the base cypress webpack config to add the babel preset @@ -28,4 +31,5 @@ defineConfig({ }, }, }); -export default baseConfig; + +module.exports = baseConfig; diff --git a/packages/react-components/babel-preset-global-context/project.json b/packages/react-components/babel-preset-global-context/project.json index a692c5e265aeb..d1a97ad0efa11 100644 --- a/packages/react-components/babel-preset-global-context/project.json +++ b/packages/react-components/babel-preset-global-context/project.json @@ -4,5 +4,10 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/react-components/babel-preset-global-context/src", - "tags": ["vNext", "platform:node", "tools"] + "tags": ["vNext", "platform:node", "tools"], + "targets": { + "e2e": { + "dependsOn": ["build"] + } + } } diff --git a/packages/react-components/babel-preset-global-context/src/Test.cy.tsx b/packages/react-components/babel-preset-global-context/src/Test.cy.tsx index f030b9a90a8e2..57ed35e304a2e 100644 --- a/packages/react-components/babel-preset-global-context/src/Test.cy.tsx +++ b/packages/react-components/babel-preset-global-context/src/Test.cy.tsx @@ -1,5 +1,5 @@ /* - * These tests are run with the specific cypress.config.ts file + * These tests are run with the specific cypress.config.js file * in this project in order to consume @fluentui/babel-preset-global-context during bundling */ @@ -68,7 +68,7 @@ describe('babel-preset-global-context', () => { }); // The contexts used in these tests should be ignored by babel preset - // configured in cypress.config.ts + // configured in cypress.config.js describe('untargeted packages', () => { const v1Foo = 'v1-foo'; // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/packages/react-components/deprecated/react-infobutton/cypress.config.js b/packages/react-components/deprecated/react-infobutton/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/deprecated/react-infobutton/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/deprecated/react-infobutton/cypress.config.ts b/packages/react-components/deprecated/react-infobutton/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/deprecated/react-infobutton/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/eslint-plugin-react-components/etc/eslint-plugin-react-components.api.md b/packages/react-components/eslint-plugin-react-components/etc/eslint-plugin-react-components.api.md index bc570c9853879..564dade860811 100644 --- a/packages/react-components/eslint-plugin-react-components/etc/eslint-plugin-react-components.api.md +++ b/packages/react-components/eslint-plugin-react-components/etc/eslint-plugin-react-components.api.md @@ -5,8 +5,8 @@ ```ts import type { ESLint } from 'eslint'; -import { RuleListener } from '@typescript-eslint/utils/dist/ts-eslint'; -import { RuleModule } from '@typescript-eslint/utils/dist/ts-eslint'; +import { RuleListener } from '@typescript-eslint/utils/ts-eslint'; +import { RuleModule } from '@typescript-eslint/utils/ts-eslint'; // @public (undocumented) export const configs: { @@ -16,7 +16,7 @@ export const configs: { }; 'flat/recommended': { plugins: { - [x: string]: ESLint.Plugin; + [pluginName: string]: ESLint.Plugin; }; rules: {}; }; @@ -28,10 +28,19 @@ export const meta: { version: string; }; +// @public +export interface RuleOptions { + rscUnsafeFunctions?: string[]; +} + // @public (undocumented) export const rules: { - "enforce-use-client": RuleModule<"missingUseClient" | "unnecessaryUseClient", [(RuleOptions | undefined)?], unknown, RuleListener>; - "prefer-fluentui-v9": RuleModule<"replaceFluent8With9" | "replaceIconWithJsx" | "replaceStackWithFlex" | "replaceFocusZoneWithTabster", {}[], unknown, RuleListener>; + "enforce-use-client": RuleModule<"missingUseClient" | "unnecessaryUseClient", [(RuleOptions | undefined)?], unknown, RuleListener> & { + name: string; + }; + "prefer-fluentui-v9": RuleModule<"replaceFluent8With9" | "replaceIconWithJsx" | "replaceStackWithFlex" | "replaceFocusZoneWithTabster", {}[], unknown, RuleListener> & { + name: string; + }; }; // (No @packageDocumentation comment for this package) diff --git a/packages/react-components/eslint-plugin-react-components/package.json b/packages/react-components/eslint-plugin-react-components/package.json index e3021bf034850..695c406a17af9 100644 --- a/packages/react-components/eslint-plugin-react-components/package.json +++ b/packages/react-components/eslint-plugin-react-components/package.json @@ -14,13 +14,13 @@ "eslint-plugin-eslint-plugin": "6.4.0" }, "dependencies": { - "@typescript-eslint/utils": "^8.46.2", + "@typescript-eslint/utils": "^8.64.0", "@swc/helpers": "^0.5.1" }, "peerDependencies": { "typescript-eslint": ">= 8.46.2", "eslint": ">= 8.0.0", - "typescript": ">= 5.0.0" + "typescript": ">=5.0.0 <7.0.0" }, "exports": { ".": { diff --git a/packages/react-components/eslint-plugin-react-components/src/index.ts b/packages/react-components/eslint-plugin-react-components/src/index.ts index b3884c69a70d6..caa7e27262dbb 100644 --- a/packages/react-components/eslint-plugin-react-components/src/index.ts +++ b/packages/react-components/eslint-plugin-react-components/src/index.ts @@ -1,9 +1,18 @@ import type { ESLint } from 'eslint'; import { name, version } from '../package.json'; -import { RULE_NAME as enforceUseClientName, rule as enforceUseClient } from './rules/enforce-use-client'; +import { + RULE_NAME as enforceUseClientName, + rule as enforceUseClient, + type RuleOptions, +} from './rules/enforce-use-client'; import { RULE_NAME as preferFluentUIV9Name, rule as preferFluentUIV9 } from './rules/prefer-fluentui-v9'; +// `RuleOptions` is part of the shipped `rules` signature, so it has to be imported statically and re-exported +// from the entry point, otherwise declaration emit references it through an inline `import()` type that +// API Extractor rolls up as an import of a module that is not published. +export type { RuleOptions }; + export const meta = { name, version, @@ -17,15 +26,21 @@ const recommendedRules = { // Add rules to the recommended config here in the future }; +// The index signature must be explicit: TypeScript >=6 preserves computed property names in declaration +// output, which would emit an unusable `[name]` key and a dangling `../package.json` import in `.d.ts`. +const flatRecommendedPlugins: { [pluginName: string]: ESLint.Plugin } = { + // Define plugins as an object to satisfy ESLint v9 flat config format + // the actual plugin will be assigned later to avoid circular dependencies + [name]: {} as ESLint.Plugin, +}; + export const configs = { recommended: { plugins: [name], rules: recommendedRules, }, 'flat/recommended': { - // Define plugins as an object to satisfy ESLint v9 flat config format - // the actual plugin will be assigned later to avoid circular dependencies - plugins: { [name]: {} as ESLint.Plugin }, + plugins: flatRecommendedPlugins, rules: recommendedRules, }, }; diff --git a/packages/react-components/global-context/cypress.config.js b/packages/react-components/global-context/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/global-context/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/global-context/cypress.config.ts b/packages/react-components/global-context/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/global-context/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-accordion/library/etc/react-accordion.api.md b/packages/react-components/react-accordion/library/etc/react-accordion.api.md index 22c878f6285c7..761f2771636c3 100644 --- a/packages/react-components/react-accordion/library/etc/react-accordion.api.md +++ b/packages/react-components/react-accordion/library/etc/react-accordion.api.md @@ -211,10 +211,10 @@ export const renderAccordionItem_unstable: (state: AccordionItemState, contextVa export const renderAccordionPanel_unstable: (state: AccordionPanelState) => JSXElement; // @public -export const useAccordion_unstable: (props: AccordionProps, ref: React_2.Ref) => AccordionState; +export const useAccordion_unstable: (props: AccordionProps, ref: React_2.Ref) => AccordionState; // @public -export const useAccordionBase_unstable: (props: AccordionBaseProps, ref: React_2.Ref) => AccordionBaseState; +export const useAccordionBase_unstable: (props: AccordionBaseProps, ref: React_2.Ref) => AccordionBaseState; // @public (undocumented) export const useAccordionContext_unstable: (selector: ContextSelector) => T; diff --git a/packages/react-components/react-aria/library/cypress.config.js b/packages/react-components/react-aria/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-aria/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-aria/library/cypress.config.ts b/packages/react-components/react-aria/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-aria/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-avatar/library/cypress.config.js b/packages/react-components/react-avatar/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-avatar/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-avatar/library/cypress.config.ts b/packages/react-components/react-avatar/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-avatar/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-breadcrumb/library/cypress.config.js b/packages/react-components/react-breadcrumb/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-breadcrumb/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-breadcrumb/library/cypress.config.ts b/packages/react-components/react-breadcrumb/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-breadcrumb/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md index 21b597d5eb78d..b0addf183a59a 100644 --- a/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md +++ b/packages/react-components/react-breadcrumb/library/etc/react-breadcrumb.api.md @@ -107,7 +107,7 @@ export type BreadcrumbProps = ComponentProps & { }; // @internal (undocumented) -export const BreadcrumbProvider: React_2.Provider> | undefined>; +export const BreadcrumbProvider: React_2.Provider; // @public (undocumented) export type BreadcrumbSlots = { diff --git a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/BreadcrumbContext.ts b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/BreadcrumbContext.ts index ea1b23456afca..d6b4ca48244b3 100644 --- a/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/BreadcrumbContext.ts +++ b/packages/react-components/react-breadcrumb/library/src/components/Breadcrumb/BreadcrumbContext.ts @@ -15,7 +15,7 @@ export const breadcrumbDefaultValue: BreadcrumbContextValues = { /** * @internal */ -export const BreadcrumbProvider = BreadcrumbContext.Provider; +export const BreadcrumbProvider: React.Provider = BreadcrumbContext.Provider; /** * @internal diff --git a/packages/react-components/react-calendar-compat/library/etc/react-calendar-compat.api.md b/packages/react-components/react-calendar-compat/library/etc/react-calendar-compat.api.md index de6ee0d363d40..4c29da30cf3ab 100644 --- a/packages/react-components/react-calendar-compat/library/etc/react-calendar-compat.api.md +++ b/packages/react-components/react-calendar-compat/library/etc/react-calendar-compat.api.md @@ -6,6 +6,7 @@ import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import type { SlotClassNames } from '@fluentui/react-utilities'; // @public @@ -219,7 +220,7 @@ export interface CalendarPickerStyles { } // @public (undocumented) -export interface CalendarProps extends React_2.RefAttributes { +export interface CalendarProps extends RefAttributes { allFocusable?: boolean; calendarDayProps?: Partial; calendarMonthProps?: Partial; diff --git a/packages/react-components/react-calendar-compat/library/src/components/Calendar/Calendar.types.ts b/packages/react-components/react-calendar-compat/library/src/components/Calendar/Calendar.types.ts index d9f3e02e44bd5..5dee024073013 100644 --- a/packages/react-components/react-calendar-compat/library/src/components/Calendar/Calendar.types.ts +++ b/packages/react-components/react-calendar-compat/library/src/components/Calendar/Calendar.types.ts @@ -1,4 +1,5 @@ import type * as React from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import type { CalendarStrings, DateFormatting, DateRangeType, DayOfWeek, FirstWeekOfYear } from '../../utils'; import type { CalendarDayProps } from '../CalendarDay/CalendarDay.types'; import type { CalendarMonthProps } from '../CalendarMonth/CalendarMonth.types'; @@ -9,7 +10,7 @@ export interface ICalendar { focus: () => void; } -export interface CalendarProps extends React.RefAttributes { +export interface CalendarProps extends RefAttributes { /** * Optional callback to access the ICalendar interface. Use this instead of ref for accessing * the public methods and properties of the component. diff --git a/packages/react-components/react-card/library/cypress.config.js b/packages/react-components/react-card/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-card/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-card/library/cypress.config.ts b/packages/react-components/react-card/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-card/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-carousel/library/cypress.config.js b/packages/react-components/react-carousel/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-carousel/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-carousel/library/cypress.config.ts b/packages/react-components/react-carousel/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-carousel/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-color-picker/library/cypress.config.js b/packages/react-components/react-color-picker/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-color-picker/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-color-picker/library/cypress.config.ts b/packages/react-components/react-color-picker/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-color-picker/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-combobox/library/cypress.config.js b/packages/react-components/react-combobox/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-combobox/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-combobox/library/cypress.config.ts b/packages/react-components/react-combobox/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-combobox/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-conformance-griffel/package.json b/packages/react-components/react-conformance-griffel/package.json index b9b8143a9a805..10d9fb13bb372 100644 --- a/packages/react-components/react-conformance-griffel/package.json +++ b/packages/react-components/react-conformance-griffel/package.json @@ -12,7 +12,7 @@ "peerDependencies": { "@types/react": ">=16.14.0 <20.0.0", "@types/react-dom": ">=16.9.0 <20.0.0", - "typescript": "^4.3.0", + "typescript": ">=4.3.0 <7.0.0", "@fluentui/react-conformance": "^0.20.1" }, "dependencies": { diff --git a/packages/react-components/react-context-selector/cypress.config.js b/packages/react-components/react-context-selector/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-context-selector/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-context-selector/cypress.config.ts b/packages/react-components/react-context-selector/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-context-selector/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-datepicker-compat/library/cypress.config.js b/packages/react-components/react-datepicker-compat/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-datepicker-compat/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-datepicker-compat/library/cypress.config.ts b/packages/react-components/react-datepicker-compat/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-datepicker-compat/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-dialog/library/cypress.config.js b/packages/react-components/react-dialog/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-dialog/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-dialog/library/cypress.config.ts b/packages/react-components/react-dialog/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-dialog/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-drawer/library/cypress.config.js b/packages/react-components/react-drawer/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-drawer/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-drawer/library/cypress.config.ts b/packages/react-components/react-drawer/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-drawer/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-field/library/etc/react-field.api.md b/packages/react-components/react-field/library/etc/react-field.api.md index db74981ff3ff0..208f831ba4b59 100644 --- a/packages/react-components/react-field/library/etc/react-field.api.md +++ b/packages/react-components/react-field/library/etc/react-field.api.md @@ -27,12 +27,7 @@ export type FieldBaseState = DistributiveOmit; // @public (undocumented) -export const FieldContextProvider: React_2.Provider & { - labelFor?: string; - labelId?: string; - validationMessageId?: string; - hintId?: string; -}> | undefined>; +export const FieldContextProvider: React_2.Provider; // @public (undocumented) export type FieldContextValue = Readonly & { diff --git a/packages/react-components/react-field/library/src/contexts/FieldContext.ts b/packages/react-components/react-field/library/src/contexts/FieldContext.ts index e1f0ce0860499..336736542a722 100644 --- a/packages/react-components/react-field/library/src/contexts/FieldContext.ts +++ b/packages/react-components/react-field/library/src/contexts/FieldContext.ts @@ -6,6 +6,6 @@ import type { FieldContextValue } from '../Field'; const FieldContext = React.createContext(undefined); -export const FieldContextProvider = FieldContext.Provider; +export const FieldContextProvider: React.Provider = FieldContext.Provider; export const useFieldContext_unstable = (): FieldContextValue | undefined => React.useContext(FieldContext); diff --git a/packages/react-components/react-headless-components-preview/library/cypress.config.js b/packages/react-components/react-headless-components-preview/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-headless-components-preview/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-headless-components-preview/library/cypress.config.ts b/packages/react-components/react-headless-components-preview/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-headless-components-preview/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-headless-components-preview/library/etc/provider.api.md b/packages/react-components/react-headless-components-preview/library/etc/provider.api.md index f4af9d2bdbe29..278fe28f9e9c9 100644 --- a/packages/react-components/react-headless-components-preview/library/etc/provider.api.md +++ b/packages/react-components/react-headless-components-preview/library/etc/provider.api.md @@ -7,14 +7,15 @@ import type { ComponentProps } from '@fluentui/react-utilities'; import type { ComponentState } from '@fluentui/react-utilities'; import type { FluentProviderContextValues } from '@fluentui/react-provider'; -import { FluentProviderProps } from '@fluentui/react-provider'; +import type { FluentProviderProps } from '@fluentui/react-provider'; import type { JSXElement } from '@fluentui/react-utilities'; import * as React_2 from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import type { Slot } from '@fluentui/react-utilities'; import { useFluent_unstable as useProviderContext } from '@fluentui/react-shared-contexts'; // @public -export const Provider: React_2.ForwardRefExoticComponent & React_2.FragmentProps & Pick & React_2.RefAttributes>; +export const Provider: React_2.ForwardRefExoticComponent>; // @public export type ProviderProps = ComponentProps & Pick; diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.cy.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.cy.tsx index b1a8a183cd68f..dbf45bc6e2fcb 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.cy.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Dialog/Dialog.cy.tsx @@ -186,7 +186,9 @@ describe('Dialog', () => { cy.get('#open-popover-btn').should('have.focus'); // Open Tooltip, wait for the tooltip to appear and then close it with Escape - cy.get(dialogTriggerCloseSelector).focus().wait(0).realType('{esc}'); + cy.get(dialogTriggerCloseSelector).focus(); + cy.get('[role="tooltip"]').should('be.visible'); + cy.focused().realType('{esc}'); cy.get(dialogSurfaceSelector).should('exist'); }); diff --git a/packages/react-components/react-headless-components-preview/library/src/components/Provider/Provider.tsx b/packages/react-components/react-headless-components-preview/library/src/components/Provider/Provider.tsx index 8582a6b9bafd8..767c4ebac09c6 100644 --- a/packages/react-components/react-headless-components-preview/library/src/components/Provider/Provider.tsx +++ b/packages/react-components/react-headless-components-preview/library/src/components/Provider/Provider.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import { renderProvider } from './renderProvider'; import { useProvider } from './useProvider'; @@ -10,11 +11,17 @@ import { useProviderContextValues } from './useProviderContextValues'; /** * Renders required context providers for Fluent Headless Components. */ -export const Provider = React.forwardRef((props, ref) => { - const state = useProvider(props, ref); - const contextValues = useProviderContextValues(state); +// NOTE: the type annotation is explicit so declaration emit does not reference `./Provider.types` through an +// inline `import('./Provider.types')` type, which api-extractor turns into a relative import in the `.d.ts` +// rollup (see https://github.com/microsoft/rushstack/issues/3335). +// It intentionally spells out the shape React already inferred here instead of using `ForwardRefComponent`: +// `ForwardRefComponent` adds the polymorphic `as` prop handling and would change the published API surface. +export const Provider: React.ForwardRefExoticComponent> = + React.forwardRef((props, ref) => { + const state = useProvider(props, ref); + const contextValues = useProviderContextValues(state); - return renderProvider(state, contextValues); -}); + return renderProvider(state, contextValues); + }); Provider.displayName = 'Provider'; diff --git a/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json b/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json index a24e9b9b7eca5..5978000d3df74 100644 --- a/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json +++ b/packages/react-components/react-headless-components-preview/stories/tsconfig.lib.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "lib": ["ES2022", "dom"], - "moduleResolution": "bundler", + "moduleResolution": "nodenext", "outDir": "../../../../dist/out-tsc", "inlineSources": true, "types": ["static-assets", "environment"] diff --git a/packages/react-components/react-infolabel/library/cypress.config.js b/packages/react-components/react-infolabel/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-infolabel/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-infolabel/library/cypress.config.ts b/packages/react-components/react-infolabel/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-infolabel/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-list/library/cypress.config.js b/packages/react-components/react-list/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-list/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-list/library/cypress.config.ts b/packages/react-components/react-list/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-list/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-menu-grid-preview/library/cypress.config.js b/packages/react-components/react-menu-grid-preview/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-menu-grid-preview/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-menu-grid-preview/library/cypress.config.ts b/packages/react-components/react-menu-grid-preview/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-menu-grid-preview/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-menu/library/cypress.config.js b/packages/react-components/react-menu/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-menu/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-menu/library/cypress.config.ts b/packages/react-components/react-menu/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-menu/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-message-bar/library/cypress.config.js b/packages/react-components/react-message-bar/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-message-bar/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-message-bar/library/cypress.config.ts b/packages/react-components/react-message-bar/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-message-bar/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-migration-v0-v9/stories/tsconfig.lib.json b/packages/react-components/react-migration-v0-v9/stories/tsconfig.lib.json index 368f7937f140f..dfce6aaa4f3cf 100644 --- a/packages/react-components/react-migration-v0-v9/stories/tsconfig.lib.json +++ b/packages/react-components/react-migration-v0-v9/stories/tsconfig.lib.json @@ -5,7 +5,6 @@ "outDir": "../../../../dist/out-tsc", "inlineSources": true, "types": ["static-assets", "environment"], - "baseUrl": ".", "paths": { "react": ["../../../../node_modules/@types/react/index.d.ts"] } diff --git a/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts b/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts index ead176e18262a..94a4e773042fb 100644 --- a/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts +++ b/packages/react-components/react-motion-components-preview/library/src/testing/testUtils.ts @@ -93,6 +93,7 @@ export const mockAnimation: () => Animation = () => ({ addEventListener: jest.fn(), dispatchEvent: jest.fn(), onremove: null, + overallProgress: null, pending: false, replaceState: 'active', commitStyles: jest.fn(), diff --git a/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx index aaa0c58b1d780..250d86d8c542b 100644 --- a/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx +++ b/packages/react-components/react-motion-components-preview/stories/src/Atoms/ComposingAtomsDemo.tsx @@ -142,10 +142,10 @@ createRoot(document.getElementById('root')!).render( compilerOptions: { target: 'ES2020', useDefineForClassFields: true, - lib: ['ES2020', 'DOM', 'DOM.Iterable'], + lib: ['ES2020', 'DOM'], module: 'ESNext', skipLibCheck: true, - moduleResolution: 'node', + moduleResolution: 'bundler', resolveJsonModule: true, isolatedModules: true, noEmit: true, diff --git a/packages/react-components/react-motion/library/cypress.config.js b/packages/react-components/react-motion/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-motion/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-motion/library/cypress.config.ts b/packages/react-components/react-motion/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-motion/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-overflow/library/cypress.config.js b/packages/react-components/react-overflow/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-overflow/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-overflow/library/cypress.config.ts b/packages/react-components/react-overflow/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-overflow/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-overflow/library/etc/react-overflow.api.md b/packages/react-components/react-overflow/library/etc/react-overflow.api.md index 8d1e3ff9a0baf..0739044aed889 100644 --- a/packages/react-components/react-overflow/library/etc/react-overflow.api.md +++ b/packages/react-components/react-overflow/library/etc/react-overflow.api.md @@ -10,9 +10,10 @@ import type { OverflowDividerEntry } from '@fluentui/priority-overflow'; import { OverflowEventPayload } from '@fluentui/priority-overflow'; import type { OverflowGroupState } from '@fluentui/priority-overflow'; import type { OverflowItemEntry } from '@fluentui/priority-overflow'; -import { OverflowOptions } from '@fluentui/priority-overflow'; +import type { OverflowOptions } from '@fluentui/priority-overflow'; import type { OverflowSnapshot } from '@fluentui/priority-overflow'; import * as React_2 from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; // @public (undocumented) export const DATA_OVERFLOW_DIVIDER = "data-overflow-divider"; @@ -35,10 +36,7 @@ export { OnUpdateItemVisibility } export { OnUpdateOverflow } // @public -export const Overflow: React_2.ForwardRefExoticComponent> & { - children: React_2.ReactElement; - onOverflowChange?: (ev: null, data: OverflowState) => void; -} & React_2.RefAttributes>; +export const Overflow: React_2.ForwardRefExoticComponent>; // @public export type OverflowComponentState = UseOverflowContainerReturn & { diff --git a/packages/react-components/react-overflow/library/src/components/Overflow/Overflow.tsx b/packages/react-components/react-overflow/library/src/components/Overflow/Overflow.tsx index 45f62194ba47d..c6c48675a5a31 100644 --- a/packages/react-components/react-overflow/library/src/components/Overflow/Overflow.tsx +++ b/packages/react-components/react-overflow/library/src/components/Overflow/Overflow.tsx @@ -1,6 +1,7 @@ 'use client'; import * as React from 'react'; +import type { RefAttributes } from '@fluentui/react-utilities'; import type { OverflowProps } from './Overflow.types'; import { useOverflow_unstable } from './useOverflow'; import { useOverflowContextValues_unstable } from '../../useOverflowContextValues'; @@ -10,11 +11,18 @@ import { renderOverflow_unstable } from './renderOverflow'; /** * Provides an OverflowContext for OverflowItem descendants. */ -export const Overflow = React.forwardRef((props: OverflowProps, ref) => { - const state = useOverflow_unstable(props, ref as React.Ref); - const contextValues = useOverflowContextValues_unstable(state); +// NOTE: the type annotation is explicit so declaration emit does not reference `./Overflow.types` through an +// inline `import('./Overflow.types')` type, which api-extractor turns into a relative import in the `.d.ts` +// rollup (see https://github.com/microsoft/rushstack/issues/3335). +// It intentionally spells out the shape React already inferred here instead of using `ForwardRefComponent`: +// `ForwardRefComponent` adds the polymorphic `as` prop handling and would change the published API surface. +export const Overflow: React.ForwardRefExoticComponent> = React.forwardRef( + (props: OverflowProps, ref) => { + const state = useOverflow_unstable(props, ref as React.Ref); + const contextValues = useOverflowContextValues_unstable(state); - useOverflowStyles_unstable(state); + useOverflowStyles_unstable(state); - return renderOverflow_unstable(state, contextValues); -}); + return renderOverflow_unstable(state, contextValues); + }, +); diff --git a/packages/react-components/react-popover/library/cypress.config.js b/packages/react-components/react-popover/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-popover/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-popover/library/cypress.config.ts b/packages/react-components/react-popover/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-popover/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-portal-compat-context/tsconfig.json b/packages/react-components/react-portal-compat-context/tsconfig.json index 4c7d6354c45f9..b0a33e059c75b 100644 --- a/packages/react-components/react-portal-compat-context/tsconfig.json +++ b/packages/react-components/react-portal-compat-context/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../../tsconfig.base.json", "compilerOptions": { - "target": "ES5", + "target": "ES2015", "lib": ["ES5", "ES2015.Iterable", "dom"], "noEmit": true, "isolatedModules": true, diff --git a/packages/react-components/react-portal-compat/cypress.config.js b/packages/react-components/react-portal-compat/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-portal-compat/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-portal-compat/cypress.config.ts b/packages/react-components/react-portal-compat/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-portal-compat/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-positioning/library/cypress.config.js b/packages/react-components/react-positioning/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-positioning/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-positioning/library/cypress.config.ts b/packages/react-components/react-positioning/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-positioning/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.spec.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.spec.ts index ad3a68f998465..d614f9e5c2844 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.spec.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.spec.ts @@ -32,7 +32,7 @@ describe(`sabdbox-scaffold`, () => { \\"devDependencies\\": { \\"@types/react\\": \\"^17\\", \\"@types/react-dom\\": \\"^17\\", - \\"typescript\\": \\"~4.7.0\\", + \\"typescript\\": \\"~6.0.0\\", \\"react-scripts\\": \\"^5.0.0\\", \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" }, @@ -149,7 +149,7 @@ describe(`sabdbox-scaffold`, () => { \\"devDependencies\\": { \\"@types/react\\": \\"^17\\", \\"@types/react-dom\\": \\"^17\\", - \\"typescript\\": \\"~4.7.0\\", + \\"typescript\\": \\"~6.0.0\\", \\"react-scripts\\": \\"^5.0.0\\", \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" }, @@ -232,7 +232,7 @@ describe(`sabdbox-scaffold`, () => { \\"devDependencies\\": { \\"@types/react\\": \\"^17\\", \\"@types/react-dom\\": \\"^17\\", - \\"typescript\\": \\"~4.7.0\\", + \\"typescript\\": \\"~6.0.0\\", \\"react-scripts\\": \\"^5.0.0\\", \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" }, @@ -383,7 +383,7 @@ describe(`sabdbox-scaffold`, () => { \\"devDependencies\\": { \\"@types/react\\": \\"^17\\", \\"@types/react-dom\\": \\"^17\\", - \\"typescript\\": \\"~4.7.0\\", + \\"typescript\\": \\"~6.0.0\\", \\"@vitejs/plugin-react\\": \\"^4.2.0\\", \\"vite\\": \\"^5.0.0\\" } @@ -425,12 +425,11 @@ describe(`sabdbox-scaffold`, () => { \\"useDefineForClassFields\\": true, \\"lib\\": [ \\"ES2020\\", - \\"DOM\\", - \\"DOM.Iterable\\" + \\"DOM\\" ], \\"module\\": \\"ESNext\\", \\"skipLibCheck\\": true, - \\"moduleResolution\\": \\"node\\", + \\"moduleResolution\\": \\"bundler\\", \\"allowImportingTsExtensions\\": true, \\"resolveJsonModule\\": true, \\"isolatedModules\\": true, @@ -510,7 +509,7 @@ describe(`sabdbox-scaffold`, () => { \\"devDependencies\\": { \\"@types/react\\": \\"^17\\", \\"@types/react-dom\\": \\"^17\\", - \\"typescript\\": \\"~4.7.0\\", + \\"typescript\\": \\"~6.0.0\\", \\"@vitejs/plugin-react\\": \\"^4.2.0\\", \\"vite\\": \\"^5.0.0\\" } @@ -552,12 +551,11 @@ describe(`sabdbox-scaffold`, () => { \\"useDefineForClassFields\\": true, \\"lib\\": [ \\"ES2020\\", - \\"DOM\\", - \\"DOM.Iterable\\" + \\"DOM\\" ], \\"module\\": \\"ESNext\\", \\"skipLibCheck\\": true, - \\"moduleResolution\\": \\"node\\", + \\"moduleResolution\\": \\"bundler\\", \\"allowImportingTsExtensions\\": true, \\"resolveJsonModule\\": true, \\"isolatedModules\\": true, diff --git a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.ts b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.ts index 5b0dfb2cffabc..d62896dce58c6 100644 --- a/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.ts +++ b/packages/react-components/react-storybook-addon-export-to-sandbox/src/sandbox-scaffold.ts @@ -4,7 +4,7 @@ import type { SandboxContext } from './public-types'; import type { Data } from './sandbox-utils'; import { serializeJson } from './utils'; -const commonDevDeps = { '@types/react': '^17', '@types/react-dom': '^17', typescript: '~4.7.0' }; +const commonDevDeps = { '@types/react': '^17', '@types/react-dom': '^17', typescript: '~6.0.0' }; export const scaffold = { vite: (data: Data): Record => { @@ -160,13 +160,12 @@ const Vite = { compilerOptions: { target: 'ES2020', useDefineForClassFields: true, - lib: ['ES2020', 'DOM', 'DOM.Iterable'], + lib: ['ES2020', 'DOM'], module: 'ESNext', skipLibCheck: true, /* Bundler mode */ - moduleResolution: 'node', - // moduleResolution: 'bundler', + moduleResolution: 'bundler', allowImportingTsExtensions: true, resolveJsonModule: true, isolatedModules: true, diff --git a/packages/react-components/react-swatch-picker/library/cypress.config.js b/packages/react-components/react-swatch-picker/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-swatch-picker/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-swatch-picker/library/cypress.config.ts b/packages/react-components/react-swatch-picker/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-swatch-picker/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-table/library/cypress.config.js b/packages/react-components/react-table/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-table/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-table/library/cypress.config.ts b/packages/react-components/react-table/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-table/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-tabster/cypress.config.js b/packages/react-components/react-tabster/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-tabster/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-tabster/cypress.config.ts b/packages/react-components/react-tabster/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-tabster/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-tag-picker/library/cypress.config.js b/packages/react-components/react-tag-picker/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-tag-picker/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-tag-picker/library/cypress.config.ts b/packages/react-components/react-tag-picker/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-tag-picker/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-tag-picker/library/src/utils/tagPicker2Tag.ts b/packages/react-components/react-tag-picker/library/src/utils/tagPicker2Tag.ts index a2a9baa776dff..f91a538f3b095 100644 --- a/packages/react-components/react-tag-picker/library/src/utils/tagPicker2Tag.ts +++ b/packages/react-components/react-tag-picker/library/src/utils/tagPicker2Tag.ts @@ -1,4 +1,4 @@ -import type { TagAppearance, TagSize } from '@fluentui/react-tags/src/index'; +import type { TagAppearance, TagSize } from '@fluentui/react-tags'; import type { TagPickerSize } from '../TagPicker'; import type { ComboboxBaseProps } from '@fluentui/react-combobox'; diff --git a/packages/react-components/react-tags/library/cypress.config.js b/packages/react-components/react-tags/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-tags/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-tags/library/cypress.config.ts b/packages/react-components/react-tags/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-tags/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-teaching-popover/library/cypress.config.js b/packages/react-components/react-teaching-popover/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-teaching-popover/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-teaching-popover/library/cypress.config.ts b/packages/react-components/react-teaching-popover/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-teaching-popover/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-timepicker-compat/library/cypress.config.js b/packages/react-components/react-timepicker-compat/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-timepicker-compat/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-timepicker-compat/library/cypress.config.ts b/packages/react-components/react-timepicker-compat/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-timepicker-compat/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-toast/library/cypress.config.js b/packages/react-components/react-toast/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-toast/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-toast/library/cypress.config.ts b/packages/react-components/react-toast/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-toast/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-toolbar/library/cypress.config.js b/packages/react-components/react-toolbar/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-toolbar/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-toolbar/library/cypress.config.ts b/packages/react-components/react-toolbar/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-toolbar/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-tree/library/cypress.config.js b/packages/react-components/react-tree/library/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-tree/library/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-tree/library/cypress.config.ts b/packages/react-components/react-tree/library/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-tree/library/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-components/react-utilities/cypress.config.js b/packages/react-components/react-utilities/cypress.config.js new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/packages/react-components/react-utilities/cypress.config.js @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/packages/react-components/react-utilities/cypress.config.ts b/packages/react-components/react-utilities/cypress.config.ts deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/packages/react-components/react-utilities/cypress.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/packages/react-conformance/package.json b/packages/react-conformance/package.json index 709167be0f16c..9bbd4bae76708 100644 --- a/packages/react-conformance/package.json +++ b/packages/react-conformance/package.json @@ -29,7 +29,7 @@ "jest": "^26.0.0", "react": ">=16.8.0 <19.0.0", "react-dom": ">=16.8.0 <19.0.0", - "typescript": "^4.3.0" + "typescript": ">=4.3.0 <7.0.0" }, "exports": { ".": { diff --git a/packages/react-date-time/project.json b/packages/react-date-time/project.json index 34b3634c4a1ca..903289a89162b 100644 --- a/packages/react-date-time/project.json +++ b/packages/react-date-time/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-date-time/src", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/react-date-time/tsconfig.json b/packages/react-date-time/tsconfig.json index 37795cab10a56..a32d70c07e2af 100644 --- a/packages/react-date-time/tsconfig.json +++ b/packages/react-date-time/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-docsite-components/project.json b/packages/react-docsite-components/project.json index 8cf778c0ec428..5c38dd5d91cfd 100644 --- a/packages/react-docsite-components/project.json +++ b/packages/react-docsite-components/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-docsite-components/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/react-docsite-components/tsconfig.json b/packages/react-docsite-components/tsconfig.json index 9f668c7b5ca3a..0db57eb19bb6a 100644 --- a/packages/react-docsite-components/tsconfig.json +++ b/packages/react-docsite-components/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -9,15 +10,24 @@ "importHelpers": true, "noImplicitAny": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "noImplicitThis": true, "outDir": "lib", "lib": ["es5", "dom", "es2015.promise", "ES2015.Symbol.WellKnown"], "types": ["webpack-env", "jest"], - "isolatedModules": true + "isolatedModules": true, + // `markdown-to-jsx` only publishes the `MarkdownToJSX` namespace behind its `import` export condition; + // its `require` declarations (`dist/index.cjs.d.ts`) omit the namespace re-export entirely. Map the + // specifier directly at its `import`-condition declaration file instead of flipping `customConditions`, + // which would resolve every other dependency's types through the `import` condition too. + "paths": { + "markdown-to-jsx": ["../../node_modules/markdown-to-jsx/dist/index.d.ts"] + } }, "include": ["src"] } diff --git a/packages/react-examples/cypress.config.js b/packages/react-examples/cypress.config.js new file mode 100644 index 0000000000000..5b9b29b32027e --- /dev/null +++ b/packages/react-examples/cypress.config.js @@ -0,0 +1,33 @@ +// @ts-check + +const { baseConfig, baseWebpackConfig } = require('@fluentui/scripts-cypress'); +const { createStorybookWebpackConfig } = require('@fluentui/scripts-webpack'); + +/** @type {import('@fluentui/scripts-cypress').BaseConfig} */ +const config = { ...baseConfig }; +const v8webpackConfig = createStorybookWebpackConfig(baseWebpackConfig); + +// we need to remove scripts-cypress from aliases as we wanna keep node_modules resolution to make +// browser path work for `import { mount } from '@fluentui/scripts-cypress';` +config.component.devServer.webpackConfig = removeAliases(v8webpackConfig, [ + '@fluentui/scripts-cypress/', + '@fluentui/scripts-cypress$', +]); + +module.exports = config; + +/** + * @param {typeof v8webpackConfig} webpackConfig + * @param {string[]} aliases + */ +function removeAliases(webpackConfig, aliases) { + const alias = webpackConfig?.resolve?.alias ?? {}; + + for (const key of Object.keys(alias)) { + if (aliases.includes(key)) { + delete (/** @type {Record} */ (alias)[key]); + } + } + + return webpackConfig; +} diff --git a/packages/react-examples/cypress.config.ts b/packages/react-examples/cypress.config.ts deleted file mode 100644 index 60ca645be5842..0000000000000 --- a/packages/react-examples/cypress.config.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { baseConfig, baseWebpackConfig } from '@fluentui/scripts-cypress'; -import { createStorybookWebpackConfig } from '@fluentui/scripts-webpack'; - -const config = { ...baseConfig }; -const v8webpackConfig = createStorybookWebpackConfig(baseWebpackConfig); - -// we need to remove scripts-cypress from aliases as we wanna keep node_modules resolution to make browser path work for `import { mount } from '@fluentui/scripts-cypress';` -config.component.devServer.webpackConfig = removeAliases(v8webpackConfig, [ - '@fluentui/scripts-cypress/', - '@fluentui/scripts-cypress$', -]); - -export default config; - -function removeAliases(webpackConfig: typeof v8webpackConfig, aliases: string[]) { - const alias = webpackConfig?.resolve?.alias ?? {}; - - for (const key of Object.keys(alias)) { - if (aliases.includes(key)) { - delete (alias as Record)[key]; - } - } - - return webpackConfig; -} diff --git a/packages/react-examples/eslint.config.js b/packages/react-examples/eslint.config.js index 75fe5c99d1763..dd5f529392761 100644 --- a/packages/react-examples/eslint.config.js +++ b/packages/react-examples/eslint.config.js @@ -10,10 +10,17 @@ module.exports = [ 'no-alert': 'off', 'no-restricted-globals': 'off', '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-deprecated': 'warn', '@typescript-eslint/explicit-module-boundary-types': 'off', }, }, + { + // type aware rules are only wired up for TypeScript sources - `cypress.config.js` and friends + // are plain JavaScript and have no `parserOptions.project` + files: ['**/*.{ts,tsx}'], + rules: { + '@typescript-eslint/no-deprecated': 'warn', + }, + }, { files: ['**/*.Example.{ts,tsx}'], rules: { diff --git a/packages/react-examples/project.json b/packages/react-examples/project.json index 2a5e2379d53f6..5c093211cd790 100644 --- a/packages/react-examples/project.json +++ b/packages/react-examples/project.json @@ -4,7 +4,7 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/react-examples/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "targets": { "type-check": { "command": "echo 'type-check is noop for v8 project which uses tsc for build'" } } diff --git a/packages/react-examples/tsconfig.json b/packages/react-examples/tsconfig.json index fe13d94ef3091..464c9bdfa4de5 100644 --- a/packages/react-examples/tsconfig.json +++ b/packages/react-examples/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "lib": ["es5", "dom", "es2015.promise"], diff --git a/packages/react-examples/tsconfig.rit.json b/packages/react-examples/tsconfig.rit.json index b4901f354fe45..f5a4161ce7261 100644 --- a/packages/react-examples/tsconfig.rit.json +++ b/packages/react-examples/tsconfig.rit.json @@ -1,13 +1,15 @@ { "compilerOptions": { "target": "ES2019", - "module": "esnext", + "module": "nodenext", "strictFunctionTypes": false, "strictPropertyInitialization": false, "noEmit": true, "jsx": "react", "experimentalDecorators": true, - "moduleResolution": "node", + "strict": false, + "moduleResolution": "nodenext", + "resolvePackageJsonExports": false, "isolatedModules": true }, "exclude": ["node_modules"], diff --git a/packages/react-experiments/project.json b/packages/react-experiments/project.json index c21e94f746a02..f9168c9f2f089 100644 --- a/packages/react-experiments/project.json +++ b/packages/react-experiments/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-experiments/src", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/react-experiments/tsconfig.json b/packages/react-experiments/tsconfig.json index ebcf0612b406a..3ddf4a4d7b13b 100644 --- a/packages/react-experiments/tsconfig.json +++ b/packages/react-experiments/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "lib": ["es5", "dom", "ES2016.Array.Include", "ES2015.Core"], diff --git a/packages/react-file-type-icons/project.json b/packages/react-file-type-icons/project.json index 49d600378aadd..2ab33574597cb 100644 --- a/packages/react-file-type-icons/project.json +++ b/packages/react-file-type-icons/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-file-type-icons/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/react-file-type-icons/tsconfig.json b/packages/react-file-type-icons/tsconfig.json index 178315f897be5..6d5c753f15e65 100644 --- a/packages/react-file-type-icons/tsconfig.json +++ b/packages/react-file-type-icons/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "module": "commonjs", "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "jsx": "react", @@ -9,7 +10,8 @@ "experimentalDecorators": true, "importHelpers": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "typeRoots": ["../../node_modules/@types", "../../typings"], "types": ["jest", "custom-global"], diff --git a/packages/react-focus/project.json b/packages/react-focus/project.json index 630237635930e..4e01c11473339 100644 --- a/packages/react-focus/project.json +++ b/packages/react-focus/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-focus/src", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/react-focus/tsconfig.json b/packages/react-focus/tsconfig.json index 5a58d52709ecc..737b2948c9c67 100644 --- a/packages/react-focus/tsconfig.json +++ b/packages/react-focus/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es2015", "dom", "es2015.promise"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-hooks/project.json b/packages/react-hooks/project.json index b61f4b9c02e82..70ffafcf5bacc 100644 --- a/packages/react-hooks/project.json +++ b/packages/react-hooks/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react-hooks/src", - "tags": ["v8", "ships-bundle"], + "tags": ["v8", "ships-bundle", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/react-hooks/tsconfig.json b/packages/react-hooks/tsconfig.json index 37795cab10a56..a32d70c07e2af 100644 --- a/packages/react-hooks/tsconfig.json +++ b/packages/react-hooks/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-icon-provider/tsconfig.json b/packages/react-icon-provider/tsconfig.json index 9492dae7675b5..8cf7420e8563e 100644 --- a/packages/react-icon-provider/tsconfig.json +++ b/packages/react-icon-provider/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", "target": "ES2019", - "module": "CommonJS", + "module": "commonjs", "jsx": "react", "declaration": true, "sourceMap": true, @@ -11,7 +11,8 @@ "importHelpers": true, "noUnusedLocals": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["ES2019", "DOM"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-icons-mdl2-branded/tsconfig.json b/packages/react-icons-mdl2-branded/tsconfig.json index afe224ba9d157..0bfd193baadfe 100644 --- a/packages/react-icons-mdl2-branded/tsconfig.json +++ b/packages/react-icons-mdl2-branded/tsconfig.json @@ -1,9 +1,9 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", "target": "ES2019", - "module": "CommonJS", + "module": "commonjs", "jsx": "react", "declaration": true, "sourceMap": true, @@ -11,7 +11,8 @@ "importHelpers": true, "noUnusedLocals": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["ES2019", "DOM"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react-icons-mdl2/tsconfig.json b/packages/react-icons-mdl2/tsconfig.json index c2dd2c6f6dd95..849e2b1b445d8 100644 --- a/packages/react-icons-mdl2/tsconfig.json +++ b/packages/react-icons-mdl2/tsconfig.json @@ -1,18 +1,20 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", "target": "ES2019", - "module": "CommonJS", + "module": "commonjs", "jsx": "react", "declaration": true, "sourceMap": true, "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["ES2019", "DOM"], "types": ["jest", "node"], diff --git a/packages/react-monaco-editor/project.json b/packages/react-monaco-editor/project.json index e14cb3cf70517..02b7f03910c75 100644 --- a/packages/react-monaco-editor/project.json +++ b/packages/react-monaco-editor/project.json @@ -2,7 +2,7 @@ "name": "react-monaco-editor", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/react-monaco-editor/tsconfig.json b/packages/react-monaco-editor/tsconfig.json index 4ac19d8ecde65..cd6d339ba70f2 100644 --- a/packages/react-monaco-editor/tsconfig.json +++ b/packages/react-monaco-editor/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "esModuleInterop": true, "preserveConstEnums": true, "lib": ["es5", "dom", "es2015.promise", "es2015.iterable", "ES2015.Symbol.WellKnown"], diff --git a/packages/react-window-provider/project.json b/packages/react-window-provider/project.json index 26405f46526e6..007a0b86ad003 100644 --- a/packages/react-window-provider/project.json +++ b/packages/react-window-provider/project.json @@ -2,6 +2,6 @@ "name": "react-window-provider", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [] } diff --git a/packages/react-window-provider/tsconfig.json b/packages/react-window-provider/tsconfig.json index ad11212959695..4d880074862be 100644 --- a/packages/react-window-provider/tsconfig.json +++ b/packages/react-window-provider/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -12,7 +12,8 @@ "noUnusedLocals": true, "strict": true, "isolatedModules": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es2015", "dom", "es2015.promise"], "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/react/project.json b/packages/react/project.json index 05a6f12671d65..675e7972fe201 100644 --- a/packages/react/project.json +++ b/packages/react/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/react/src", - "tags": ["v8", "ships-bundle", "ships-umd"], + "tags": ["v8", "ships-bundle", "ships-umd", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/react/tsconfig.json b/packages/react/tsconfig.json index ebd52a0fd9035..a02c9c26778bd 100644 --- a/packages/react/tsconfig.json +++ b/packages/react/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "lib", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "isolatedModules": true, @@ -11,9 +11,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es5", "dom", "es2015.promise", "ES2015.Symbol.WellKnown"], "skipLibCheck": true, diff --git a/packages/scheme-utilities/project.json b/packages/scheme-utilities/project.json index 10cfb8bdf072d..3af2cdbf6b730 100644 --- a/packages/scheme-utilities/project.json +++ b/packages/scheme-utilities/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/scheme-utilities/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/scheme-utilities/tsconfig.json b/packages/scheme-utilities/tsconfig.json index 75e1e34acad9a..e72037055014f 100644 --- a/packages/scheme-utilities/tsconfig.json +++ b/packages/scheme-utilities/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es5", "dom"], @@ -11,7 +12,8 @@ "importHelpers": true, "strict": true, "skipLibCheck": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "isolatedModules": true }, diff --git a/packages/set-version/project.json b/packages/set-version/project.json index fe6796dd5c850..85e5d42541172 100644 --- a/packages/set-version/project.json +++ b/packages/set-version/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/set-version/src", - "tags": ["v8"] + "tags": ["v8", "ships-es5"] } diff --git a/packages/set-version/tsconfig.json b/packages/set-version/tsconfig.json index 63cd60b9a95fa..c4e37d92e1232 100644 --- a/packages/set-version/tsconfig.json +++ b/packages/set-version/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["ES5", "ES2015.Collection", "ES2015.Iterable", "ES2015.Symbol.WellKnown", "dom"], @@ -8,8 +9,10 @@ "sourceMap": true, "importHelpers": true, "noImplicitAny": true, + "strict": false, "strictNullChecks": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "isolatedModules": true, "types": ["jest"] }, diff --git a/packages/storybook/tsconfig.json b/packages/storybook/tsconfig.json index 04dcd3aad13dc..f43e7201c7150 100644 --- a/packages/storybook/tsconfig.json +++ b/packages/storybook/tsconfig.json @@ -1,18 +1,20 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "lib", "target": "es6", - "module": "es6", + "module": "commonjs", "jsx": "react", "declaration": true, "sourceMap": true, "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es6", "dom"], "types": [], diff --git a/packages/style-utilities/project.json b/packages/style-utilities/project.json index 885ea0b21f0fd..4df1d51036d0c 100644 --- a/packages/style-utilities/project.json +++ b/packages/style-utilities/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/style-utilities/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "implicitDependencies": [], "targets": { "test": { diff --git a/packages/style-utilities/tsconfig.json b/packages/style-utilities/tsconfig.json index 42f0329c8451c..2f6da9bc218e0 100644 --- a/packages/style-utilities/tsconfig.json +++ b/packages/style-utilities/tsconfig.json @@ -1,7 +1,7 @@ { "compilerOptions": { - "baseUrl": ".", - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], @@ -13,8 +13,10 @@ "importHelpers": true, "noImplicitAny": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "typeRoots": ["../../node_modules/@types", "../../typings"], "types": ["jest", "custom-global"] diff --git a/packages/test-utilities/project.json b/packages/test-utilities/project.json index 37e012a702064..176dd1e670df0 100644 --- a/packages/test-utilities/project.json +++ b/packages/test-utilities/project.json @@ -3,6 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "implicitDependencies": [], - "tags": ["v8", "platform:node"], + "tags": ["v8", "platform:node", "ships-es5"], "sourceRoot": "packages/test-utilities/src" } diff --git a/packages/test-utilities/tsconfig.json b/packages/test-utilities/tsconfig.json index dccc61e40499c..ba9aea6ab8043 100644 --- a/packages/test-utilities/tsconfig.json +++ b/packages/test-utilities/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -11,9 +11,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "lib": ["es2017", "dom"], "types": ["jest"], diff --git a/packages/theme-samples/project.json b/packages/theme-samples/project.json index bc0ffecdde63a..254570bbceb64 100644 --- a/packages/theme-samples/project.json +++ b/packages/theme-samples/project.json @@ -4,5 +4,5 @@ "projectType": "library", "implicitDependencies": [], "sourceRoot": "packages/theme-samples/src", - "tags": ["v8", "ships-bundle"] + "tags": ["v8", "ships-bundle", "ships-es5"] } diff --git a/packages/theme-samples/tsconfig.json b/packages/theme-samples/tsconfig.json index 8182e77b0c330..7bee94da993bc 100644 --- a/packages/theme-samples/tsconfig.json +++ b/packages/theme-samples/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -10,9 +10,11 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "skipLibCheck": true, "preserveConstEnums": true, "lib": ["es5", "dom"], diff --git a/packages/theme/project.json b/packages/theme/project.json index 2ea07047b2efe..7a2468cd0384e 100644 --- a/packages/theme/project.json +++ b/packages/theme/project.json @@ -2,7 +2,7 @@ "name": "theme", "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/theme/tsconfig.json b/packages/theme/tsconfig.json index bf7d01cbeee1b..cac4977cd7937 100644 --- a/packages/theme/tsconfig.json +++ b/packages/theme/tsconfig.json @@ -1,8 +1,8 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "dist", - "target": "es5", + "target": "es2015", "module": "commonjs", "jsx": "react", "declaration": true, @@ -11,7 +11,8 @@ "importHelpers": true, "noUnusedLocals": true, "strict": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "isolatedModules": true, "preserveConstEnums": true, "lib": ["es5", "dom", "ES2015.Iterable", "ES2015.Symbol.WellKnown"], diff --git a/packages/utilities/project.json b/packages/utilities/project.json index 5cc21d3db1029..5439c8a72bad7 100644 --- a/packages/utilities/project.json +++ b/packages/utilities/project.json @@ -3,7 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "sourceRoot": "packages/utilities/src", - "tags": ["v8"], + "tags": ["v8", "ships-es5"], "targets": { "test": { "dependsOn": ["^build"] diff --git a/packages/utilities/src/EventGroup.ts b/packages/utilities/src/EventGroup.ts index 58ef83a515f9d..97257ef92faa0 100644 --- a/packages/utilities/src/EventGroup.ts +++ b/packages/utilities/src/EventGroup.ts @@ -83,7 +83,9 @@ export class EventGroup { const theDoc = doc ?? getDocument()!; if (EventGroup._isElement(target)) { + // eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy event creation kept for behavior parity if (typeof theDoc !== 'undefined' && theDoc.createEvent) { + // eslint-disable-next-line @typescript-eslint/no-deprecated -- legacy event creation kept for behavior parity let ev = theDoc.createEvent('HTMLEvents'); // eslint-disable-next-line @typescript-eslint/no-deprecated diff --git a/packages/utilities/tsconfig.json b/packages/utilities/tsconfig.json index b2666ed799656..9a801b122458d 100644 --- a/packages/utilities/tsconfig.json +++ b/packages/utilities/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es5", "es2015.promise", "dom"], @@ -13,7 +14,8 @@ "experimentalDecorators": true, "importHelpers": true, "noUnusedLocals": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "typeRoots": ["../../node_modules/@types", "../../typings"], diff --git a/packages/web-components/.storybook/tsconfig.json b/packages/web-components/.storybook/tsconfig.json index c1ee4022c8c6e..62d4ec9800efc 100644 --- a/packages/web-components/.storybook/tsconfig.json +++ b/packages/web-components/.storybook/tsconfig.json @@ -6,7 +6,7 @@ "allowJs": true, "checkJs": true, "noEmit": true, - "types": ["node"] + "types": ["node", "static-assets"] }, "include": ["*", "./*.d.ts", "../public", "../src/**/*.stories.*"] } diff --git a/packages/web-components/scripts/compile.js b/packages/web-components/scripts/compile.js index 162ff7e79a898..7dda77f59b0f7 100644 --- a/packages/web-components/scripts/compile.js +++ b/packages/web-components/scripts/compile.js @@ -6,6 +6,8 @@ import { dirname, join } from 'node:path'; import chalk from 'chalk'; +import { createTsConfigWithoutPathAliases } from './tsconfig-utils.js'; + const SRC = 'src'; const OUT = 'dist/esm'; @@ -33,7 +35,12 @@ async function compile() { execSync(`node ./scripts/generate-tokens`, { stdio: 'inherit' }); console.log(chalk.blueBright(`compile: running tsc`)); - execSync(`tsc -p tsconfig.lib.json --rootDir ./src --baseUrl .`, { stdio: 'inherit' }); + const noPathAliasesConfig = createTsConfigWithoutPathAliases('tsconfig.lib.json', 'compile'); + try { + execSync(`tsc -p ${noPathAliasesConfig.path} --rootDir ./src`, { stdio: 'inherit' }); + } finally { + noPathAliasesConfig.cleanup(); + } console.log(chalk.blueBright(`compile: copying SSR assets`)); await copySsrAssets(); diff --git a/packages/web-components/scripts/generate-ssr.js b/packages/web-components/scripts/generate-ssr.js index 2c11052c6565b..a23b5b73a1a69 100644 --- a/packages/web-components/scripts/generate-ssr.js +++ b/packages/web-components/scripts/generate-ssr.js @@ -38,6 +38,8 @@ import prettier from 'prettier'; import { generateStylesheets } from '@microsoft/fast-test-harness/build/generate-stylesheets.js'; import { generateFTemplates } from '@microsoft/fast-test-harness/build/generate-templates.js'; +import { createTsConfigWithoutPathAliases } from './tsconfig-utils.js'; + const cwd = process.cwd(); const TEMP_PARENT = join(cwd, 'temp'); const checkMode = process.argv.includes('--check'); @@ -55,9 +57,14 @@ async function main() { console.log(chalk.bold(`🎬 ${label} start`)); console.log(chalk.blueBright(`${label}: compiling src → ${tempDir}`)); - execSync(`tsc -p tsconfig.lib.json --rootDir ./src --baseUrl . --outDir ${tempDir} --declaration false`, { - stdio: 'inherit', - }); + const noPathAliasesConfig = createTsConfigWithoutPathAliases('tsconfig.lib.json', 'generate-ssr'); + try { + execSync(`tsc -p ${noPathAliasesConfig.path} --rootDir ./src --outDir ${tempDir} --declaration false`, { + stdio: 'inherit', + }); + } finally { + noPathAliasesConfig.cleanup(); + } console.log(chalk.blueBright(`${label}: writing *.template.html → ${outDir}/`)); await generateFTemplates({ diff --git a/packages/web-components/scripts/tsconfig-utils.js b/packages/web-components/scripts/tsconfig-utils.js new file mode 100644 index 0000000000000..bfb46e854a2be --- /dev/null +++ b/packages/web-components/scripts/tsconfig-utils.js @@ -0,0 +1,106 @@ +// @ts-check + +/** + * This script should be shared for all web-component packages. + * Tracking issue - https://github.com/microsoft/fluentui/issues/33576 + */ + +import crypto from 'node:crypto'; +import fs from 'node:fs'; +import path from 'node:path'; + +/** + * All transient configs created by this module which have not been cleaned up yet. + * + * Registering them in one place keeps the number of process listeners constant - one listener per + * module - no matter how many `tsc` invocations a script performs. + * + * NOTE: behaviourally aligned with `scripts/tasks/src/utils.ts#createTsConfigWithoutPathAliases`. + * The duplication is intentional - web-components packages must not depend on the `just` based + * v8 build tooling. + * + * @type {Set} + */ +const pendingTransientTsConfigs = new Set(); +let transientTsConfigsCounter = 0; +let processListenersRegistered = false; + +/** + * @param {string} generatedPath + */ +function removeTransientTsConfig(generatedPath) { + pendingTransientTsConfigs.delete(generatedPath); + fs.rmSync(generatedPath, { force: true }); +} + +function cleanupTransientTsConfigs() { + for (const generatedPath of [...pendingTransientTsConfigs]) { + removeTransientTsConfig(generatedPath); + } +} + +function registerProcessListeners() { + if (processListenersRegistered) { + return; + } + + processListenersRegistered = true; + + process.on('exit', cleanupTransientTsConfigs); + + // node does not run `exit` listeners when a process is terminated by a signal, + // so clean up explicitly and re-raise to keep the default termination semantics + for (const signal of /** @type {const} */ (['SIGINT', 'SIGTERM'])) { + process.once(signal, cleanupTransientTsConfigsOnSignal); + } +} + +/** + * @param {NodeJS.Signals} signal + */ +function cleanupTransientTsConfigsOnSignal(signal) { + cleanupTransientTsConfigs(); + process.kill(process.pid, signal); +} + +/** + * Creates a transient tsconfig, next to `tsConfigPath`, which turns TS path aliases off + * (`"paths": null`) for a single `tsc` invocation and returns its path. + * + * TypeScript 6 deprecates `baseUrl`, which used to be (ab)used as `tsc --baseUrl .` to make the + * workspace root relative `paths` entries unresolvable. TypeScript 6 resolves `paths` relative to + * the config file that declares them, so nulling `paths` is now the only supported way to opt a + * compilation out of path aliases - and it cannot be expressed via CLI flags, only via a config file. + * + * NOTES: + * - the generated config lives next to the original one, so every relative path + * (`extends`/`include`/`outDir`/`rootDir`/`references`) keeps resolving identically + * - the file name is unique per process and invocation, so the concurrent `tsc` runs these + * scripts spawn can never delete each other's config + * + * @param {string} tsConfigPath + * @param {string} purpose + */ +export function createTsConfigWithoutPathAliases(tsConfigPath, purpose) { + if (!fs.existsSync(tsConfigPath)) { + throw new Error(`Cannot disable TS path aliases for "${tsConfigPath}", because the file doesn't exist.`); + } + + const configFileName = path.basename(tsConfigPath); + const uniqueId = `${process.pid}-${transientTsConfigsCounter++}-${crypto.randomBytes(4).toString('hex')}`; + const generatedPath = path.join( + path.dirname(tsConfigPath), + `tsconfig.__generated-no-path-aliases-${purpose}-${uniqueId}-${configFileName}`, + ); + + fs.writeFileSync( + generatedPath, + JSON.stringify({ extends: `./${configFileName}`, compilerOptions: { paths: null } }, null, 2), + 'utf-8', + ); + + pendingTransientTsConfigs.add(generatedPath); + registerProcessListeners(); + + return { path: generatedPath, cleanup: () => removeTransientTsConfig(generatedPath) }; +} diff --git a/packages/web-components/scripts/type-check.js b/packages/web-components/scripts/type-check.js index c8b0c178ee119..9ac0122721a5b 100644 --- a/packages/web-components/scripts/type-check.js +++ b/packages/web-components/scripts/type-check.js @@ -6,6 +6,8 @@ import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { exit } from 'node:process'; +import { createTsConfigWithoutPathAliases } from './tsconfig-utils.js'; + const asyncExec = promisify(exec); main().catch(err => { @@ -23,15 +25,24 @@ async function main() { const asyncQueue = []; + const cleanupQueue = []; + for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --noEmit --baseUrl .`; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(ref, 'type-check'); + cleanupQueue.push(noPathAliasesConfig.cleanup); + + const program = `tsc -p ${noPathAliasesConfig.path} --pretty --noEmit`; asyncQueue.push(asyncExec(program)); } - return Promise.all(asyncQueue).catch(err => { - console.error(err.stdout); - exit(1); - }); + return Promise.all(asyncQueue) + .catch(err => { + console.error(err.stdout); + exit(1); + }) + .finally(() => { + cleanupQueue.forEach(cleanup => cleanup()); + }); } /** diff --git a/packages/web-components/src/tooltip/tooltip.ts b/packages/web-components/src/tooltip/tooltip.ts index 9e67fdbf9998b..84d85bce17c9c 100644 --- a/packages/web-components/src/tooltip/tooltip.ts +++ b/packages/web-components/src/tooltip/tooltip.ts @@ -92,7 +92,6 @@ export class Tooltip extends FASTElement { return; } - // @ts-expect-error - Baseline 2024 const anchorName = this.anchorElement.style.anchorName || `--${this.anchor}`; const describedBy = this.anchorElement.getAttribute('aria-describedby'); @@ -105,9 +104,7 @@ export class Tooltip extends FASTElement { if (AnchorPositioningCSSSupported) { if (!AnchorPositioningHTMLSupported) { - // @ts-expect-error - Baseline 2024 this.anchorElement.style.anchorName = anchorName; - // @ts-expect-error - Baseline 2024 this.style.positionAnchor = anchorName; } return; @@ -211,7 +208,6 @@ export class Tooltip extends FASTElement { if (!this.anchorElement) { return; } - // @ts-expect-error - Baseline 2024 const anchorName = this.anchorElement.style.anchorName || `--${this.anchor}`; // Provide style fallback for browsers that do not support anchor positioning diff --git a/packages/web-components/tsconfig.api-extractor.json b/packages/web-components/tsconfig.api-extractor.json index e245193e1fb3d..2640248432cb8 100644 --- a/packages/web-components/tsconfig.api-extractor.json +++ b/packages/web-components/tsconfig.api-extractor.json @@ -1,7 +1,6 @@ { "extends": "./tsconfig.lib.json", "compilerOptions": { - "paths": null, - "baseUrl": "." + "paths": null } } diff --git a/packages/web-components/tsconfig.json b/packages/web-components/tsconfig.json index 2391e39c3e047..584629da98832 100644 --- a/packages/web-components/tsconfig.json +++ b/packages/web-components/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.wc.json", "compilerOptions": { - "lib": ["ESNext", "DOM", "DOM.Iterable"], + "lib": ["ESNext", "DOM"], "moduleResolution": "bundler", "experimentalDecorators": true, "resolveJsonModule": true, diff --git a/packages/webpack-utilities/project.json b/packages/webpack-utilities/project.json index 8555bc1180df6..d21d3c49f6fea 100644 --- a/packages/webpack-utilities/project.json +++ b/packages/webpack-utilities/project.json @@ -3,6 +3,6 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "projectType": "library", "implicitDependencies": [], - "tags": ["v8", "platform:node", "tools"], + "tags": ["v8", "platform:node", "tools", "ships-es5"], "sourceRoot": "packages/webpack-utilities/src" } diff --git a/packages/webpack-utilities/tsconfig.json b/packages/webpack-utilities/tsconfig.json index d088dbdbe7138..247c59bbdf955 100644 --- a/packages/webpack-utilities/tsconfig.json +++ b/packages/webpack-utilities/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "target": "es5", + "rootDir": "./src", + "target": "es2015", "outDir": "lib", "module": "commonjs", "lib": ["es2017"], @@ -10,8 +11,10 @@ "experimentalDecorators": true, "importHelpers": true, "noImplicitAny": true, + "strict": false, "strictNullChecks": true, - "moduleResolution": "node", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "preserveConstEnums": true, "skipLibCheck": true, "isolatedModules": true diff --git a/patches/@swc-node+register+1.9.2.patch b/patches/@swc-node+register+1.9.2.patch new file mode 100644 index 0000000000000..104fed92f0797 --- /dev/null +++ b/patches/@swc-node+register+1.9.2.patch @@ -0,0 +1,50 @@ +diff --git a/node_modules/@swc-node/register/lib/read-default-tsconfig.js b/node_modules/@swc-node/register/lib/read-default-tsconfig.js +--- a/node_modules/@swc-node/register/lib/read-default-tsconfig.js ++++ b/node_modules/@swc-node/register/lib/read-default-tsconfig.js +@@ -153,12 +153,12 @@ + useBuiltins: true, + } + : undefined, +- baseUrl: options.baseUrl ? (0, path_1.resolve)(options.baseUrl) : undefined, ++ baseUrl: resolvePathsBase(options, filename), + paths: Object.fromEntries(Object.entries((_l = options.paths) !== null && _l !== void 0 ? _l : {}).map(([aliasKey, aliasPaths]) => { + var _a; + return [ + aliasKey, +- ((_a = aliasPaths) !== null && _a !== void 0 ? _a : []).map((path) => { var _a; return (0, path_1.resolve)((_a = options.baseUrl) !== null && _a !== void 0 ? _a : './', path); }), ++ ((_a = aliasPaths) !== null && _a !== void 0 ? _a : []).map((path) => { var _a; return (0, path_1.resolve)((_a = resolvePathsBase(options, filename)) !== null && _a !== void 0 ? _a : './', path); }), + ]; + })), + ignoreDynamic: Boolean(process.env.SWC_NODE_IGNORE_DYNAMIC), +@@ -168,5 +168,30 @@ + }, + }; + } ++/** ++ * TypeScript 6 removed `baseUrl` and resolves `paths` relative to the tsconfig which declares them, ++ * exposing it as `pathsBasePath`. `@swc-node/register` only maps `baseUrl`, which makes SWC panic ++ * (or silently resolve aliases against the process cwd) for baseUrl-less configs. ++ * ++ * @see https://github.com/swc-project/swc-node - fixed upstream in a later release; this repo pins ++ * the direct "@swc-node/register" devDependency to "1.9.2" (it is only an optional peer ++ * dependency of `nx` itself, not installed by "@nx/js") specifically to install this patch. ++ * Bump the pin and drop this patch once an unpatched release maps "pathsBasePath" upstream. ++ */ ++function resolvePathsBase(options, filename) { ++ var base = options.baseUrl || options.pathsBasePath; ++ if (base) { ++ return (0, path_1.resolve)(base); ++ } ++ if (options.paths && Object.keys(options.paths).length > 0) { ++ throw new Error('[@swc-node/register] Cannot resolve tsconfig "paths"' + ++ (filename ? ' for "' + filename + '"' : '') + ++ ': neither "baseUrl" nor "pathsBasePath" is set.\n' + ++ 'TypeScript 6 resolves "paths" relative to the config file which declares them and reports that as "pathsBasePath". ' + ++ 'Getting neither means the compiler options were not read from a tsconfig file (eg they were passed in programmatically) - ' + ++ 'pass the tsconfig path/`pathsBasePath` explicitly, otherwise every path alias would silently resolve against the current working directory.'); ++ } ++ return undefined; ++} + exports.tsCompilerOptionsToSwcConfig = tsCompilerOptionsToSwcConfig; + //# sourceMappingURL=read-default-tsconfig.js.map +\ No newline at end of file diff --git a/patches/@typescript-eslint+eslint-plugin+8.46.2.patch b/patches/@typescript-eslint+eslint-plugin+8.64.0.patch similarity index 98% rename from patches/@typescript-eslint+eslint-plugin+8.46.2.patch rename to patches/@typescript-eslint+eslint-plugin+8.64.0.patch index ec68047848978..f328f33ed62a7 100644 --- a/patches/@typescript-eslint+eslint-plugin+8.46.2.patch +++ b/patches/@typescript-eslint+eslint-plugin+8.64.0.patch @@ -1,5 +1,5 @@ diff --git a/node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-deprecated.js b/node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-deprecated.js -index 84d6ced..4512f06 100644 +index 0f6fc7c..fd696a8 100644 --- a/node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-deprecated.js +++ b/node_modules/@typescript-eslint/eslint-plugin/dist/rules/no-deprecated.js @@ -90,19 +90,26 @@ exports.default = (0, util_1.createRule)({ diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000000000..d962dad814f3d --- /dev/null +++ b/patches/README.md @@ -0,0 +1,68 @@ +# Patches + +Patches in this folder are applied to `node_modules` on `postinstall` via +[patch-package](https://github.com/ds300/patch-package) (`yarn patch-package`, see the root +`package.json` `postinstall` script). + +Every patch must be documented here with **what** it changes and **when it can be removed**. + +## `@swc-node/register+1.9.2` + +`@swc-node/register` is only an _optional peer dependency_ of `nx` itself (see +`node_modules/nx/package.json`) - it is not installed by `@nx/js` or any other Nx package. Without +it present in the workspace, Nx falls back to its `ts-node` transpiler to execute the TypeScript +files of this workspace (`just.config.ts`, executors, generators, plugins, ...), and that fallback +is not TypeScript 6 compatible. This branch adds `@swc-node/register` directly to the root +`package.json` `devDependencies` so it is actually installed, which makes Nx pick it over the +`ts-node` fallback and execute those files as SWC transpile-only instead. + +**Related CI setting:** Nx only picks SWC when `NX_PREFER_TS_NODE` is _not_ `true` (see +`node_modules/nx/src/plugins/js/utils/register.js`). The variable used to be set in every GitHub +workflow and in `.devops/templates/variables.yml`; it was removed there because the `ts-node` +fallback it forces hardcodes `moduleResolution: node10` and therefore fails with TS5107 under +TypeScript 6. Do not reintroduce it. + +**Related root tsconfig setting:** `tsconfig.base.json` spells out `"esModuleInterop": true`, which +is already the TypeScript 6 default. `@swc-node/register@1.9.2` reads the raw `compilerOptions` and +falls back to the pre TypeScript 6 default of `false` when the option is absent, which strips the +interop wrappers and makes `import x from ''` resolve to `undefined` in everything +Nx executes (generators, executors, `verify-packaging`, ...). Covered by +`scripts/package-manager/src/patches.spec.ts`. + +**What the patch changes:** `tsCompilerOptionsToSwcConfig()` maps the TypeScript `baseUrl` to SWC's +`jsc.baseUrl`, which is the base every `paths` alias is resolved against. TypeScript 6 removed +`baseUrl` and resolves `paths` relative to the tsconfig file which declares them, reporting that +directory as `pathsBasePath`. Without the patch SWC either panics or resolves every alias against +the process cwd. The patch falls back to `pathsBasePath` (matching what newer `@swc-node/register` +versions ship) and throws an actionable error when `paths` are declared but neither base is +available, instead of silently resolving aliases against the wrong directory. + +**Related pin:** `resolutions["@swc-node/core"] = "1.13.3"` in the root `package.json`. +`@swc-node/register@1.9.2` depends on `@swc-node/core@^1.13.1`. Newer `@swc-node/core` releases +raise their `@swc/core` peer requirement above the repo wide `@swc/core@1.11.24`, so without the +resolution the install resolves to a version whose peer dependency cannot be satisfied. `1.13.3` +declares `"@swc/core": ">= 1.4.13"`, which `1.11.24` satisfies. + +**Removal:** this is actionable, not merely aspirational - bump the direct `@swc-node/register` pin +(and drop this patch, and re-evaluate the `@swc-node/core` resolution) as soon as an unpatched +`@swc-node/register` release maps `pathsBasePath` upstream (tracked in the `@swc-node/register` +release notes and CHANGELOG for a version `> 1.9.2`, expected around `1.10`+). Once such a version +is installed, first try removing the patch alone: if `yarn install` (patch-package) and +`scripts/package-manager/src/patches.spec.ts` succeed without it, the pin/patch pair is obsolete and +both can be removed together. + +**Tests:** `scripts/package-manager/src/patches.spec.ts` + +## `just-task+1.5.0` + +**What it changes:** `just-task` hardcodes `moduleResolution: 'node'` when it registers `ts-node` +to load `just.config.ts`. `node10` resolution is deprecated in TypeScript 6 (TS5107) and makes every +v8 `just-scripts` target fail before it starts. The patch switches it to `bundler`, which is the +resolution mode the v8 configs use. + +**Removal:** when the v8 build stops using `just-scripts`, or `just-task` stops overriding +`moduleResolution`. + +## `@typescript-eslint+eslint-plugin+8.64.0`, `storywright+0.0.27-storybook7.14` + +Pre-existing patches, unrelated to the TypeScript 6 migration. diff --git a/patches/just-task+1.5.0.patch b/patches/just-task+1.5.0.patch new file mode 100644 index 0000000000000..7eb096624dbd9 --- /dev/null +++ b/patches/just-task+1.5.0.patch @@ -0,0 +1,12 @@ +diff --git a/node_modules/just-task/lib/enableTypeScript.js b/node_modules/just-task/lib/enableTypeScript.js +--- a/node_modules/just-task/lib/enableTypeScript.js ++++ b/node_modules/just-task/lib/enableTypeScript.js +@@ -17,7 +17,7 @@ + strict: false, + skipLibCheck: true, + skipDefaultLibCheck: true, +- moduleResolution: 'node', ++ moduleResolution: 'bundler', + allowJs: true, + esModuleInterop: true, + }, diff --git a/scripts/api-extractor/tsconfig.lib.json b/scripts/api-extractor/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/api-extractor/tsconfig.lib.json +++ b/scripts/api-extractor/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/api-extractor/tsconfig.spec.json b/scripts/api-extractor/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/api-extractor/tsconfig.spec.json +++ b/scripts/api-extractor/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/babel/tsconfig.lib.json b/scripts/babel/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/babel/tsconfig.lib.json +++ b/scripts/babel/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/babel/tsconfig.spec.json b/scripts/babel/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/babel/tsconfig.spec.json +++ b/scripts/babel/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/beachball/tsconfig.lib.json b/scripts/beachball/tsconfig.lib.json index 040fb0e3c58a1..b3b5bfd36c8ef 100644 --- a/scripts/beachball/tsconfig.lib.json +++ b/scripts/beachball/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/beachball/tsconfig.spec.json b/scripts/beachball/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/beachball/tsconfig.spec.json +++ b/scripts/beachball/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/cypress/package.json b/scripts/cypress/package.json index 64bd55f953e55..93a85a80729b8 100644 --- a/scripts/cypress/package.json +++ b/scripts/cypress/package.json @@ -2,7 +2,7 @@ "name": "@fluentui/scripts-cypress", "version": "0.0.1", "private": true, - "main": "src/index.ts", + "main": "src/index.js", "browser": "src/browser/index.ts", "dependencies": {}, "devDependencies": {} diff --git a/scripts/cypress/src/base.config.js b/scripts/cypress/src/base.config.js new file mode 100644 index 0000000000000..715f85fadd90e --- /dev/null +++ b/scripts/cypress/src/base.config.js @@ -0,0 +1,152 @@ +// @ts-check + +/** + * Shared Cypress Component Testing configuration. + * + * NOTE: this module is authored in JavaScript on purpose. Cypress loads the config file in a child + * process where it registers its own bundled `ts-node` with a hardcoded `moduleResolution: 'node'` + * (node10), which TypeScript 6 rejects with TS5107. Every TypeScript file reachable from a + * `cypress.config.*` therefore fails to load, so the whole Node side of the Cypress setup - the + * config files and this module - is plain CommonJS checked with `// @ts-check`. + */ + +const crypto = require('node:crypto'); +const path = require('node:path'); + +const { defineConfig } = require('cypress'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); + +const { readWorkspacePathAliases } = require('./ts-paths'); + +/** + * @import { Configuration } from 'webpack'; + */ + +/** + * `./index` (this package's public type entry point, `src/index.d.ts`) is the single source of truth + * for `BaseConfig` - re-declaring its shape here would let the two drift apart. + * @typedef {import('./index').BaseConfig} BaseConfig + */ + +const workspaceRoot = path.resolve(__dirname, '../../..'); +const workspaceTsConfigPath = path.join(workspaceRoot, 'tsconfig.base.json'); + +const projectRoot = process.cwd(); + +// Use a high port range unlikely to collide with other services: 20000-29999 +const deterministicPort = 20000 + (hashToInt(projectRoot) % 10000); + +/** + * @type {Configuration} + */ +const baseWebpackConfig = { + resolve: { + extensions: ['.js', '.ts', '.jsx', '.tsx'], + }, + mode: 'development', + devtool: 'eval', + // Ensure parallel Cypress component runs don't collide on a fixed port (8080 is webpack-dev-server default). + // Pick a deterministic port per project (can be overridden) since some CI setups ignore 'auto'. + // @ts-expect-error - devServer is provided by webpack-dev-server typings + devServer: { + port: process.env.WEBPACK_DEV_SERVER_PORT ? Number(process.env.WEBPACK_DEV_SERVER_PORT) : deterministicPort, + host: '127.0.0.1', + }, + output: { + publicPath: '/', + chunkFilename: '[name].bundle.js', + }, + module: { + rules: [], + }, +}; + +/** + * @returns {Configuration} + */ +const cypressWebpackConfig = () => { + if (baseWebpackConfig.module) { + baseWebpackConfig.module.rules?.push({ + test: /\.(ts|tsx)$/, + loader: 'esbuild-loader', + options: { + tsconfig: './tsconfig.cy.json', + }, + }); + } + + // TODO: remove this once esbuild-loader properly handles module loading https://github.com/privatenumber/esbuild-loader/issues/343#issuecomment-1845836603 + baseWebpackConfig.ignoreWarnings = [ + ...(baseWebpackConfig.ignoreWarnings ?? []), + { + module: /[esbuild-loader]/, + message: + /The specified tsconfig at\s+"[/a-z0-9-/.\s]+"\s+was applied to the file\s+"[/a-z0-9-.\s]+"\s+but does not match its "include" patterns/i, + }, + ]; + + baseWebpackConfig.resolve ??= {}; + baseWebpackConfig.resolve.plugins ??= []; + baseWebpackConfig.resolve.plugins.push( + new TsconfigPathsPlugin({ + configFile: workspaceTsConfigPath, + // explicit, because the aliases are `pathsBasePath` relative - see `./ts-paths` + baseUrl: readWorkspacePathAliases(workspaceTsConfigPath).absoluteBaseUrl, + }), + ); + + return baseWebpackConfig; +}; + +/** + * Programmatically create relative support support path, because Cypress bug + * @see https://github.com/cypress-io/cypress/issues/31819 + * + * This is a workaround for the issue where Cypress does not resolve the paths correctly, as it + * internally prepend the __dirname, making them invalid + * + */ +const sharedConfigSupportRootDir = path.join(__dirname, './support'); +const projectSupportDir = path.relative(projectRoot, sharedConfigSupportRootDir); + +const baseConfig = /** @type {BaseConfig} */ ( + defineConfig({ + video: false, + component: { + specPattern: [path.join(projectRoot, '**/*.e2e.tsx'), path.join(projectRoot, '**/*.cy.tsx')], + devServer: { + framework: 'react', + bundler: 'webpack', + webpackConfig: cypressWebpackConfig(), + }, + supportFile: path.join(projectSupportDir, './component.js'), + indexHtmlFile: path.join(projectSupportDir, './component-index.html'), + defaultCommandTimeout: 8000, + }, + retries: { + runMode: 4, + openMode: 0, + }, + // Screenshots go under /cypress/screenshots and can be useful to look at after failures in + // local headless runs (especially if the failure is specific to headless runs) + // screenshotOnRunFailure: isLocalRun && argv.mode === 'run', + fixturesFolder: path.join(__dirname, './fixtures'), + }) +); + +/** + * use this as base webpack config if you need to customize devServer webpack configuration + * + * Generate a deterministic, project-scoped port to avoid collisions when multiple Cypress component + * test servers start in parallel on the same machine/agent. Allows override via WEBPACK_DEV_SERVER_PORT. + * + * @param {string} str + */ +function hashToInt(str) { + // Use Node.js crypto module for better hashing + const hash = crypto.createHash('sha256').update(str).digest('hex'); + // Convert first 8 hex characters to integer + return parseInt(hash.slice(0, 8), 16); +} + +module.exports = { baseConfig, baseWebpackConfig }; diff --git a/scripts/cypress/src/base.config.ts b/scripts/cypress/src/base.config.ts deleted file mode 100644 index 0f7cc2b1ccb7e..0000000000000 --- a/scripts/cypress/src/base.config.ts +++ /dev/null @@ -1,123 +0,0 @@ -import * as crypto from 'crypto'; -import * as path from 'path'; - -import { defineConfig } from 'cypress'; -import { TsconfigPathsPlugin } from 'tsconfig-paths-webpack-plugin'; -import type { Configuration } from 'webpack'; - -const projectRoot = process.cwd(); - -// Use a high port range unlikely to collide with other services: 20000-29999 -const deterministicPort = 20000 + (hashToInt(projectRoot) % 10000); - -export const baseWebpackConfig: Configuration = { - resolve: { - extensions: ['.js', '.ts', '.jsx', '.tsx'], - }, - mode: 'development', - devtool: 'eval', - // Ensure parallel Cypress component runs don't collide on a fixed port (8080 is webpack-dev-server default). - // Pick a deterministic port per project (can be overridden) since some CI setups ignore 'auto'. - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - devServer is provided by webpack-dev-server typings - devServer: { - port: process.env.WEBPACK_DEV_SERVER_PORT ? Number(process.env.WEBPACK_DEV_SERVER_PORT) : deterministicPort, - host: '127.0.0.1', - }, - output: { - publicPath: '/', - chunkFilename: '[name].bundle.js', - }, - module: { - rules: [], - }, -}; - -const cypressWebpackConfig = (): Configuration => { - if (baseWebpackConfig.module) { - baseWebpackConfig.module.rules?.push({ - test: /\.(ts|tsx)$/, - loader: 'esbuild-loader', - options: { - tsconfig: './tsconfig.cy.json', - }, - }); - } - - // TODO: remove this once esbuild-loader properly handles module loading https://github.com/privatenumber/esbuild-loader/issues/343#issuecomment-1845836603 - baseWebpackConfig.ignoreWarnings = [ - ...(baseWebpackConfig.ignoreWarnings ?? []), - { - module: /[esbuild-loader]/, - message: - /The specified tsconfig at\s+"[/a-z0-9-/.\s]+"\s+was applied to the file\s+"[/a-z0-9-.\s]+"\s+but does not match its "include" patterns/i, - }, - ]; - - baseWebpackConfig.resolve ??= {}; - baseWebpackConfig.resolve.plugins ??= []; - baseWebpackConfig.resolve.plugins.push( - new TsconfigPathsPlugin({ - configFile: path.resolve(__dirname, '../../../tsconfig.base.json'), - }), - ); - - return baseWebpackConfig; -}; - -interface BaseConfig extends Cypress.ConfigOptions { - component: Cypress.Config['component'] & { - devServer: { - bundler: 'webpack'; - framework: 'react'; - webpackConfig: Configuration; - }; - }; -} - -/** - * Programmatically create relative support support path, because Cypress bug - * @see https://github.com/cypress-io/cypress/issues/31819 - * - * This is a workaround for the issue where Cypress does not resolve the paths correctly, as it - * internally prepend the __dirname, making them invalid - * - */ -const sharedConfigSupportRootDir = path.join(__dirname, './support'); -const projectSupportDir = path.relative(projectRoot, sharedConfigSupportRootDir); - -export const baseConfig = defineConfig({ - video: false, - component: { - specPattern: [path.join(projectRoot, '**/*.e2e.tsx'), path.join(projectRoot, '**/*.cy.tsx')], - devServer: { - framework: 'react', - bundler: 'webpack', - webpackConfig: cypressWebpackConfig(), - }, - supportFile: path.join(projectSupportDir, './component.js'), - indexHtmlFile: path.join(projectSupportDir, './component-index.html'), - defaultCommandTimeout: 8000, - }, - retries: { - runMode: 4, - openMode: 0, - }, - // Screenshots go under /cypress/screenshots and can be useful to look at after failures in - // local headless runs (especially if the failure is specific to headless runs) - // screenshotOnRunFailure: isLocalRun && argv.mode === 'run', - fixturesFolder: path.join(__dirname, './fixtures'), -}) as BaseConfig; - -/** - * use this as base webpack config if you need to customize devServer webpack configuration - * - * Generate a deterministic, project-scoped port to avoid collisions when multiple Cypress component - * test servers start in parallel on the same machine/agent. Allows override via WEBPACK_DEV_SERVER_PORT. - */ -function hashToInt(str: string) { - // Use Node.js crypto module for better hashing - const hash = crypto.createHash('sha256').update(str).digest('hex'); - // Convert first 8 hex characters to integer - return parseInt(hash.slice(0, 8), 16); -} diff --git a/scripts/cypress/src/index.d.ts b/scripts/cypress/src/index.d.ts new file mode 100644 index 0000000000000..0a9a69e5ead20 --- /dev/null +++ b/scripts/cypress/src/index.d.ts @@ -0,0 +1,24 @@ +import type { Configuration } from 'webpack'; + +export type BaseConfig = Cypress.ConfigOptions & { + component: Cypress.Config['component'] & { + devServer: { + bundler: 'webpack'; + framework: 'react'; + webpackConfig: Configuration; + }; + }; +}; + +export declare const baseConfig: BaseConfig; +export declare const baseWebpackConfig: Configuration; + +// =========== TS PATH ALIASES ================== + +export declare const readWorkspacePathAliases: typeof import('./ts-paths').readWorkspacePathAliases; + +// =========== BROWSER APIs ================== + +// TODO: Browser related APIs should be exposed via export maps or moved to separate package +// Expose Browser specific API under same barrel +export declare const mount: typeof import('./browser').mount; diff --git a/scripts/cypress/src/index.js b/scripts/cypress/src/index.js new file mode 100644 index 0000000000000..79fab9ce0b355 --- /dev/null +++ b/scripts/cypress/src/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...require('./base.config'), + ...require('./ts-paths'), +}; diff --git a/scripts/cypress/src/index.test.js b/scripts/cypress/src/index.test.js new file mode 100644 index 0000000000000..400018be048e8 --- /dev/null +++ b/scripts/cypress/src/index.test.js @@ -0,0 +1,29 @@ +// @ts-nocheck +// This file intentionally opts out of `checkJs` (unlike every other module in this package): it +// `require`s `./index`, whose sibling `./index.d.ts` re-exports `BaseConfig` (a `Cypress.ConfigOptions` +// derivative). Pulling that ambient `Cypress` global into the same TS program as this project's +// `tsconfig.spec.json` (`@types/jest`) collides two ambient `Assertion`/`expect` globals - the same +// conflict `tsconfig.spec.json` already works around by excluding `src/index.d.ts` itself. Runtime +// behavior (what this file actually tests) is unaffected by `@ts-nocheck`. + +const index = require('./index'); + +describe('@fluentui/scripts-cypress public entry point', () => { + it('exposes baseConfig and baseWebpackConfig from ./base.config', () => { + expect(index.baseConfig).toBeDefined(); + expect(index.baseConfig.component).toBeDefined(); + expect(index.baseWebpackConfig).toBeDefined(); + }); + + it('exposes readWorkspacePathAliases from ./ts-paths, so consumers do not need a deep import', () => { + // `apps/rit-tests-v8/cypress.config.js` (and any future caller that needs to thread an explicit + // `baseUrl` into `@fluentui/scripts-storybook`'s `registerTsPaths`) relies on this being part of + // the package's public surface rather than reaching into `@fluentui/scripts-cypress/src/ts-paths`. + expect(typeof index.readWorkspacePathAliases).toBe('function'); + + const result = index.readWorkspacePathAliases( + require('node:path').resolve(__dirname, '../../../tsconfig.base.json'), + ); + expect(typeof result.absoluteBaseUrl).toBe('string'); + }); +}); diff --git a/scripts/cypress/src/index.ts b/scripts/cypress/src/index.ts deleted file mode 100644 index 54f6d609d26ba..0000000000000 --- a/scripts/cypress/src/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { baseConfig, baseWebpackConfig } from './base.config'; - -// =========== BROWSER APIs ================== - -// TODO: Browser related APIs should be exposed via export maps or moved to separate package -// Expose Browser specific API under same barrel -export declare const mount: typeof import('./browser').mount; diff --git a/scripts/cypress/src/ts-paths.js b/scripts/cypress/src/ts-paths.js new file mode 100644 index 0000000000000..8971bdd4e0bbf --- /dev/null +++ b/scripts/cypress/src/ts-paths.js @@ -0,0 +1,126 @@ +// @ts-check + +const path = require('node:path'); + +const ts = require('typescript'); + +/** + * @typedef {Object} WorkspacePathAliases + * @property {string} absoluteBaseUrl directory `paths` entries are resolved against + */ + +/** @type {import('typescript').FormatDiagnosticsHost} */ +const diagnosticsHost = { + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => ts.sys.getCurrentDirectory(), + getNewLine: () => ts.sys.newLine, +}; + +/** + * Host passed to `parseJsonConfigFileContent`, deliberately restricted to what this module actually + * needs: `compilerOptions` (through an `extends` chain) and parse diagnostics. `readDirectory` is only + * used by TypeScript to expand `include`/`exclude`/`files` into the config's resulting file list, which + * this module never reads (`readWorkspacePathAliases` only returns `absoluteBaseUrl`) - answering it + * with `[]` skips enumerating the whole monorepo for every call. `fileExists`/`readFile` still delegate + * to the real filesystem (`ts.sys`) because they drive the `extends` chain resolution and parse + * diagnostics (e.g. TS5083 "Cannot read file" for a missing `extends` target). + * + * @type {import('typescript').ParseConfigHost} + */ +const parseConfigHost = { + useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames, + readDirectory: () => [], + fileExists: ts.sys.fileExists, + readFile: ts.sys.readFile, +}; + +/** + * @param {string} tsConfigPath + * @param {readonly import('typescript').Diagnostic[]} diagnostics + */ +function createConfigParseError(tsConfigPath, diagnostics) { + return new Error( + [ + `Failed to parse "${tsConfigPath}".`, + ``, + ts.formatDiagnostics(diagnostics, diagnosticsHost).trimEnd(), + ``, + `Path aliases cannot be resolved from a config that does not parse - fix the config above.`, + ].join('\n'), + ); +} + +/** + * Reads the path aliases of a tsconfig with explicit `pathsBasePath` semantics. + * + * TypeScript 6 deprecated `baseUrl` - `paths` entries are resolved against the directory of the config + * file that *declares* them (which, through an `extends` chain, is not necessarily the config file + * passed in here) rather than against a project-wide `baseUrl`. TypeScript exposes that directory on + * the parsed options as `pathsBasePath`, but only computes it when it knows the identity of the config + * file being parsed - i.e. when `configFileName` is passed to `parseJsonConfigFileContent`. Without it, + * `pathsBasePath` is left undefined and callers fall back to resolving `paths` against the wrong + * directory whenever they are declared in a base config. + * + * - `tsconfig-paths`, which Cypress registers for the Node side before it loads the config file, does + * *not* skip alias resolution just because `baseUrl` is missing (as of `tsconfig-paths@4.2`, it only + * gives up when no `tsconfig.json`/`jsconfig.json` can be found at all). For a `baseUrl`-less config + * it still resolves `paths`, but anchors them at the directory of the *nearest* config file instead of + * the one that declares them - which silently breaks aliases declared in a shared base config such as + * this workspace's `tsconfig.base.json`. That is inconsequential here - every workspace package the + * Cypress Node process can reference is resolvable through its Yarn workspace `node_modules` symlink, + * and the aliases point at TypeScript sources which Cypress' bundled `ts-node` cannot compile under + * TypeScript 6 anyway. + * - `tsconfig-paths-webpack-plugin` (the bundler side, where the aliases _do_ matter) has the same + * "nearest config file" fallback. Handing it the `pathsBasePath` resolved here keeps mapping roots + * correct through the `extends` chain instead of accidentally correct only when `paths` happen to be + * declared in the outermost config. + * + * Parse diagnostics are fatal: silently continuing would resolve `paths` from a config that doesn't + * actually parse (e.g. an invalid `extends` target or an unknown compiler option). + * + * @param {string} tsConfigPath absolute path to the tsconfig declaring the aliases + * @returns {WorkspacePathAliases} + */ +function readWorkspacePathAliases(tsConfigPath) { + const { config, error } = ts.readConfigFile(tsConfigPath, ts.sys.readFile); + + if (error) { + throw createConfigParseError(tsConfigPath, [error]); + } + + const parsedConfig = ts.parseJsonConfigFileContent( + config, + parseConfigHost, + path.dirname(tsConfigPath), + /* existingOptions */ undefined, + // Required for `pathsBasePath` to be computed correctly through `extends` chains - without it + // TypeScript has no config file identity to resolve `paths`/`pathsBasePath` against. + tsConfigPath, + ); + + // `parsedConfig.errors` is misleadingly named: TypeScript can also put `Warning`/`Suggestion` category + // diagnostics in it (e.g. deprecated compiler option notices), which must not fail alias resolution - + // only genuine `Error` category diagnostics may. TS18003 ("No inputs were found in config file ...") + // is filtered separately: it is a direct, expected artifact of `parseConfigHost.readDirectory` above + // always answering `[]` - every workspace config that relies on the default `include: ["**/*"]` (no + // explicit `files`) would otherwise fail here even though its `compilerOptions`/`paths` parsed fine. + const NO_INPUTS_FOUND_DIAGNOSTIC_CODE = 18003; + const configErrors = parsedConfig.errors.filter( + diagnostic => + diagnostic.category === ts.DiagnosticCategory.Error && diagnostic.code !== NO_INPUTS_FOUND_DIAGNOSTIC_CODE, + ); + + if (configErrors.length > 0) { + throw createConfigParseError(tsConfigPath, configErrors); + } + + const { options } = parsedConfig; + // `CompilerOptions` widens every non declared option to `CompilerOptionsValue` + const pathsBasePath = typeof options.pathsBasePath === 'string' ? options.pathsBasePath : undefined; + + return { + absoluteBaseUrl: pathsBasePath ? path.resolve(pathsBasePath) : path.dirname(tsConfigPath), + }; +} + +module.exports = { readWorkspacePathAliases }; diff --git a/scripts/cypress/src/ts-paths.test.js b/scripts/cypress/src/ts-paths.test.js new file mode 100644 index 0000000000000..71c2421b9afa5 --- /dev/null +++ b/scripts/cypress/src/ts-paths.test.js @@ -0,0 +1,243 @@ +// @ts-check + +const { mkdirSync, mkdtempSync, rmSync, writeFileSync } = require('node:fs'); +const os = require('node:os'); +const { join } = require('node:path'); + +/** + * `parseJsonConfigFileContent(...).errors` is misleadingly named: TypeScript can also place + * `Warning`/`Suggestion` category diagnostics in that array, which must not fail path alias resolution - + * only genuine `Error` category diagnostics may. `typescript`'s own exports are frozen, so the module is + * wrapped in a `jest.fn()` (rather than `jest.spyOn`, which cannot redefine a non-configurable property). + * + * NOTE: this file is plain CommonJS (see `./ts-paths` and `./base.config` for why), so - unlike a + * TypeScript test transformed by `ts-jest`/`@swc/jest` - `jest.mock` calls are *not* hoisted above + * `require` calls by babel. `jest.mock('typescript', ...)` therefore has to be the first thing in this + * file, textually before `./ts-paths` (and anything else) requires `typescript`, or the module under + * test would keep the real, un-mocked module instance. + */ +jest.mock('typescript', () => { + const actual = jest.requireActual('typescript'); + return { ...actual, parseJsonConfigFileContent: jest.fn(actual.parseJsonConfigFileContent) }; +}); + +const ts = require('typescript'); + +const { readWorkspacePathAliases } = require('./ts-paths'); + +/** + * Mirrors the repository topology that {@link readWorkspacePathAliases} has to support: + * + * ``` + * /tsconfig.base.json <- declares `paths`, no `baseUrl` + * /packages/pkg/tsconfig.json <- extends the base config + * /packages/dep/src/index.ts <- alias target, outside the child config directory + * ``` + * + * @param {{ baseConfig?: Record; packageConfig?: Record }} [overrides] + */ +function prepareFixture(overrides = {}) { + // written under the OS temp directory (never inside a source tree) so an interrupted run cannot leave + // fixture files behind for git/tsconfig to pick up + const root = mkdtempSync(join(os.tmpdir(), 'ts-paths-workspace-')); + const packageRoot = join(root, 'packages', 'pkg'); + const dependencyRoot = join(root, 'packages', 'dep'); + + mkdirSync(packageRoot, { recursive: true }); + mkdirSync(join(dependencyRoot, 'src'), { recursive: true }); + + writeFileSync(join(dependencyRoot, 'src', 'index.ts'), 'export const dep = 1;', 'utf-8'); + + writeFileSync( + join(root, 'tsconfig.base.json'), + JSON.stringify( + overrides.baseConfig ?? { + compilerOptions: { + target: 'ES2019', + module: 'esnext', + moduleResolution: 'bundler', + paths: { + '@proj/dep': ['./packages/dep/src/index.ts'], + }, + }, + }, + null, + 2, + ), + 'utf-8', + ); + + writeFileSync( + join(packageRoot, 'tsconfig.json'), + JSON.stringify( + overrides.packageConfig ?? { + extends: '../../tsconfig.base.json', + compilerOptions: { noEmit: true }, + include: [], + files: [], + }, + null, + 2, + ), + 'utf-8', + ); + + return { + cleanup: () => rmSync(root, { recursive: true, force: true }), + paths: { root, packageRoot, dependencyRoot, tsConfigPath: join(packageRoot, 'tsconfig.json') }, + }; +} + +describe('readWorkspacePathAliases', () => { + /** @type {ReturnType | undefined} */ + let fixture; + + afterEach(() => { + fixture?.cleanup(); + fixture = undefined; + }); + + it('resolves the base directory of paths declared in an extended base config that has no baseUrl', () => { + fixture = prepareFixture(); + + const result = readWorkspacePathAliases(fixture.paths.tsConfigPath); + + expect(result).toEqual({ absoluteBaseUrl: fixture.paths.root }); + }); + + it('does not enumerate the workspace to resolve the base directory', () => { + // `readDirectory` backs `include`/`exclude`/`files` glob expansion, which this module never reads - + // a real filesystem walk here would be both wasted work and a footgun for monorepo-wide configs. + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { noEmit: true }, + // deliberately omit `include`/`files` so a real `readDirectory` would enumerate the fixture root + }, + }); + + const result = readWorkspacePathAliases(fixture.paths.tsConfigPath); + + expect(result).toEqual({ absoluteBaseUrl: fixture.paths.root }); + }); + + it('throws with formatted diagnostics when the config extends a missing file', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.does-not-exist.json', + include: [], + files: [], + }, + }); + const { tsConfigPath } = fixture.paths; + + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/Failed to parse/); + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow( + /error TS5083: Cannot read file .*tsconfig\.does-not-exist\.json/, + ); + }); + + it('throws with formatted diagnostics when the config declares an unknown compiler option', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { noEmit: true, thisOptionDoesNotExist: true }, + include: [], + files: [], + }, + }); + const { tsConfigPath } = fixture.paths; + + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/Failed to parse/); + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/thisOptionDoesNotExist/); + }); + + it('throws with formatted diagnostics when a compiler option has an invalid value', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { moduleResolution: 'not-a-resolution-mode' }, + include: [], + files: [], + }, + }); + const { tsConfigPath } = fixture.paths; + + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/Failed to parse/); + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/moduleResolution/); + }); + + it('throws with formatted diagnostics when the tsconfig itself cannot be read', () => { + fixture = prepareFixture(); + const missingConfigPath = join(fixture.paths.packageRoot, 'does-not-exist.json'); + + expect(() => readWorkspacePathAliases(missingConfigPath)).toThrow(/Failed to parse/); + }); +}); + +describe('readWorkspacePathAliases - non-error config diagnostics', () => { + /** @type {ReturnType | undefined} */ + let fixture; + const parseJsonConfigFileContentMockFn = /** @type {jest.MockedFunction} */ ( + ts.parseJsonConfigFileContent + ); + const actualParseJsonConfigFileContent = jest.requireActual('typescript').parseJsonConfigFileContent; + + afterEach(() => { + fixture?.cleanup(); + fixture = undefined; + parseJsonConfigFileContentMockFn.mockImplementation(actualParseJsonConfigFileContent); + }); + + /** + * @param {import('typescript').DiagnosticCategory} category + * @param {string} messageText + * @returns {import('typescript').Diagnostic} + */ + function createDiagnostic(category, messageText) { + return { + category, + code: 9999, + file: undefined, + start: undefined, + length: undefined, + messageText, + }; + } + + /** + * Parses the real fixture config, then swaps in a synthetic `errors` array so the diagnostic category + * filtering can be exercised directly, without depending on a real world config that happens to produce + * a non-error diagnostic (none of the fixtures above do). + * + * @param {import('typescript').Diagnostic[]} errors + */ + function stubParsedConfigErrors(errors) { + parseJsonConfigFileContentMockFn.mockImplementation((...args) => { + const parsedConfig = actualParseJsonConfigFileContent(...args); + return { ...parsedConfig, errors }; + }); + } + + it('does not throw when only a warning/suggestion category diagnostic is reported', () => { + fixture = prepareFixture(); + const { tsConfigPath } = fixture.paths; + stubParsedConfigErrors([ + createDiagnostic(ts.DiagnosticCategory.Warning, 'a harmless warning'), + createDiagnostic(ts.DiagnosticCategory.Suggestion, 'a helpful suggestion'), + ]); + + expect(() => readWorkspacePathAliases(tsConfigPath)).not.toThrow(); + }); + + it('still throws for a real error even when a warning/suggestion diagnostic is also present', () => { + fixture = prepareFixture(); + const { tsConfigPath } = fixture.paths; + stubParsedConfigErrors([ + createDiagnostic(ts.DiagnosticCategory.Warning, 'a harmless warning'), + createDiagnostic(ts.DiagnosticCategory.Error, 'a real configuration error'), + ]); + + expect(() => readWorkspacePathAliases(tsConfigPath)).toThrow(/a real configuration error/); + }); +}); diff --git a/scripts/cypress/tsconfig.lib.json b/scripts/cypress/tsconfig.lib.json index 2cd7456061fb1..99b323c1383c5 100644 --- a/scripts/cypress/tsconfig.lib.json +++ b/scripts/cypress/tsconfig.lib.json @@ -1,11 +1,12 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", "types": ["node", "cypress"] }, - "exclude": ["**/*.spec.ts", "**/*.test.ts"], + "exclude": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.js", "**/*.test.js"], "include": ["./src/**/*.ts", "./src/**/*.js"] } diff --git a/scripts/cypress/tsconfig.spec.json b/scripts/cypress/tsconfig.spec.json index a0a0008c224b9..7855ff80b3988 100644 --- a/scripts/cypress/tsconfig.spec.json +++ b/scripts/cypress/tsconfig.spec.json @@ -1,10 +1,21 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.spec.js", "**/*.test.js", "**/*.d.ts"], + // `src/index.d.ts` is the package's public type entry point - it references the global `Cypress` + // namespace (via `cypress`'s own ambient globals), which collides with `@types/jest`'s ambient + // `expect`/`Assertion` globals if both end up in the same program. It isn't needed to type-check the + // Jest specs themselves, so it's excluded here rather than widening this project's `types`. + // + // `src/index.test.js` is excluded for the same reason: it `require`s `./index`, which pulls in + // `./index.d.ts` as a dependency (not just as a root file), reintroducing the exact ambient + // collision above even though `index.d.ts` itself is excluded - `@ts-nocheck` on the test file does + // not help, because the collision comes from both ambient global namespaces sharing one Program, + // not from checking the test file's own contents. + "exclude": ["src/index.d.ts", "src/index.test.js"] } diff --git a/scripts/executors/tsconfig.lib.json b/scripts/executors/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/executors/tsconfig.lib.json +++ b/scripts/executors/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/executors/tsconfig.spec.json b/scripts/executors/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/executors/tsconfig.spec.json +++ b/scripts/executors/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/generators/src/create-package/plop-templates-node/tsconfig.json b/scripts/generators/src/create-package/plop-templates-node/tsconfig.json index 53a3799e383e0..8e7fd81fd8d58 100644 --- a/scripts/generators/src/create-package/plop-templates-node/tsconfig.json +++ b/scripts/generators/src/create-package/plop-templates-node/tsconfig.json @@ -1,15 +1,15 @@ { "compilerOptions": { - "baseUrl": ".", + "rootDir": "./src", "outDir": "lib", "target": "es6", - "module": "commonjs", + "module": "NodeNext", + "moduleResolution": "NodeNext", "declaration": true, "sourceMap": true, "importHelpers": true, "noUnusedLocals": true, "strict": true, - "moduleResolution": "node", "preserveConstEnums": true, "esModuleInterop": true, "lib": ["es2017"], diff --git a/scripts/generators/tsconfig.lib.json b/scripts/generators/tsconfig.lib.json index bfb749f339c50..52694ece53159 100644 --- a/scripts/generators/tsconfig.lib.json +++ b/scripts/generators/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/generators/tsconfig.spec.json b/scripts/generators/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/generators/tsconfig.spec.json +++ b/scripts/generators/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/github/tsconfig.lib.json b/scripts/github/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/github/tsconfig.lib.json +++ b/scripts/github/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/github/tsconfig.spec.json b/scripts/github/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/github/tsconfig.spec.json +++ b/scripts/github/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/jest/src/shared.js b/scripts/jest/src/shared.js index 6c8ce0e1b8b9a..dcec9524df1a8 100644 --- a/scripts/jest/src/shared.js +++ b/scripts/jest/src/shared.js @@ -9,7 +9,8 @@ * based on testing spawning only 50% of available workers is fastest on both Local Machine and CI env atm ( 8 Core machine, 16GB RAM) */ -// - for macos large runner on GHA we need to set it to 2 {@link file://./../../../.github/workflows/pr.yml#77} +// - on GHA `FLUENT_JEST_WORKER` is set per runner size because Nx already runs tasks concurrently +// {@link file://./../../../.github/workflows/pr.yml} // - for ADO runners it's 50% // - temporary adding FLUENT_JEST_WORKER var in order to make it pass on both GHA and ADO const workersConfig = { maxWorkers: process.env.FLUENT_JEST_WORKER || '50%' }; diff --git a/scripts/jest/tsconfig.lib.json b/scripts/jest/tsconfig.lib.json index 040fb0e3c58a1..b3b5bfd36c8ef 100644 --- a/scripts/jest/tsconfig.lib.json +++ b/scripts/jest/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/jest/tsconfig.spec.json b/scripts/jest/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/jest/tsconfig.spec.json +++ b/scripts/jest/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/lint-staged/tsconfig.lib.json b/scripts/lint-staged/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/lint-staged/tsconfig.lib.json +++ b/scripts/lint-staged/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/lint-staged/tsconfig.spec.json b/scripts/lint-staged/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/lint-staged/tsconfig.spec.json +++ b/scripts/lint-staged/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/monorepo/tsconfig.lib.json b/scripts/monorepo/tsconfig.lib.json index 040fb0e3c58a1..b3b5bfd36c8ef 100644 --- a/scripts/monorepo/tsconfig.lib.json +++ b/scripts/monorepo/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/monorepo/tsconfig.spec.json b/scripts/monorepo/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/monorepo/tsconfig.spec.json +++ b/scripts/monorepo/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/package-manager/src/patches.spec.ts b/scripts/package-manager/src/patches.spec.ts new file mode 100644 index 0000000000000..d3c7e1435da4a --- /dev/null +++ b/scripts/package-manager/src/patches.spec.ts @@ -0,0 +1,69 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { workspaceRoot } from '@nx/devkit'; + +/** + * Regression tests for the `patch-package` patches this repo applies on `postinstall`. + * + * They run against the patched files in `node_modules`, so they also verify that the patches were + * applied at all - the whole build tooling silently misbehaves without them. + */ +describe(`patches`, () => { + describe(`@swc-node/register`, () => { + // the package `exports` map doesn't expose internals, and this is exactly what the patch touches + const patchedModulePath = path.join(workspaceRoot, 'node_modules/@swc-node/register/lib/read-default-tsconfig.js'); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { tsCompilerOptionsToSwcConfig } = require(patchedModulePath); + + it(`should map TypeScript 6 "pathsBasePath" to the swc "baseUrl"`, () => { + const actual = tsCompilerOptionsToSwcConfig( + { paths: { '@proj/one': ['./packages/one/src/index.ts'] }, pathsBasePath: '/workspace' }, + 'file.ts', + ); + + expect(actual.baseUrl).toEqual('/workspace'); + expect(actual.paths).toEqual({ '@proj/one': ['/workspace/packages/one/src/index.ts'] }); + }); + + it(`should keep honouring an explicit "baseUrl"`, () => { + const actual = tsCompilerOptionsToSwcConfig( + { paths: { '@proj/one': ['./packages/one/src/index.ts'] }, baseUrl: '/legacy' }, + 'file.ts', + ); + + expect(actual.baseUrl).toEqual('/legacy'); + expect(actual.paths).toEqual({ '@proj/one': ['/legacy/packages/one/src/index.ts'] }); + }); + + it(`should not require a base when there are no path aliases`, () => { + expect(tsCompilerOptionsToSwcConfig({}, 'file.ts').baseUrl).toBeUndefined(); + expect(tsCompilerOptionsToSwcConfig({ paths: {} }, 'file.ts').baseUrl).toBeUndefined(); + }); + + it(`should fail with an actionable error instead of silently breaking path aliases`, () => { + expect(() => + tsCompilerOptionsToSwcConfig({ paths: { '@proj/one': ['./packages/one/src/index.ts'] } }, 'file.ts'), + ).toThrow(/Cannot resolve tsconfig "paths" for "file\.ts": neither "baseUrl" nor "pathsBasePath" is set/); + }); + + /** + * Nx executes the workspace TypeScript (plugins, generators, executors, `just.config.ts`, ...) + * through `@swc-node/register`, handing it the raw `compilerOptions` of the root + * `tsconfig.base.json`. `@swc-node/register@1.9.2` falls back to the pre TypeScript 6 default + * of `esModuleInterop: false` whenever the option is not spelled out, which drops the interop + * wrappers and turns every `import x from ''` into `undefined` at runtime. + */ + it(`should fall back to "esModuleInterop: false", which the root tsconfig has to compensate for`, () => { + expect(tsCompilerOptionsToSwcConfig({}, 'file.ts').esModuleInterop).toBe(false); + + const rootTsConfig = JSON.parse(fs.readFileSync(path.join(workspaceRoot, 'tsconfig.base.json'), 'utf-8')); + + expect(rootTsConfig.compilerOptions.esModuleInterop).toBe(true); + expect( + tsCompilerOptionsToSwcConfig({ esModuleInterop: rootTsConfig.compilerOptions.esModuleInterop }, 'file.ts') + .esModuleInterop, + ).toBe(true); + }); + }); +}); diff --git a/scripts/package-manager/tsconfig.lib.json b/scripts/package-manager/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/package-manager/tsconfig.lib.json +++ b/scripts/package-manager/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/package-manager/tsconfig.spec.json b/scripts/package-manager/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/package-manager/tsconfig.spec.json +++ b/scripts/package-manager/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/perf-test-flamegrill/tsconfig.lib.json b/scripts/perf-test-flamegrill/tsconfig.lib.json index f21721b5ee0d1..d33c2d4266d36 100644 --- a/scripts/perf-test-flamegrill/tsconfig.lib.json +++ b/scripts/perf-test-flamegrill/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019", "DOM"], "jsx": "react", diff --git a/scripts/perf-test-flamegrill/tsconfig.spec.json b/scripts/perf-test-flamegrill/tsconfig.spec.json index 5c32da1788b0c..9d48f4405c366 100644 --- a/scripts/perf-test-flamegrill/tsconfig.spec.json +++ b/scripts/perf-test-flamegrill/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/prettier/tsconfig.lib.json b/scripts/prettier/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/prettier/tsconfig.lib.json +++ b/scripts/prettier/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/prettier/tsconfig.spec.json b/scripts/prettier/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/prettier/tsconfig.spec.json +++ b/scripts/prettier/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/projects-test/tsconfig.lib.json b/scripts/projects-test/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/projects-test/tsconfig.lib.json +++ b/scripts/projects-test/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/projects-test/tsconfig.spec.json b/scripts/projects-test/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/projects-test/tsconfig.spec.json +++ b/scripts/projects-test/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/puppeteer/src/utils.ts b/scripts/puppeteer/src/utils.ts index 27d6c40b3b58d..6c801a0a3f4bf 100644 --- a/scripts/puppeteer/src/utils.ts +++ b/scripts/puppeteer/src/utils.ts @@ -4,16 +4,20 @@ import type { LaunchOptions } from './types'; export async function launch(options: LaunchOptions = {}) { const maxAttempts = 5; + const launchOptions = + process.platform === 'linux' && process.env.CI + ? { ...options, args: [...(options.args ?? []), '--no-sandbox', '--disable-setuid-sandbox'] } + : options; let attempt = 1; let browser: puppeteer.Browser | undefined; - console.log(`puppeteer: launching with settings: ${JSON.stringify(options)}`); + console.log(`puppeteer: launching with settings: ${JSON.stringify(launchOptions)}`); while (!browser) { try { - browser = await puppeteer.launch(options); + browser = await puppeteer.launch(launchOptions); console.log('puppeteer: launched...'); } catch (err) { if (attempt === maxAttempts) { diff --git a/scripts/puppeteer/tsconfig.lib.json b/scripts/puppeteer/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/puppeteer/tsconfig.lib.json +++ b/scripts/puppeteer/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/puppeteer/tsconfig.spec.json b/scripts/puppeteer/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/puppeteer/tsconfig.spec.json +++ b/scripts/puppeteer/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/storybook/src/utils.js b/scripts/storybook/src/utils.js index 92e45d574ca3c..55cbb16363b8f 100644 --- a/scripts/storybook/src/utils.js +++ b/scripts/storybook/src/utils.js @@ -291,7 +291,7 @@ function getPackageStoriesGlob(options) { function getMetadata( /** @type {string}*/ packageName, /** @type {ReturnType}*/ allProjects, - /** @type {Partial<{throwIfNotFound:boolean}>}*/ _options, + /** @type {Partial<{throwIfNotFound:boolean}>}*/ _options = {}, ) { const { throwIfNotFound = true } = { ..._options }; const metadata = allProjects.get(packageName); @@ -314,12 +314,22 @@ function getPackageStoriesGlob(options) { * @param {Object} options * @param {string} options.configFile - absolute path to tsconfig that contains path aliases * @param {Configuration} options.config - webpack config + * @param {string=} options.baseUrl - explicit absolute directory `paths` entries are resolved against. + * TypeScript 6 deprecated `baseUrl`, so `paths` are resolved relative to the config file that + * *declares* them - which, through an `extends` chain, is not necessarily `configFile` itself. + * `TsconfigPathsPlugin` cannot see that identity: without an explicit `baseUrl` it falls back to + * anchoring `paths` at the directory of `configFile`, silently breaking aliases declared in a shared + * base config. Pass the `absoluteBaseUrl` returned by `@fluentui/scripts-cypress`'s + * `readWorkspacePathAliases(configFile)` (or an equivalent `pathsBasePath` lookup) here to keep that + * mapping correct. Omitted for backward compatibility with existing callers whose aliases are declared + * directly in `configFile` (where the plugin's own fallback is already correct). * @returns */ function registerTsPaths(options) { - const { config, configFile } = options; + const { config, configFile, baseUrl } = options; const tsPaths = new TsconfigPathsPlugin({ configFile, + baseUrl, }); config.resolve = config.resolve ?? {}; diff --git a/scripts/storybook/src/utils.spec.js b/scripts/storybook/src/utils.spec.js index af96711ae6ee3..f21cf5ca2f15e 100644 --- a/scripts/storybook/src/utils.spec.js +++ b/scripts/storybook/src/utils.spec.js @@ -6,12 +6,14 @@ const { getAllPackageInfo } = require('@fluentui/scripts-monorepo'); const { stripIndents, workspaceRoot } = require('@nx/devkit'); const semver = require('semver'); const tmp = require('tmp'); +const { TsconfigPathsPlugin } = require('tsconfig-paths-webpack-plugin'); const { loadWorkspaceAddon, getPackageStoriesGlob, getImportMappingsForExportToSandboxAddon, processBabelLoaderOptions, + registerTsPaths, } = require('./utils'); tmp.setGracefulCleanup(); @@ -222,6 +224,79 @@ describe(`utils`, () => { }); }); + describe(`#registerTsPaths`, () => { + /** + * `TsconfigPathsPlugin`'s constructor eagerly reads `configFile` off disk, so a real fixture is + * required (an arbitrary/non-existent path throws synchronously before any of the assertions here + * are reached). + * @param {{compilerOptions?: Record}} [overrides] + */ + function writeTsConfigFixture(overrides = {}) { + const { name: rootDir } = tmp.dirSync({ prefix: 'sb-utils-register-ts-paths', unsafeCleanup: true }); + const tsConfigPath = path.join(rootDir, 'tsconfig.json'); + fs.writeFileSync( + tsConfigPath, + JSON.stringify({ compilerOptions: { paths: { '@proj/*': ['./src/*'] }, ...overrides.compilerOptions } }), + 'utf-8', + ); + return tsConfigPath; + } + + /** + * `registerTsPaths` always sets `config.resolve.plugins`, but `Configuration['resolve']['plugins']` + * is typed as optional - this narrows that away for the assertions below instead of repeating an + * unsafe optional chain (`?.`) at every call site. + * @param {import('webpack').Configuration} config + */ + function getRegisteredPlugins(config) { + if (!config.resolve || !config.resolve.plugins) { + throw new Error('expected registerTsPaths to have set config.resolve.plugins'); + } + return config.resolve.plugins; + } + + it(`registers a single TsconfigPathsPlugin instance on the webpack config`, () => { + const configFile = writeTsConfigFixture(); + /** @type {import('webpack').Configuration} */ + const config = {}; + + registerTsPaths({ config, configFile }); + + const plugins = getRegisteredPlugins(config); + expect(plugins).toHaveLength(1); + expect(plugins[0]).toBeInstanceOf(TsconfigPathsPlugin); + expect(/** @type {TsconfigPathsPlugin} */ (plugins[0]).baseUrl).toBeUndefined(); + }); + + it(`threads an explicit baseUrl through to TsconfigPathsPlugin, backward compatibly (no baseUrl -> plugin's own fallback)`, () => { + const configFile = writeTsConfigFixture(); + /** @type {import('webpack').Configuration} */ + const config = {}; + + registerTsPaths({ + config, + configFile, + baseUrl: '/workspace', + }); + + const plugin = /** @type {TsconfigPathsPlugin} */ (getRegisteredPlugins(config)[0]); + expect(plugin.baseUrl).toBe('/workspace'); + }); + + it(`replaces a previously registered TsconfigPathsPlugin instead of stacking a second one`, () => { + const configFile = writeTsConfigFixture(); + /** @type {import('webpack').Configuration} */ + const config = {}; + + registerTsPaths({ config, configFile }); + registerTsPaths({ config, configFile, baseUrl: '/workspace' }); + + const plugins = getRegisteredPlugins(config); + expect(plugins).toHaveLength(1); + expect(/** @type {TsconfigPathsPlugin} */ (plugins[0]).baseUrl).toBe('/workspace'); + }); + }); + describe(`#getPackageStoriesGlob`, () => { it(`should generate storybook stories string array of glob based on package.json#dependencies field`, () => { const actual = getPackageStoriesGlob({ diff --git a/scripts/storybook/tsconfig.lib.json b/scripts/storybook/tsconfig.lib.json index 040fb0e3c58a1..b3b5bfd36c8ef 100644 --- a/scripts/storybook/tsconfig.lib.json +++ b/scripts/storybook/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/storybook/tsconfig.spec.json b/scripts/storybook/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/storybook/tsconfig.spec.json +++ b/scripts/storybook/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/tasks/bin/type-check-project.js b/scripts/tasks/bin/type-check-project.js new file mode 100755 index 0000000000000..88bc970e8c83c --- /dev/null +++ b/scripts/tasks/bin/type-check-project.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +// @ts-check + +const { joinPathFragments } = require('@nx/devkit'); +const { registerTsProject } = require('@nx/js/src/internal'); + +registerTsProject(joinPathFragments(__dirname, '..', 'tsconfig.lib.json')); + +require('../src/type-check-project').main(); diff --git a/scripts/tasks/bin/type-check-project.spec.ts b/scripts/tasks/bin/type-check-project.spec.ts new file mode 100644 index 0000000000000..ce5d3caff47c7 --- /dev/null +++ b/scripts/tasks/bin/type-check-project.spec.ts @@ -0,0 +1,42 @@ +import { spawnSync } from 'node:child_process'; +import * as path from 'node:path'; + +/** + * `bin/type-check-project.js` is a plain (`@ts-check`'d, but untranspiled) Node.js CLI entry point, + * not part of any TS project the compiler builds - so it needs its own direct coverage: a syntax + * check (it's the sort of file that's easy to break without anyone noticing, since it's neither + * compiled nor executed by `tsc`/jest by default), plus a wiring test that it registers the + * workspace's TS project and delegates to `../src/type-check-project`'s `main()`. + */ +describe(`bin/type-check-project.js`, () => { + const binPath = path.join(__dirname, 'type-check-project.js'); + + it(`should be valid, executable JavaScript`, () => { + const result = spawnSync(process.execPath, ['--check', binPath], { encoding: 'utf-8' }); + + expect(result.stderr).toEqual(''); + expect(result.status).toEqual(0); + }); + + it(`should register the workspace TS project and delegate to type-check-project's main()`, () => { + jest.resetModules(); + + const registerTsProject = jest.fn(); + const main = jest.fn(); + + jest.doMock('@nx/devkit', () => ({ joinPathFragments: (...segments: string[]) => path.join(...segments) })); + jest.doMock('@nx/js/src/internal', () => ({ registerTsProject })); + jest.doMock('../src/type-check-project', () => ({ main })); + + jest.isolateModules(() => { + require('./type-check-project'); + }); + + expect(registerTsProject).toHaveBeenCalledWith(path.join(__dirname, '..', 'tsconfig.lib.json')); + expect(main).toHaveBeenCalledTimes(1); + + jest.dontMock('@nx/devkit'); + jest.dontMock('@nx/js/src/internal'); + jest.dontMock('../src/type-check-project'); + }); +}); diff --git a/scripts/tasks/project.json b/scripts/tasks/project.json index 56f6829680638..4470405c23398 100644 --- a/scripts/tasks/project.json +++ b/scripts/tasks/project.json @@ -3,5 +3,16 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "scripts/tasks/src", "projectType": "library", - "tags": ["tools"] + "tags": ["tools"], + "targets": { + "test": { + "inputs": [ + "default", + "^production", + "{workspaceRoot}/jest.preset.js", + "{workspaceRoot}/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts", + "{workspaceRoot}/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts" + ] + } + } } diff --git a/scripts/tasks/src/api-extractor.spec.ts b/scripts/tasks/src/api-extractor.spec.ts new file mode 100644 index 0000000000000..09559d16264ce --- /dev/null +++ b/scripts/tasks/src/api-extractor.spec.ts @@ -0,0 +1,152 @@ +import type { ExtractorResult } from '@microsoft/api-extractor'; +import type { ApiExtractorOptions } from 'just-scripts'; +import { logger } from 'just-scripts'; + +import { apiExtractor } from './api-extractor'; +import { assertSelfContainedDtsRollups } from './dts-rollup'; + +/** + * `just-scripts` vendors its own `ExtractorMessage` declaration (distinct from - and nominally + * incompatible with - `@microsoft/api-extractor`'s), so `messageCallback`'s parameter type has to be + * derived from `ApiExtractorOptions` itself rather than imported directly. + */ +type CapturedMessage = Parameters>[0]; + +/** + * The subset of {@link ApiExtractorOptions} that `onResult` wiring tests need to invoke directly - + * `apiExtractorVerifyTask` itself is mocked away, so its callbacks have to be captured and called by hand. + */ +type CapturedApiExtractorOptions = ApiExtractorOptions & { + onResult: NonNullable; + messageCallback: NonNullable; +}; + +const apiExtractorVerifyTaskCalls: CapturedApiExtractorOptions[] = []; + +jest.mock('just-scripts', () => { + const noop = () => undefined; + + return { + task: jest.fn((_name: string, definition: unknown) => definition), + series: jest.fn((...tasks: unknown[]) => tasks), + logger: { info: noop, warn: noop, error: jest.fn(), verbose: noop }, + apiExtractorVerifyTask: jest.fn((options: CapturedApiExtractorOptions) => { + apiExtractorVerifyTaskCalls.push(options); + return noop; + }), + }; +}); + +jest.mock('glob', () => ({ sync: jest.fn(() => ['/proj/config/api-extractor.json']) })); + +jest.mock('./argv', () => ({ getJustArgv: jest.fn(() => ({})) })); + +jest.mock('./utils', () => ({ + getTsPathAliasesConfig: jest.fn(() => ({ + isUsingTsSolutionConfigs: true, + packageJson: { name: '@fluentui/react-theme' }, + tsConfigs: {}, + })), + getTsPathAliasesApiExtractorConfig: jest.fn(), +})); + +jest.mock('./dts-rollup', () => ({ assertSelfContainedDtsRollups: jest.fn() })); + +const assertSelfContainedDtsRollupsMockFn = assertSelfContainedDtsRollups as jest.MockedFunction< + typeof assertSelfContainedDtsRollups +>; +const loggerErrorMockFn = logger.error as jest.MockedFunction; + +function getCapturedOnResult() { + expect(apiExtractorVerifyTaskCalls).toHaveLength(1); + return apiExtractorVerifyTaskCalls[0]; +} + +/** + * `onResult` wiring must match the vNext `generate-api` executor (`tools/workspace-plugin/src/executors/generate-api/executor.ts`): + * the rollup guard is a self contained `.d.ts` rollup check, a different class of problem than API + * Extractor's own diagnostics, and it must never mask (or be masked by) those diagnostics. + */ +describe(`apiExtractor onResult wiring`, () => { + beforeEach(() => { + jest.clearAllMocks(); + apiExtractorVerifyTaskCalls.length = 0; + }); + + it(`does not run the self contained rollup guard when API Extractor failed`, () => { + apiExtractor(); + const { onResult } = getCapturedOnResult(); + const fakeExtractorConfig = { projectFolder: '/proj' } as unknown as ExtractorResult['extractorConfig']; + + onResult({ succeeded: false, extractorConfig: fakeExtractorConfig } as ExtractorResult, {} as never); + + expect(assertSelfContainedDtsRollupsMockFn).not.toHaveBeenCalled(); + }); + + it(`surfaces missing dependency type declarations before skipping the rollup guard on failure`, () => { + apiExtractor(); + const { onResult, messageCallback } = getCapturedOnResult(); + const fakeExtractorConfig = { projectFolder: '/proj' } as unknown as ExtractorResult['extractorConfig']; + + messageCallback({ + category: 'Compiler', + messageId: 'TS7016', + text: `Could not find a declaration file for module '@fluentui/react-theme'`, + } as CapturedMessage); + + onResult({ succeeded: false, extractorConfig: fakeExtractorConfig } as ExtractorResult, {} as never); + + expect(loggerErrorMockFn).toHaveBeenCalledWith( + expect.stringContaining('MISSING DEPENDENCY TYPE DECLARATIONS'), + expect.stringContaining('@fluentui/react-theme'), + '\n', + expect.stringContaining('generate-api'), + '\n', + ); + expect(assertSelfContainedDtsRollupsMockFn).not.toHaveBeenCalled(); + }); + + it(`runs the self contained rollup guard once API Extractor succeeded`, () => { + apiExtractor(); + const { onResult } = getCapturedOnResult(); + const fakeExtractorConfig = { projectFolder: '/proj' } as unknown as ExtractorResult['extractorConfig']; + + onResult({ succeeded: true, extractorConfig: fakeExtractorConfig } as ExtractorResult, {} as never); + + expect(assertSelfContainedDtsRollupsMockFn).toHaveBeenCalledTimes(1); + expect(assertSelfContainedDtsRollupsMockFn).toHaveBeenCalledWith(fakeExtractorConfig, { + scannedFilePaths: expect.any(Set), + }); + }); + + it(`shares scannedFilePaths across every config executed within the same run`, () => { + // two `config/api-extractor*.json` files -> two entry point configs executed by one `apiExtractor()` call + jest + .requireMock('glob') + .sync.mockReturnValueOnce(['/proj/config/api-extractor.json', '/proj/config/api-extractor.fast.json']); + + apiExtractor(); + + expect(apiExtractorVerifyTaskCalls).toHaveLength(2); + + const fakeExtractorConfig = { projectFolder: '/proj' } as unknown as ExtractorResult['extractorConfig']; + apiExtractorVerifyTaskCalls[0].onResult( + { succeeded: true, extractorConfig: fakeExtractorConfig } as ExtractorResult, + {} as never, + ); + apiExtractorVerifyTaskCalls[1].onResult( + { succeeded: true, extractorConfig: fakeExtractorConfig } as ExtractorResult, + {} as never, + ); + + expect(assertSelfContainedDtsRollupsMockFn).toHaveBeenCalledTimes(2); + const [firstCallScannedFilePaths] = assertSelfContainedDtsRollupsMockFn.mock.calls[0].slice(1) as [ + { scannedFilePaths: Set }, + ]; + const [secondCallScannedFilePaths] = assertSelfContainedDtsRollupsMockFn.mock.calls[1].slice(1) as [ + { scannedFilePaths: Set }, + ]; + + expect(firstCallScannedFilePaths.scannedFilePaths).toBe(secondCallScannedFilePaths.scannedFilePaths); + }); +}); diff --git a/scripts/tasks/src/api-extractor.ts b/scripts/tasks/src/api-extractor.ts index 52d037080d866..d012de59342ff 100644 --- a/scripts/tasks/src/api-extractor.ts +++ b/scripts/tasks/src/api-extractor.ts @@ -5,10 +5,12 @@ import { workspaceRoot } from '@nx/devkit'; import chalk from 'chalk'; import { isCI } from 'ci-info'; import * as glob from 'glob'; -import { ApiExtractorOptions, TaskFunction, apiExtractorVerifyTask, logger, series, task } from 'just-scripts'; +import type { ApiExtractorOptions, TaskFunction } from 'just-scripts'; +import { apiExtractorVerifyTask, logger, series, task } from 'just-scripts'; import type * as ApiExtractorTypes from 'just-scripts/src/tasks/apiExtractorTypes'; import { getJustArgv } from './argv'; +import { assertSelfContainedDtsRollups } from './dts-rollup'; import { getTsPathAliasesApiExtractorConfig, getTsPathAliasesConfig } from './utils'; const compilerMessages = { @@ -55,6 +57,11 @@ export function apiExtractor(): TaskFunction { TS7016: [] as string[], TS2305: [] as string[], }; + /** + * Shared across every config executed by this task so that a rollup emitted by more than one entry + * point config is only parsed once. + */ + const scannedRollupPaths = new Set(); const args: ReturnType & Partial = getJustArgv(); const { isUsingTsSolutionConfigs, packageJson, tsConfigs } = getTsPathAliasesConfig(); @@ -137,15 +144,9 @@ export function apiExtractor(): TaskFunction { } function onResult(result: ExtractorResult, _extractorOptions: ApiExtractorTypes.IExtractorInvokeOptions): void { - if (!isUsingTsSolutionConfigs) { - return; - } - - if (result.succeeded === true) { - return; - } - - if (messages.TS7016.length) { + // surface the actionable api-extractor diagnostics first - the rollup guard reports a different class + // of problem and must not mask them + if (isUsingTsSolutionConfigs && result.succeeded !== true && messages.TS7016.length) { const errTitle = [ chalk.bgRed.white.bold(`api-extractor | MISSING DEPENDENCY TYPE DECLARATIONS:`), chalk.red(` Package dependencies are missing index.d.ts type definitions:`), @@ -160,6 +161,13 @@ export function apiExtractor(): TaskFunction { logger.error(errTitle, logErr, '\n', logFix, '\n'); } + + // matches the vNext `generate-api` executor: the rollup guard reports a different class of problem + // than API Extractor's own diagnostics, so it must only run once API Extractor itself succeeded - + // otherwise a rollup guard failure would mask (or be masked by) the real API Extractor errors above. + if (result.succeeded === true) { + assertSelfContainedDtsRollups(result.extractorConfig, { scannedFilePaths: scannedRollupPaths }); + } } } diff --git a/scripts/tasks/src/dts-rollup.spec.ts b/scripts/tasks/src/dts-rollup.spec.ts new file mode 100644 index 0000000000000..d2362aecf5cf4 --- /dev/null +++ b/scripts/tasks/src/dts-rollup.spec.ts @@ -0,0 +1,307 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import * as os from 'node:os'; +import { join } from 'node:path'; + +import { workspaceRoot } from '@nx/devkit'; + +import { + type DtsRollupConfig, + assertSelfContainedDtsRollups, + findRelativeImportsInDtsRollup, + getGeneratedDtsRollupPaths, +} from './dts-rollup'; + +/** + * ⚠️ SHARED SOURCE — this spec exists byte identical next to both copies of `dts-rollup.ts`. + * See the module doc comment in `dts-rollup.ts` for why the module cannot be extracted into a library. + */ + +describe(`shared source parity`, () => { + const sharedSources = [ + ['tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts', 'scripts/tasks/src/dts-rollup.ts'], + [ + 'tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts', + 'scripts/tasks/src/dts-rollup.spec.ts', + ], + ] as const; + + it.each(sharedSources)(`'%s' should stay byte identical with '%s'`, (a, b) => { + expect(readFileSync(join(workspaceRoot, a), 'utf-8')).toEqual(readFileSync(join(workspaceRoot, b), 'utf-8')); + }); +}); + +describe(`findRelativeImportsInDtsRollup`, () => { + it(`should return no violations for a self contained rollup`, () => { + const rollup = [ + `import type { ESLint } from 'eslint';`, + `import { RuleModule } from '@typescript-eslint/utils/ts-eslint';`, + `import * as React_2 from 'react';`, + ``, + `export declare const Overflow: React_2.ForwardRefExoticComponent;`, + ``, + `export { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should report relative static imports`, () => { + const rollup = [ + `import { BreadcrumbProps as BreadcrumbProps_2 } from './Breadcrumb.types';`, + `import type { ButtonProps } from '@fluentui/react-button';`, + `import { FieldState as FieldState_2 } from '..';`, + `import defaultExport from '.';`, + `import '../side-effect';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types', '..', '.', '../side-effect']); + }); + + it(`should report relative re-exports and deduplicate specifiers`, () => { + const rollup = [ + `export { RuleOptions } from './rules/enforce-use-client';`, + `export type { RuleOptions as RuleOptions_2 } from './rules/enforce-use-client';`, + `export * from './rules/enforce-use-client';`, + `export * as rules from './rules';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./rules/enforce-use-client', './rules']); + }); + + it(`should report inline import() types on exported declarations`, () => { + const rollup = [ + `export declare const BreadcrumbProvider: Provider;`, + `export declare function useField(): import("./contexts/FieldContext").FieldState;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types', './contexts/FieldContext']); + }); + + it(`should report inline import() types on declarations that are not exported`, () => { + const rollup = [ + `declare const internalContext: import('./internal/Context').ContextValue;`, + `declare type Internal = {`, + ` nested: Array;`, + `};`, + `export { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./internal/Context', '../shared/types']); + }); + + it(`should report relative specifiers spread over multiple lines`, () => { + const rollup = [ + `import {`, + ` OverflowItemProps,`, + ` OverflowProps`, + `} from './Overflow.types';`, + ``, + `export declare const useOverflow: () => import(`, + ` '../hooks/useOverflowContext'`, + `).OverflowContextValue;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Overflow.types', '../hooks/useOverflowContext']); + }); + + it(`should report 'import x = require()' specifiers`, () => { + const rollup = [`import legacy = require('./legacy');`, `import external = require('lodash');`].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./legacy']); + }); + + it(`should report relative module augmentations`, () => { + const rollup = [ + `declare module './augmented' {`, + ` interface Extra { }`, + `}`, + `declare module '@fluentui/react-theme' {`, + ` interface Theme { }`, + `}`, + `declare module Namespaced { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./augmented']); + }); + + it(`should report specifiers that use the Windows path separator`, () => { + const rollup = [ + String.raw`import { Foo } from '.\\Foo.types';`, + String.raw`export declare const bar: import('..\\shared\\types').Bar;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([String.raw`.\Foo.types`, String.raw`..\shared\types`]); + }); + + it(`should not report package specifiers, 'node:' builtins, or URLs`, () => { + const rollup = [ + `import { compiler } from 'markdown-to-jsx';`, + `import type { Options } from '@fluentui/react-utilities';`, + `import prettier from 'prettier/parser-html.js';`, + `import node from 'node:path';`, + `import url from 'https://example.com/module.js';`, + `import protocolRelative from '//example.com/module.js';`, + `export declare const theme: import('@fluentui/react-theme').Theme;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should report absolute filesystem specifiers`, () => { + const rollup = [ + `import posix from '/opt/generated/types';`, + String.raw`import windows from 'C:\\generated\\types';`, + `export declare const alt: import('/opt/generated/alt').Alt;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([ + '/opt/generated/types', + String.raw`C:\generated\types`, + '/opt/generated/alt', + ]); + }); + + it(`should report triple-slash reference path directives`, () => { + const rollup = [ + `/// `, + `/// `, + `/// `, + `export declare const Foo: number;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types.d.ts', '/opt/generated/types.d.ts']); + }); + + it(`should not report string literal types that look like relative or absolute specifiers`, () => { + const rollup = [ + `export declare const numberFormat: '.2f';`, + `export declare type Separator = '.' | '..' | './';`, + `export declare function format(spec: '.2f' | '.0%'): string;`, + `export declare const from: "from './types'";`, + `export declare const doc: {`, + ` value: '../not-an-import';`, + `};`, + `export declare const rootPath: '/opt/generated/types';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should not report relative specifiers that only appear inside comments`, () => { + const rollup = [ + `/**`, + ` * @example`, + ` * import { Foo } from './types';`, + ` */`, + `export declare const Foo: number;`, + `// export * from '../internal';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); +}); + +describe(`getGeneratedDtsRollupPaths / assertSelfContainedDtsRollups`, () => { + let projectFolder: string; + + const selfContained = `export declare const Foo: number;\nexport { }\n`; + const broken = `export declare const Foo: import('./types').Foo;\nexport { }\n`; + + function createConfig(overrides: Partial = {}): DtsRollupConfig { + return { + projectFolder, + rollupEnabled: true, + untrimmedFilePath: '', + alphaTrimmedFilePath: '', + betaTrimmedFilePath: '', + publicTrimmedFilePath: '', + ...overrides, + }; + } + + function writeRollup(fileName: string, contents: string) { + const filePath = join(projectFolder, fileName); + writeFileSync(filePath, contents, 'utf-8'); + return filePath; + } + + beforeEach(() => { + // written under the OS temp directory (never inside a source tree) so an interrupted run cannot leave + // fixture files behind for git/tsconfig to pick up + projectFolder = mkdtempSync(join(os.tmpdir(), 'dts-rollup-')); + }); + + afterEach(() => { + rmSync(projectFolder, { recursive: true, force: true }); + }); + + it(`should return no paths when the rollup is disabled`, () => { + const untrimmedFilePath = writeRollup('index.d.ts', broken); + const config = createConfig({ rollupEnabled: false, untrimmedFilePath }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should skip configured rollups that were not emitted`, () => { + const config = createConfig({ untrimmedFilePath: join(projectFolder, 'missing.d.ts') }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should collect every enabled rollup variant that exists`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', selfContained), + publicTrimmedFilePath: writeRollup('index.public.d.ts', selfContained), + betaTrimmedFilePath: writeRollup('index.beta.d.ts', selfContained), + alphaTrimmedFilePath: join(projectFolder, 'index.alpha.d.ts'), + }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([ + join(projectFolder, 'index.d.ts'), + join(projectFolder, 'index.public.d.ts'), + join(projectFolder, 'index.beta.d.ts'), + ]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should throw for a trimmed rollup even when the untrimmed rollup is self contained`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', selfContained), + publicTrimmedFilePath: writeRollup('index.public.d.ts', broken), + }); + + expect(() => assertSelfContainedDtsRollups(config)).toThrow(/index\.public\.d\.ts imports modules/); + expect(() => assertSelfContainedDtsRollups(config)).toThrow(/- \.\/types/); + }); + + it(`should report every violating rollup variant in a single error`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', broken), + betaTrimmedFilePath: writeRollup('index.beta.d.ts', broken), + }); + + let message = ''; + try { + assertSelfContainedDtsRollups(config); + } catch (err) { + message = (err as Error).message; + } + + expect(message).toContain('index.d.ts imports modules'); + expect(message).toContain('index.beta.d.ts imports modules'); + }); + + it(`should not scan the same rollup twice within one run`, () => { + const untrimmedFilePath = writeRollup('index.d.ts', broken); + const scannedFilePaths = new Set(); + const config = createConfig({ untrimmedFilePath }); + + expect(() => assertSelfContainedDtsRollups(config, { scannedFilePaths })).toThrow(/BROKEN TYPE DECLARATION ROLLUP/); + expect(scannedFilePaths).toEqual(new Set([untrimmedFilePath])); + + // a second entry point config emitting the same rollup is a no-op + expect(() => assertSelfContainedDtsRollups(config, { scannedFilePaths })).not.toThrow(); + }); +}); diff --git a/scripts/tasks/src/dts-rollup.ts b/scripts/tasks/src/dts-rollup.ts new file mode 100644 index 0000000000000..f63521927c847 --- /dev/null +++ b/scripts/tasks/src/dts-rollup.ts @@ -0,0 +1,223 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { ExtractorConfig } from '@microsoft/api-extractor'; +import { workspaceRoot } from '@nx/devkit'; +import * as ts from 'typescript'; + +/** + * ⚠️ SHARED SOURCE — this module exists byte identical in two places: + * + * - `tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts` (Nx `generate-api` executor, v9) + * - `scripts/tasks/src/dts-rollup.ts` (legacy `just-scripts` api-extractor task, v8) + * + * It cannot be extracted into a shared library because `tools/workspace-plugin` must not depend on any + * project within the monorepo - `tools/workspace-plugin/scripts/check-dep-graph.js` fails the build if it + * does. The spec next to each copy asserts that the two never diverge. + */ + +/** + * The subset of {@link ExtractorConfig} that describes the `.d.ts` rollup outputs. + * + * Declared structurally so the guard can be exercised without constructing a full api-extractor config. + */ +export type DtsRollupConfig = Pick< + ExtractorConfig, + | 'projectFolder' + | 'rollupEnabled' + | 'untrimmedFilePath' + | 'alphaTrimmedFilePath' + | 'betaTrimmedFilePath' + | 'publicTrimmedFilePath' +>; + +type DtsRollupFilePathKey = Exclude; + +/** + * Every rollup variant api-extractor can emit, in the order violations are reported. + */ +const rollupFilePathKeys: readonly DtsRollupFilePathKey[] = [ + 'untrimmedFilePath', + 'publicTrimmedFilePath', + 'betaTrimmedFilePath', + 'alphaTrimmedFilePath', +]; + +/** + * Mirrors TypeScript's `isExternalModuleNameRelative`, which also accepts the Windows path separator. + */ +function isRelativeModuleSpecifier(moduleSpecifier: string): boolean { + return moduleSpecifier === '.' || moduleSpecifier === '..' || /^\.\.?[/\\]/.test(moduleSpecifier); +} + +/** + * Detects an absolute filesystem path - as opposed to a package specifier (`@fluentui/react-theme`), a + * `node:` builtin, or a URL (`https://...`) - none of which are portable outside of the machine/environment + * that generated the rollup. + */ +function isAbsoluteFilesystemModuleSpecifier(moduleSpecifier: string): boolean { + // POSIX absolute path, e.g. '/opt/generated/types'. A leading '//' is excluded because it denotes a + // protocol relative URL (`//example.com/...`), not a filesystem path. + if (moduleSpecifier.startsWith('/') && !moduleSpecifier.startsWith('//')) { + return true; + } + + // Windows absolute path with a drive letter, e.g. 'C:\generated\types' or 'C:/generated/types'. A URL + // scheme such as 'https:' never matches because schemes are more than one character before the colon. + return /^[A-Za-z]:[\\/]/.test(moduleSpecifier); +} + +/** + * Finds module specifiers that a `.d.ts` rollup must never contain. + * + * A rollup is published as a single self contained file, so every relative or absolute-filesystem + * specifier within it points to a module that does not exist for consumers. + * + * This happens when TypeScript declaration output references a type through an inline `import('./module')` + * type: API Extractor resolves such references as external packages whenever a modern `moduleResolution` + * (`bundler`, `node16`, `nodenext`) is used, instead of inlining the referenced declaration. It can also + * happen through a triple-slash `/// ` directive, which API Extractor + * does not rewrite either. + * The fix belongs in the source file - annotate the exported value with a type that is imported statically. + * + * The rollup is parsed with the TypeScript compiler instead of scanned with a regular expression so that + * only real module specifiers are reported - a string literal *type* such as `'.'` or `'.2f'` is not a + * module specifier and must not be flagged. + * + * @see https://github.com/microsoft/rushstack/issues/3335 + */ +export function findRelativeImportsInDtsRollup(rollupContents: string): string[] { + const sourceFile = ts.createSourceFile( + 'dts-rollup.d.ts', + rollupContents, + ts.ScriptTarget.Latest, + /* setParentNodes */ false, + ts.ScriptKind.TS, + ); + const moduleSpecifiers = new Set(); + + // `/// ` - collected separately by the parser, not part of the AST + for (const reference of sourceFile.referencedFiles) { + addIfNotSelfContained(reference.fileName); + } + + visit(sourceFile); + + return [...moduleSpecifiers]; + + function visit(node: ts.Node): void { + collectModuleSpecifier(node); + ts.forEachChild(node, visit); + } + + function collectModuleSpecifier(node: ts.Node): void { + // `import ... from './module'`, `export ... from './module'`, `export * from './module'` + if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { + addIfNotSelfContainedNode(node.moduleSpecifier); + return; + } + + // `import('./module').Foo` - anywhere in a type position, exported or not + if (ts.isImportTypeNode(node)) { + addIfNotSelfContainedNode(ts.isLiteralTypeNode(node.argument) ? node.argument.literal : undefined); + return; + } + + // `import Foo = require('./module')` + if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) { + addIfNotSelfContainedNode(node.moduleReference.expression); + return; + } + + // `declare module './module' { ... }` + if (ts.isModuleDeclaration(node)) { + addIfNotSelfContainedNode(node.name); + } + } + + function addIfNotSelfContainedNode(node: ts.Node | undefined): void { + if (!node || !ts.isStringLiteralLike(node)) { + return; + } + + addIfNotSelfContained(node.text); + } + + function addIfNotSelfContained(moduleSpecifier: string): void { + if (isRelativeModuleSpecifier(moduleSpecifier) || isAbsoluteFilesystemModuleSpecifier(moduleSpecifier)) { + moduleSpecifiers.add(moduleSpecifier); + } + } +} + +/** + * Resolves every `.d.ts` rollup that api-extractor was configured to emit and that exists on disk. + * + * A single config can emit up to four variants (untrimmed + public/beta/alpha trimmed); variants that were + * not configured are normalized to an empty path by api-extractor. + */ +export function getGeneratedDtsRollupPaths(extractorConfig: DtsRollupConfig): string[] { + if (!extractorConfig.rollupEnabled) { + return []; + } + + const rollupPaths = new Set(); + + for (const key of rollupFilePathKeys) { + const filePath = extractorConfig[key]; + + if (filePath && fs.existsSync(filePath)) { + rollupPaths.add(path.normalize(filePath)); + } + } + + return [...rollupPaths]; +} + +/** + * Fails when any generated `.d.ts` rollup imports a module that is not published alongside it. + * + * @param extractorConfig - the config api-extractor was invoked with + * @param options - `scannedFilePaths` is shared across invocations within a single api-extractor run so + * that a rollup emitted by more than one entry point config is parsed only once + */ +export function assertSelfContainedDtsRollups( + extractorConfig: DtsRollupConfig, + options: { scannedFilePaths?: Set } = {}, +): void { + const { scannedFilePaths } = options; + const violations: Array<{ filePath: string; moduleSpecifiers: string[] }> = []; + + for (const filePath of getGeneratedDtsRollupPaths(extractorConfig)) { + if (scannedFilePaths?.has(filePath)) { + continue; + } + scannedFilePaths?.add(filePath); + + const moduleSpecifiers = findRelativeImportsInDtsRollup(fs.readFileSync(filePath, 'utf-8')); + + if (moduleSpecifiers.length > 0) { + violations.push({ filePath, moduleSpecifiers }); + } + } + + if (violations.length === 0) { + return; + } + + throw new Error( + [ + `api-extractor | BROKEN TYPE DECLARATION ROLLUP:`, + ...violations.flatMap(violation => [ + ` ${path.relative(workspaceRoot, violation.filePath)} imports modules that are not published:`, + ...violation.moduleSpecifiers.map(moduleSpecifier => ` - ${moduleSpecifier}`), + ]), + ``, + ` This happens when declaration output references a type through an inline \`import('./module')\` type.`, + ` 🛠 FIX: annotate the affected export in ${path.relative( + workspaceRoot, + extractorConfig.projectFolder, + )} with a statically imported type.`, + ].join('\n'), + ); +} diff --git a/scripts/tasks/src/ecma-syntax.spec.ts b/scripts/tasks/src/ecma-syntax.spec.ts new file mode 100644 index 0000000000000..e12dec9b34dcd --- /dev/null +++ b/scripts/tasks/src/ecma-syntax.spec.ts @@ -0,0 +1,67 @@ +import { detectModuleShape, findSyntaxAboveTarget } from './ecma-syntax'; + +describe(`ecma-syntax`, () => { + describe(`#findSyntaxAboveTarget`, () => { + it.each([ + [`var a = function () {};`, []], + [`const a = 1;`, ['ConstDeclaration']], + [`let a = 1;`, ['LetDeclaration']], + [`var a = () => 1;`, ['ArrowFunction']], + ['var a = `hello`;', ['NoSubstitutionTemplateLiteral']], + [`class A {}`, ['ClassDeclaration']], + [`var a = [...b];`, ['SpreadElement']], + [`function a(...rest) {}`, ['RestParameter']], + [`function a(b = 1) {}`, ['DefaultParameter']], + [`for (var a of b) {}`, ['ForOfStatement']], + ])(`should report %j as above ES5`, (code, expected) => { + expect(findSyntaxAboveTarget(code, 'es5').map(feature => feature.name)).toEqual(expected); + }); + + it(`should not report syntax which the target supports`, () => { + const code = [`const a = () => 1;`, `class B {}`, `var c = { ...d };`].join('\n'); + + expect(findSyntaxAboveTarget(code, 'es2018')).toEqual([]); + expect(findSyntaxAboveTarget(code, 'es2015').map(feature => feature.name)).toEqual(['SpreadAssignment']); + }); + + it(`should report the minimum target of each construct`, () => { + expect(findSyntaxAboveTarget(`var a = b?.c ?? d;`, 'es2019')).toEqual([ + { name: 'NullishCoalescing', minTarget: 'es2020' }, + { name: 'OptionalChaining', minTarget: 'es2020' }, + ]); + expect(findSyntaxAboveTarget(`async function a() { await b; }`, 'es2016').map(f => f.minTarget)).toEqual([ + 'es2017', + 'es2017', + ]); + expect(findSyntaxAboveTarget(`try { a(); } catch { }`, 'es2018').map(f => f.name)).toEqual([ + 'OptionalCatchBinding', + ]); + // `class` itself is ES2015, only the class field is ES2022 + expect(findSyntaxAboveTarget(`class A { b = 1; }`, 'es2021').map(f => f.name)).toEqual(['PropertyDeclaration']); + }); + }); + + describe(`#detectModuleShape`, () => { + it(`should detect ESM`, () => { + expect(detectModuleShape(`import a from './a';\nexport var b = a;`)).toEqual('esm'); + expect(detectModuleShape(`export * from './a';`)).toEqual('esm'); + expect(detectModuleShape(`var a = 1;\nexport { a };`)).toEqual('esm'); + expect(detectModuleShape(`export default 1;`)).toEqual('esm'); + }); + + it(`should detect AMD`, () => { + expect(detectModuleShape(`define(["require", "exports"], function (require, exports) {});`)).toEqual('amd'); + // the "use strict" prologue must not confuse the detection + expect(detectModuleShape(`"use strict";\ndefine(["require"], function () {});`)).toEqual('amd'); + }); + + it(`should detect CommonJS`, () => { + expect(detectModuleShape(`"use strict";\nvar a = require("./a");\nexports.b = a;`)).toEqual('commonjs'); + expect(detectModuleShape(`module.exports = 1;`)).toEqual('commonjs'); + }); + + it(`should detect plain scripts`, () => { + expect(detectModuleShape(`"use strict";\nvar a = 1;`)).toEqual('script'); + }); + }); +}); diff --git a/scripts/tasks/src/ecma-syntax.ts b/scripts/tasks/src/ecma-syntax.ts new file mode 100644 index 0000000000000..f82e4413407b1 --- /dev/null +++ b/scripts/tasks/src/ecma-syntax.ts @@ -0,0 +1,195 @@ +import * as ts from 'typescript'; + +/** + * Static analysis of emitted JavaScript. + * + * Since TypeScript 6 the published ES5/AMD artifacts of v8 packages are not produced by `tsc` + * anymore (`target: 'es5'` and `module: 'amd'` were removed), but by a SWC post processing step. + * These helpers are the shared "did we emit what we promised" checks used both by unit tests of + * that step and by `verify-packaging`, which runs against the actual published files on CI. + */ + +export type EsTarget = 'es5' | 'es2015' | 'es2016' | 'es2017' | 'es2018' | 'es2019' | 'es2020' | 'es2021' | 'es2022'; + +const targetOrder: EsTarget[] = ['es5', 'es2015', 'es2016', 'es2017', 'es2018', 'es2019', 'es2020', 'es2021', 'es2022']; + +export interface EsFeature { + /** syntax construct which was found, eg `ArrowFunction` */ + name: string; + /** lowest ECMAScript version the construct can be emitted for */ + minTarget: EsTarget; +} + +/** + * Names are declared explicitly (instead of using the `ts.SyntaxKind` reverse mapping) because + * several kinds share a numeric value with a marker entry (eg `NoSubstitutionTemplateLiteral` maps + * back to `FirstTemplateToken`), which would make the reported feature names unstable. + */ +const featureByKind: Partial> = { + [ts.SyntaxKind.ArrowFunction]: { name: 'ArrowFunction', minTarget: 'es2015' }, + [ts.SyntaxKind.ClassDeclaration]: { name: 'ClassDeclaration', minTarget: 'es2015' }, + [ts.SyntaxKind.ClassExpression]: { name: 'ClassExpression', minTarget: 'es2015' }, + [ts.SyntaxKind.TemplateExpression]: { name: 'TemplateExpression', minTarget: 'es2015' }, + [ts.SyntaxKind.NoSubstitutionTemplateLiteral]: { name: 'NoSubstitutionTemplateLiteral', minTarget: 'es2015' }, + [ts.SyntaxKind.TaggedTemplateExpression]: { name: 'TaggedTemplateExpression', minTarget: 'es2015' }, + [ts.SyntaxKind.SpreadElement]: { name: 'SpreadElement', minTarget: 'es2015' }, + [ts.SyntaxKind.ShorthandPropertyAssignment]: { name: 'ShorthandPropertyAssignment', minTarget: 'es2015' }, + [ts.SyntaxKind.ObjectBindingPattern]: { name: 'ObjectBindingPattern', minTarget: 'es2015' }, + [ts.SyntaxKind.ArrayBindingPattern]: { name: 'ArrayBindingPattern', minTarget: 'es2015' }, + [ts.SyntaxKind.ComputedPropertyName]: { name: 'ComputedPropertyName', minTarget: 'es2015' }, + [ts.SyntaxKind.ForOfStatement]: { name: 'ForOfStatement', minTarget: 'es2015' }, + [ts.SyntaxKind.MetaProperty]: { name: 'MetaProperty', minTarget: 'es2015' }, + [ts.SyntaxKind.AwaitExpression]: { name: 'AwaitExpression', minTarget: 'es2017' }, + [ts.SyntaxKind.SpreadAssignment]: { name: 'SpreadAssignment', minTarget: 'es2018' }, + [ts.SyntaxKind.BigIntLiteral]: { name: 'BigIntLiteral', minTarget: 'es2020' }, + [ts.SyntaxKind.PropertyDeclaration]: { name: 'PropertyDeclaration', minTarget: 'es2022' }, + [ts.SyntaxKind.ClassStaticBlockDeclaration]: { name: 'ClassStaticBlockDeclaration', minTarget: 'es2022' }, + [ts.SyntaxKind.PrivateIdentifier]: { name: 'PrivateIdentifier', minTarget: 'es2022' }, +}; + +const featureByOperator: Partial> = { + [ts.SyntaxKind.AsteriskAsteriskToken]: { name: 'ExponentiationOperator', minTarget: 'es2016' }, + [ts.SyntaxKind.AsteriskAsteriskEqualsToken]: { name: 'ExponentiationAssignment', minTarget: 'es2016' }, + [ts.SyntaxKind.QuestionQuestionToken]: { name: 'NullishCoalescing', minTarget: 'es2020' }, + [ts.SyntaxKind.BarBarEqualsToken]: { name: 'LogicalOrAssignment', minTarget: 'es2021' }, + [ts.SyntaxKind.AmpersandAmpersandEqualsToken]: { name: 'LogicalAndAssignment', minTarget: 'es2021' }, + [ts.SyntaxKind.QuestionQuestionEqualsToken]: { name: 'NullishCoalescingAssignment', minTarget: 'es2021' }, +}; + +function isFunctionLikeDeclaration(node: ts.Node): node is ts.FunctionLikeDeclaration { + return ( + ts.isFunctionDeclaration(node) || + ts.isFunctionExpression(node) || + ts.isArrowFunction(node) || + ts.isMethodDeclaration(node) || + ts.isConstructorDeclaration(node) || + ts.isGetAccessorDeclaration(node) || + ts.isSetAccessorDeclaration(node) + ); +} + +function isAbove(feature: EsTarget, target: EsTarget) { + return targetOrder.indexOf(feature) > targetOrder.indexOf(target); +} + +/** + * Finds every syntax construct within `code` which cannot be emitted for `target`. + */ +export function findSyntaxAboveTarget(code: string, target: EsTarget, fileName = 'output.js'): EsFeature[] { + const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.JS); + const found = new Map(); + + const add = (name: string, minTarget: EsTarget) => { + if (isAbove(minTarget, target)) { + found.set(name, { name, minTarget }); + } + }; + const addFeature = (feature: EsFeature | undefined) => { + if (feature) { + add(feature.name, feature.minTarget); + } + }; + + const visit = (node: ts.Node) => { + addFeature(featureByKind[node.kind]); + + if (ts.isBinaryExpression(node)) { + addFeature(featureByOperator[node.operatorToken.kind]); + } + + if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isCallExpression(node)) { + if (node.questionDotToken) { + add('OptionalChaining', 'es2020'); + } + } + + if (ts.isVariableDeclarationList(node)) { + const declarationKeyword = node.getFirstToken()?.kind; + + if (declarationKeyword === ts.SyntaxKind.LetKeyword) { + add('LetDeclaration', 'es2015'); + } + if (declarationKeyword === ts.SyntaxKind.ConstKeyword) { + add('ConstDeclaration', 'es2015'); + } + } + + if (isFunctionLikeDeclaration(node)) { + if ('asteriskToken' in node && node.asteriskToken) { + add('Generator', 'es2015'); + } + if (ts.getModifiers(node)?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) { + add('AsyncFunction', 'es2017'); + } + for (const parameter of node.parameters) { + if (parameter.initializer) { + add('DefaultParameter', 'es2015'); + } + if (parameter.dotDotDotToken) { + add('RestParameter', 'es2015'); + } + } + } + + if (ts.isMethodDeclaration(node) && ts.isObjectLiteralExpression(node.parent)) { + add('ObjectLiteralMethod', 'es2015'); + } + + if (ts.isCatchClause(node) && !node.variableDeclaration) { + add('OptionalCatchBinding', 'es2019'); + } + + ts.forEachChild(node, visit); + }; + + ts.forEachChild(sourceFile, visit); + + return [...found.values()]; +} + +export type ModuleShape = 'amd' | 'esm' | 'commonjs' | 'script'; + +/** + * Determines the module format of emitted JavaScript. + */ +export function detectModuleShape(code: string, fileName = 'output.js'): ModuleShape { + const sourceFile = ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true, ts.ScriptKind.JS); + + const hasEsmSyntax = sourceFile.statements.some( + statement => + ts.isImportDeclaration(statement) || + ts.isExportDeclaration(statement) || + ts.isExportAssignment(statement) || + Boolean( + ts.canHaveModifiers(statement) && + ts + .getModifiers(statement) + ?.some( + modifier => + modifier.kind === ts.SyntaxKind.ExportKeyword || modifier.kind === ts.SyntaxKind.DefaultKeyword, + ), + ), + ); + + if (hasEsmSyntax) { + return 'esm'; + } + + const firstStatement = sourceFile.statements.find( + statement => !(ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)), + ); + + if ( + firstStatement && + ts.isExpressionStatement(firstStatement) && + ts.isCallExpression(firstStatement.expression) && + ts.isIdentifier(firstStatement.expression.expression) && + firstStatement.expression.expression.text === 'define' + ) { + return 'amd'; + } + + const hasCommonJsSyntax = /\brequire\s*\(|\bmodule\.exports\b|\bexports\.[A-Za-z_$]/.test(code); + + return hasCommonJsSyntax ? 'commonjs' : 'script'; +} diff --git a/scripts/tasks/src/generate-api.ts b/scripts/tasks/src/generate-api.ts index 1f64a35638e2a..920166130c42d 100644 --- a/scripts/tasks/src/generate-api.ts +++ b/scripts/tasks/src/generate-api.ts @@ -3,7 +3,7 @@ import { execSync } from 'child_process'; import { series } from 'just-scripts'; import { apiExtractor } from './api-extractor'; -import { getTsPathAliasesConfigUsedOnlyForDx } from './utils'; +import { createTsConfigWithoutPathAliases, getTsPathAliasesConfigUsedOnlyForDx } from './utils'; export function generateApi() { return series(generateTypeDeclarations, apiExtractor); @@ -11,15 +11,13 @@ export function generateApi() { function generateTypeDeclarations() { const { tsConfigFileForCompilation } = getTsPathAliasesConfigUsedOnlyForDx(); - const cmd = [ - 'tsc', - `-p ./${tsConfigFileForCompilation}`, - '--emitDeclarationOnly', - // turn off path aliases. - '--baseUrl .', - ] - .filter(Boolean) - .join(' '); + // turn off path aliases. + const noPathAliasesConfig = createTsConfigWithoutPathAliases(tsConfigFileForCompilation, 'generate-api'); + const cmd = ['tsc', `-p ./${noPathAliasesConfig.path}`, '--emitDeclarationOnly'].filter(Boolean).join(' '); - return execSync(cmd, { stdio: 'inherit' }); + try { + return execSync(cmd, { stdio: 'inherit' }); + } finally { + noPathAliasesConfig.cleanup(); + } } diff --git a/scripts/tasks/src/index.ts b/scripts/tasks/src/index.ts index 4366f7ee5fd33..feccbb864d806 100644 --- a/scripts/tasks/src/index.ts +++ b/scripts/tasks/src/index.ts @@ -87,6 +87,7 @@ export type { export { preset } from './presets'; export { expandSourcePath } from './copy'; export { typeCheckWithConfigOverride } from './type-check'; +export { typeCheckProject } from './type-check-project'; export { postprocessTask } from './postprocess'; export { getPerfRegressions, diff --git a/scripts/tasks/src/metadata-utils.spec.ts b/scripts/tasks/src/metadata-utils.spec.ts index 0bf605dd0bb1c..adf99ad60be97 100644 --- a/scripts/tasks/src/metadata-utils.spec.ts +++ b/scripts/tasks/src/metadata-utils.spec.ts @@ -24,9 +24,26 @@ describe(`metadata-utils`, () => { expect(actual.hasJest()).toEqual(false); expect(actual.hasSass()).toEqual(true); }); + + describe(`#shipsES5`, () => { + it(`should be opt-in via the "ships-es5" project tag`, () => { + const { root } = setup('react-one', { tags: ['v8', 'ships-es5'] }); + + expect(getRawMetadata(root).shipsES5()).toEqual(true); + }); + + it(`should not be inferred from any other metadata`, () => { + // `shipsAMD()` is true for this one, its `lib`/`lib-commonjs` are still on a modern baseline + const { root } = setup('react-one', { tags: ['v8', 'ships-amd'], projectType: 'library' }); + const actual = getRawMetadata(root); + + expect(actual.shipsAMD()).toEqual(true); + expect(actual.shipsES5()).toEqual(false); + }); + }); }); -function setup(projectRootDirName: string) { +function setup(projectRootDirName: string, projectJson: Record = {}) { function tmpFolder() { return `${workspaceRoot}/tmp`; } @@ -46,7 +63,7 @@ function setup(projectRootDirName: string) { `${root}/package.json`, JSON.stringify({ name: `@proj/${projectRootDirName}`, version: '0.0.0', private: true }), ); - fs.writeFileSync(`${root}/project.json`, JSON.stringify({ name: `@proj/${projectRootDirName}` })); + fs.writeFileSync(`${root}/project.json`, JSON.stringify({ name: `@proj/${projectRootDirName}`, ...projectJson })); fs.mkdirSync(`${root}/src/foo`, { recursive: true }); fs.writeFileSync(`${root}/src/foo/hello.scss`, 'body { color: red; }', 'utf-8'); diff --git a/scripts/tasks/src/metadata-utils.ts b/scripts/tasks/src/metadata-utils.ts index c66be5b3641bb..351b133c52121 100644 --- a/scripts/tasks/src/metadata-utils.ts +++ b/scripts/tasks/src/metadata-utils.ts @@ -42,6 +42,22 @@ export function getRawMetadata(projectRoot: string) { return true; } + /** + * Whether the published `lib`/`lib-commonjs` JavaScript of this project is on the ES5 baseline. + * + * TypeScript 6 removed `target: 'es5'`, so the ES5 emit moved from `tsc` to a SWC downlevel step + * (`ts:downlevel`). Which projects are on that baseline cannot be derived from other metadata + * (`shipsAMD()`, tags, versions, ...) - eg `@fluentui/react-icons-mdl2` ships AMD but its + * `lib`/`lib-commonjs` were emitted as ES2019 - so it is opt-in project metadata: every project + * whose tsconfig declared `target: es5` before the TypeScript 6 migration carries the + * `ships-es5` tag. + * + * @see https://github.com/microsoft/fluentui/issues/36409 + */ + function shipsES5() { + return new Set(project.tags ?? []).has('ships-es5'); + } + function hasJest() { return fs.existsSync(path.join(projectRoot, 'jest.config.js')); } @@ -55,5 +71,5 @@ export function getRawMetadata(projectRoot: string) { return glob.sync(path.join(projectRoot, 'src/**/*.scss')).length > 0; } - return { ...metadata, isConverged, shipsAMD, hasJest, hasBabel, hasSass, hasWebpack }; + return { ...metadata, isConverged, shipsAMD, shipsES5, hasJest, hasBabel, hasSass, hasWebpack }; } diff --git a/scripts/tasks/src/presets.spec.ts b/scripts/tasks/src/presets.spec.ts new file mode 100644 index 0000000000000..418f3a356244a --- /dev/null +++ b/scripts/tasks/src/presets.spec.ts @@ -0,0 +1,232 @@ +import { getJustArgv } from './argv'; +import { getRawMetadata } from './metadata-utils'; +import { preset } from './presets'; + +type TaskDefinition = + | string + | { type: 'series' | 'parallel'; tasks: TaskDefinition[] } + | ConditionDefinition + | Function; +interface ConditionDefinition { + type: 'condition'; + task: TaskDefinition; + isEnabled: () => boolean; +} + +jest.mock('just-scripts', () => { + const noop = () => undefined; + const taskRegistry = new Map(); + + return { + __taskRegistry: taskRegistry, + task: jest.fn((name: string, definition: TaskDefinition) => { + taskRegistry.set(name, definition); + return { cached: jest.fn() }; + }), + series: jest.fn((...tasks: TaskDefinition[]) => ({ type: 'series', tasks })), + parallel: jest.fn((...tasks: TaskDefinition[]) => ({ type: 'parallel', tasks })), + condition: jest.fn((task: TaskDefinition, isEnabled: () => boolean) => ({ type: 'condition', task, isEnabled })), + option: jest.fn(), + addResolvePath: jest.fn(), + logger: { info: noop, warn: noop, error: noop, verbose: noop }, + argv: jest.fn(() => ({})), + cleanTask: jest.fn(() => noop), + copyTask: jest.fn(() => noop), + copyInstructionsTask: jest.fn(() => noop), + eslintTask: jest.fn(() => noop), + sassTask: jest.fn(() => noop), + tscTask: jest.fn(() => noop), + webpackCliTask: jest.fn(() => noop), + webpackDevServerTask: jest.fn(() => noop), + apiExtractorVerifyTask: jest.fn(() => noop), + resolveCwd: jest.fn((value: string) => value), + }; +}); +// storybook is ESM only, it cannot be required within our CJS jest setup +jest.mock('./storybook', () => ({ + startStorybookTask: () => () => undefined, + buildStorybookTask: () => () => undefined, +})); +// `./jest` declares a `jest` binding, which collides with the global one injected by jest itself +jest.mock('./jest', () => ({ jest: () => undefined, jestWatch: () => undefined })); +jest.mock('./argv', () => ({ getJustArgv: jest.fn(() => ({})) })); +jest.mock('./metadata-utils', () => ({ getRawMetadata: jest.fn() })); + +const registry: Map = jest.requireMock('just-scripts').__taskRegistry; +const getJustArgvMockFn = getJustArgv as jest.MockedFunction; +const getRawMetadataMockFn = getRawMetadata as jest.MockedFunction; + +describe(`preset`, () => { + interface SetupOptions { + production?: boolean; + module?: { esm: boolean; cjs: boolean; amd: boolean }; + isConverged?: boolean; + shipsAMD?: boolean; + shipsES5?: boolean; + hasBabel?: boolean; + } + + function setup(options: SetupOptions = {}) { + const { + production = false, + module: moduleFlag, + isConverged = false, + shipsAMD = true, + shipsES5 = true, + hasBabel = false, + } = options; + + registry.clear(); + getJustArgvMockFn.mockReturnValue({ production, ...(moduleFlag ? { module: moduleFlag } : null) }); + getRawMetadataMockFn.mockReturnValue({ + isConverged: () => isConverged, + shipsAMD: () => shipsAMD, + shipsES5: () => shipsES5, + hasBabel: () => hasBabel, + hasSass: () => false, + hasWebpack: () => false, + hasJest: () => false, + project: { root: '', projectType: 'library' }, + packageJson: { name: '@proj/one', version: '8.0.0' }, + } as unknown as ReturnType); + + preset(); + } + + /** + * Resolves the (possibly lazy) task definition registered under `taskName` into a serializable tree, + * where every `condition()` is resolved to `taskName (enabled|disabled)`. + * + * NOTE: only the top level definition is invoked (that's how just-scripts defines lazy task graphs). + * Nested functions are the actual task implementations, thus they are never executed. + */ + function resolveTask(taskName: string): unknown { + const definition = registry.get(taskName); + + if (!definition) { + throw new Error(`"${taskName}" task is not registered`); + } + + if (typeof definition === 'function') { + const resolved = (definition as () => TaskDefinition | undefined)(); + return resolved ? resolveDefinition(resolved) : ''; + } + + return resolveDefinition(definition); + } + + function resolveDefinition(definition: TaskDefinition): unknown { + if (typeof definition === 'string') { + return definition; + } + + if (typeof definition === 'function') { + return ''; + } + + if (definition.type === 'condition') { + const taskName = typeof definition.task === 'string' ? definition.task : ''; + return `${taskName} (${definition.isEnabled() ? 'enabled' : 'disabled'})`; + } + + return { [definition.type]: definition.tasks.map(resolveDefinition) }; + } + + it(`should compile only module formats supported by the compiler`, () => { + setup({ production: true }); + + // `module: amd` was removed in TypeScript 6, thus `tsc` emits ESM + CJS only + expect(resolveTask('ts:compile')).toEqual({ parallel: ['ts:commonjs', 'ts:esm'] }); + }); + + it(`should transpile compiler output after it has been emitted and copied`, () => { + setup({ production: true }); + + expect(resolveTask('ts')).toEqual({ + series: ['ts:compile', 'copy-compiled', 'ts:transpile', 'ts:postprocess', 'babel:postprocess (disabled)'], + }); + }); + + it(`should downlevel and create amd output for v8 production builds`, () => { + setup({ production: true, shipsAMD: true, shipsES5: true, isConverged: false }); + + expect(resolveTask('ts:transpile')).toEqual({ + series: ['ts:downlevel (enabled)', 'ts:amd (enabled)'], + }); + }); + + it(`should not create amd output outside of production builds`, () => { + setup({ production: false, shipsAMD: true, shipsES5: true, isConverged: false }); + + expect(resolveTask('ts:transpile')).toEqual({ + series: ['ts:downlevel (enabled)', 'ts:amd (disabled)'], + }); + }); + + it(`should not downlevel packages which don't ship the legacy artifacts`, () => { + setup({ production: true, shipsAMD: false, shipsES5: false, isConverged: true }); + + expect(resolveTask('ts:transpile')).toEqual({ + series: ['ts:downlevel (disabled)', 'ts:amd (disabled)'], + }); + }); + + /** + * eg `@fluentui/react-icons-mdl2` ships AMD, but its `lib`/`lib-commonjs` were emitted as ES2019 + * before the TypeScript 6 migration - downleveling them to ES5 would silently change what is published + */ + it(`should create amd output without downleveling packages which are not on the ES5 baseline`, () => { + setup({ production: true, shipsAMD: true, shipsES5: false, isConverged: false }); + + expect(resolveTask('ts:transpile')).toEqual({ + series: ['ts:downlevel (disabled)', 'ts:amd (enabled)'], + }); + }); + + it(`should honour the --module flag`, () => { + setup({ production: false, shipsES5: true, module: { esm: true, cjs: false, amd: true } }); + + expect(resolveTask('ts:compile')).toEqual({ + parallel: ['ts:commonjs (disabled)', 'ts:esm (enabled)'], + }); + expect(resolveTask('ts:transpile')).toEqual({ + series: ['ts:downlevel (enabled)', 'ts:amd (enabled)'], + }); + }); + + it(`should postprocess amd output`, () => { + setup({ production: true }); + + expect(resolveTask('ts:amd')).toEqual({ series: ['', 'postprocess:amd'] }); + }); + + it(`should downlevel node only packages, which don't run the "ts" task`, () => { + setup({ shipsES5: true }); + + expect(resolveTask('build:node-lib')).toEqual({ + series: ['clean', 'copy', 'ts:commonjs', 'ts:downlevel (enabled)'], + }); + + setup({ shipsES5: false }); + + expect(resolveTask('build:node-lib')).toEqual({ + series: ['clean', 'copy', 'ts:commonjs', 'ts:downlevel (disabled)'], + }); + }); + + it(`should build v8 packages`, () => { + setup({ production: true }); + + expect(resolveTask('build:react')).toEqual({ + series: [ + 'clean', + 'copy', + 'sass', + 'ts', + 'api-extractor', + 'lint-imports:all (enabled)', + 'lint-imports:amd (disabled)', + ], + }); + }); +}); diff --git a/scripts/tasks/src/presets.ts b/scripts/tasks/src/presets.ts index c1cc44f5a5b7b..e9dfa2b20d6c0 100644 --- a/scripts/tasks/src/presets.ts +++ b/scripts/tasks/src/presets.ts @@ -63,6 +63,7 @@ export function preset() { task('postprocess:amd', postprocessAmdTask); task('ts:commonjs', ts.commonjs); task('ts:esm', ts.esm); + task('ts:downlevel', ts.downlevel); task('ts:amd', series(ts.amd, 'postprocess:amd')); task('eslint', eslint); task('webpack', webpack); @@ -82,17 +83,28 @@ export function preset() { const moduleFlag = args.module; // default behaviour if (!moduleFlag) { - return parallel( - 'ts:commonjs', - 'ts:esm', - condition('ts:amd', () => !!args.production && !metadata.isConverged()), - ); + return parallel('ts:commonjs', 'ts:esm'); } return parallel( condition('ts:commonjs', () => moduleFlag.cjs), condition('ts:esm', () => moduleFlag.esm), - condition('ts:amd', () => moduleFlag.amd), + ); + }); + + /** + * Post compilation JS transforms. + * + * These run on `tsc` emitted output (thus after `copy-compiled`, which is what materializes `lib`/`lib-commonjs` + * for packages compiling to `dist/out-tsc`) because TypeScript 6 removed `target: 'es5'` and `module: 'amd'`, + * which used to produce the published v8 artifacts. + */ + task('ts:transpile', () => { + const moduleFlag = args.module; + + return series( + condition('ts:downlevel', () => metadata.shipsES5()), + condition('ts:amd', () => (moduleFlag ? moduleFlag.amd : Boolean(args.production) && !metadata.isConverged())), ); }); @@ -100,6 +112,7 @@ export function preset() { return series( 'ts:compile', 'copy-compiled', + 'ts:transpile', 'ts:postprocess', condition('babel:postprocess', () => metadata.hasBabel()), ); @@ -130,7 +143,19 @@ export function preset() { task('dev:storybook', series('storybook:start')); task('dev', series('copy', 'sass', 'webpack-dev-server')); - task('build:node-lib', series('clean', 'copy', 'ts:commonjs')).cached!(); + /** + * `tsc` only emits the modern baseline since TypeScript 6, so projects on the ES5 baseline + * (`ships-es5`) need the SWC downlevel here as well - `build:node-lib` doesn't run the `ts` task. + */ + task( + 'build:node-lib', + series( + 'clean', + 'copy', + 'ts:commonjs', + condition('ts:downlevel', () => metadata.shipsES5()), + ), + ).cached!(); // === React v8 build tasks / START === task( diff --git a/scripts/tasks/src/static-assets-css.spec.ts b/scripts/tasks/src/static-assets-css.spec.ts new file mode 100644 index 0000000000000..b9ddda516b246 --- /dev/null +++ b/scripts/tasks/src/static-assets-css.spec.ts @@ -0,0 +1,113 @@ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { workspaceRoot } from '@nx/devkit'; + +/** + * Behavioural regression test for `typings/static-assets/index.d.ts`'s `declare module '*.css' {}` ambient + * declaration. + * + * A shorthand ambient module (`declare module '*.css';`, no `{ }` body) makes every export of the matched + * module `any`, so a value/default/named import from a plain stylesheet would silently type-check even + * though bundlers only ever inject plain CSS for its side effect. The declaration must keep an explicit + * empty body so a side-effect only import still resolves, while a named import fails to resolve and a + * default import's type carries no properties (so property access on it is still rejected). + */ +describe(`typings/static-assets '*.css' ambient module`, () => { + let root: string; + + function createTsConfig() { + fs.writeFileSync( + path.join(root, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + target: 'ES2019', + module: 'esnext', + moduleResolution: 'bundler', + strict: true, + noEmit: true, + skipLibCheck: true, + // mirrors `tsconfig.base.json`'s `typeRoots`, pointed at the real workspace `typings` folder so + // this test exercises the actual declaration file instead of a copy of it + typeRoots: [path.join(workspaceRoot, 'typings')], + types: ['static-assets'], + }, + include: ['*.ts'], + }), + 'utf-8', + ); + } + + function runTsc() { + return spawnSync(process.execPath, [require.resolve('typescript/lib/tsc.js'), '-p', '.', '--noEmit'], { + cwd: root, + encoding: 'utf-8', + }); + } + + beforeEach(() => { + // written under the OS temp directory (never inside a source tree) so an interrupted run cannot leave + // fixture files behind for git/tsconfig to pick up + root = fs.mkdtempSync(path.join(os.tmpdir(), 'static-assets-css-')); + fs.writeFileSync(path.join(root, 'style.css'), '', 'utf-8'); + createTsConfig(); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it(`accepts a side-effect only import`, () => { + fs.writeFileSync(path.join(root, 'index.ts'), [`import './style.css';`, `export {};`].join('\n'), 'utf-8'); + + const result = runTsc(); + + expect(result.stdout).toEqual(''); + expect(result.status).toEqual(0); + }); + + it(`does not let a default import silently become 'any'`, () => { + fs.writeFileSync( + path.join(root, 'index.ts'), + // `moduleResolution: bundler` allows a default import to bind to the (empty) module namespace object + // instead of erroring outright - the regression this guards is that binding being `any`, which would + // let property access through unchecked. The property access below must still be rejected. + [`import styles from './style.css';`, `export const value: string = styles.foo;`].join('\n'), + 'utf-8', + ); + + const result = runTsc(); + + expect(result.status).not.toEqual(0); + expect(result.stdout).toMatch(/Property 'foo' does not exist on type/); + }); + + it(`rejects a named import`, () => { + fs.writeFileSync( + path.join(root, 'index.ts'), + [`import { theme } from './style.css';`, `export { theme };`].join('\n'), + 'utf-8', + ); + + const result = runTsc(); + + expect(result.status).not.toEqual(0); + expect(result.stdout).toMatch(/has no exported member/); + }); + + it(`still accepts a default import from a CSS module (regression check)`, () => { + fs.writeFileSync(path.join(root, 'style.module.css'), '', 'utf-8'); + fs.writeFileSync( + path.join(root, 'index.ts'), + [`import styles from './style.module.css';`, `export const className: string = styles.foo;`].join('\n'), + 'utf-8', + ); + + const result = runTsc(); + + expect(result.stdout).toEqual(''); + expect(result.status).toEqual(0); + }); +}); diff --git a/scripts/tasks/src/storybook.ts b/scripts/tasks/src/storybook.ts index 914f38ad8cbce..4f0a4655316cc 100644 --- a/scripts/tasks/src/storybook.ts +++ b/scripts/tasks/src/storybook.ts @@ -3,7 +3,6 @@ import * as fs from 'fs'; import * as path from 'path'; import { findGitRoot } from '@fluentui/scripts-monorepo'; -// @ts-expect-error - storybook/internal/core-server is an ECMAScript module import { build as storybook } from 'storybook/internal/core-server'; // Option types are documented here but not included in package for some reason diff --git a/scripts/tasks/src/swc/index.ts b/scripts/tasks/src/swc/index.ts index 2ac1f24de3000..caca55cdc7787 100644 --- a/scripts/tasks/src/swc/index.ts +++ b/scripts/tasks/src/swc/index.ts @@ -1 +1,2 @@ export { swc } from './swc'; +export { transpileEmittedJs, type TranspileEmittedOptions } from './transpile'; diff --git a/scripts/tasks/src/swc/interop.spec.ts b/scripts/tasks/src/swc/interop.spec.ts new file mode 100644 index 0000000000000..3d9f64a45a6d0 --- /dev/null +++ b/scripts/tasks/src/swc/interop.spec.ts @@ -0,0 +1,137 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import * as vm from 'node:vm'; + +import { transpileEmittedJs } from './transpile'; + +/** + * Smoke tests for the module interoperability shape of the published v8 artifacts. + * + * v8 packages never set `esModuleInterop`, so their CommonJS emit is the plain `require` form and + * is unchanged by the migration. The AMD output, however, is produced by the SWC module transform + * (TypeScript 6 removed `module: amd`), which wraps namespace/default imports with + * `_interop_require_wildcard`. That helper is **inlined** into the emitted file (SWC + * `externalHelpers: false`), so the artifact carries no `@swc/helpers` runtime dependency. + * + * @see ../../../../docs/architecture/v8-published-artifacts.md + */ +describe(`esModuleInterop`, () => { + const tmpRoot = path.join(__dirname, '../../tmp'); + let root: string; + + /** + * mimics `tsc` ESM emit of + * ```ts + * import * as dep from 'dep'; + * import defaultDep from 'dep'; + * export function getNamespace() { return dep; } + * export function getDefault() { return defaultDep; } + * ``` + */ + const esmOutput = [ + `import * as dep from 'dep';`, + `import defaultDep from 'dep';`, + `export function getNamespace() { return dep; }`, + `export function getDefault() { return defaultDep; }`, + ].join('\n'); + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + root = fs.mkdtempSync(path.join(tmpRoot, 'interop-')); + fs.mkdirSync(path.join(root, 'lib'), { recursive: true }); + fs.writeFileSync(path.join(root, 'lib/index.js'), esmOutput, 'utf-8'); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + /** + * Minimal AMD loader - `define(deps, factory)` with `require`/`exports` injection, which is all + * the emitted modules use. + */ + function loadAmd(code: string, modules: Record) { + const exports: Record = {}; + const requireFn = (id: string) => { + if (!(id in modules)) { + throw new Error(`AMD module "${id}" is not provided`); + } + return modules[id]; + }; + const define = (dependencies: string[], factory: (...args: unknown[]) => void) => { + factory( + ...dependencies.map(dependency => { + if (dependency === 'require') { + return requireFn; + } + if (dependency === 'exports') { + return exports; + } + return requireFn(dependency); + }), + ); + }; + + vm.runInNewContext(code, { define, console }); + + return exports; + } + + it(`should wrap namespace and default imports of the AMD output`, async () => { + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + const code = fs.readFileSync(path.join(root, 'lib-amd/index.js'), 'utf-8'); + + expect(code).toMatch(/^define\(\[/); + // the interop helper is inlined, so the AMD dependency list declares no `@swc/helpers` module + const amdDependencies = code.slice(0, code.indexOf(']')); + expect(amdDependencies).not.toContain('@swc/helpers'); + expect(code).toContain('function _interop_require_wildcard'); + + // a CommonJS style dependency, ie one without `__esModule` + const dep = { a: 'named export' }; + const dependencies: Record = { dep }; + + const module = loadAmd(code, dependencies) as { + getNamespace: () => Record; + getDefault: () => unknown; + }; + + const namespace = module.getNamespace(); + + // interop namespace: own properties are re-exposed, the module itself becomes `default` + expect(namespace.a).toEqual('named export'); + expect(namespace.default).toBe(dep); + expect(module.getDefault()).toBe(dep); + // ...it is a copy, not the module object itself + expect(namespace).not.toBe(dep); + // ...and it is stable - every access returns the very same namespace object + expect(module.getNamespace()).toBe(namespace); + }); + + it(`should pass ESM dependencies through untouched`, async () => { + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + const code = fs.readFileSync(path.join(root, 'lib-amd/index.js'), 'utf-8'); + const dep = { __esModule: true, a: 'named export', default: 'the default' }; + const dependencies: Record = { dep }; + + const module = loadAmd(code, dependencies) as { + getNamespace: () => Record; + getDefault: () => unknown; + }; + + // real ES modules are not wrapped, so module identity is preserved + expect(module.getNamespace()).toBe(dep); + expect(module.getDefault()).toEqual('the default'); + }); + + it(`should not introduce interop into the ESM output`, async () => { + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib', module: 'es6', target: 'es5' }); + + const code = fs.readFileSync(path.join(root, 'lib/index.js'), 'utf-8'); + + expect(code).toContain(`import * as dep from 'dep'`); + expect(code).not.toContain('_interop_require'); + }); +}); diff --git a/scripts/tasks/src/swc/transpile.spec.ts b/scripts/tasks/src/swc/transpile.spec.ts new file mode 100644 index 0000000000000..1799157b613b9 --- /dev/null +++ b/scripts/tasks/src/swc/transpile.spec.ts @@ -0,0 +1,326 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { findSyntaxAboveTarget } from '../ecma-syntax'; + +import { transpileEmittedJs } from './transpile'; + +function findModernSyntax(code: string) { + return findSyntaxAboveTarget(code, 'es5').map(feature => feature.name); +} + +describe(`transpileEmittedJs`, () => { + const tmpRoot = path.join(__dirname, '../../tmp'); + let root: string; + + /** + * mimics `tsc` ESM emit with `target: ES2015` + `sourceMap` + `inlineSources` + */ + const esmOutput = [ + `import { Base } from './base';`, + `export class Greeter extends Base {`, + ` greet = (name) => \`hello \${name}\`;`, + `}`, + `//# sourceMappingURL=index.js.map`, + ].join('\n'); + + const commonjsOutput = [ + `"use strict";`, + `Object.defineProperty(exports, "__esModule", { value: true });`, + `exports.greet = void 0;`, + `const base_1 = require("./base");`, + `const greet = (name) => \`hello \${name}\`;`, + `exports.greet = greet;`, + `//# sourceMappingURL=index.js.map`, + ].join('\n'); + + const sourceMap = JSON.stringify({ + version: 3, + file: 'index.js', + sourceRoot: '../src/', + sources: ['index.ts'], + names: [], + mappings: 'AAAA', + sourcesContent: [`export const greet = (name: string) => \`hello \${name}\`;`], + }); + + function createFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(path.join(root, filePath)), { recursive: true }); + fs.writeFileSync(path.join(root, filePath), content, 'utf-8'); + } + + function readFile(filePath: string) { + return fs.readFileSync(path.join(root, filePath), 'utf-8'); + } + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + root = fs.mkdtempSync(path.join(tmpRoot, 'transpile-')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it(`should downlevel ESM output in place and keep the module format`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.js.map', sourceMap); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es5', + }); + + const actual = readFile('lib/index.js'); + + expect(result.files).toEqual(['index.js']); + expect(result.transpiled).toEqual(['index.js']); + // module format is untouched + expect(actual).toContain(`import { Base } from './base'`); + expect(actual).toContain('export'); + // ES2015+ syntax is gone + expect(findModernSyntax(actual)).toEqual([]); + }); + + it(`should downlevel CommonJS output in place without re-wrapping it`, async () => { + createFile('lib-commonjs/index.js', commonjsOutput); + createFile('lib-commonjs/index.js.map', sourceMap); + + await transpileEmittedJs({ + root, + inputPath: 'lib-commonjs', + outputPath: 'lib-commonjs', + module: 'commonjs', + target: 'es5', + }); + + const actual = readFile('lib-commonjs/index.js'); + + expect(actual).toContain(`require("./base")`); + expect(actual).toContain('exports.greet'); + expect(findModernSyntax(actual)).toEqual([]); + // it's a script, not an ESM module wrapped into cjs + expect(actual).not.toContain('_interop_require_default'); + }); + + it(`should create AMD output from ESM output`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.js.map', sourceMap); + createFile('lib/nested/other.js', esmOutput); + createFile('lib/nested/other.js.map', sourceMap); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib-amd', + module: 'amd', + target: 'es5', + }); + + const actual = readFile('lib-amd/index.js'); + + expect(result.files.sort()).toEqual(['index.js', path.join('nested', 'other.js')]); + expect(actual).toMatch(/^define\(\[/); + expect(findModernSyntax(actual)).toEqual([]); + // source output is left untouched + expect(readFile('lib/index.js')).toEqual(esmOutput); + expect(fs.existsSync(path.join(root, 'lib-amd/nested/other.js'))).toBe(true); + }); + + it(`should chain source maps of the compiler emitted output`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.js.map', sourceMap); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + const actual = readFile('lib-amd/index.js'); + const actualSourceMap = JSON.parse(readFile('lib-amd/index.js.map')); + + // the `//# sourceMappingURL` comment of the input must not end up within the AMD module factory + expect(actual.match(/\/\/# sourceMappingURL/g)).toHaveLength(1); + expect(actual.endsWith('//# sourceMappingURL=index.js.map')).toBe(true); + + expect(actualSourceMap.sources).toEqual(['index.ts']); + expect(actualSourceMap.sourceRoot).toEqual('../src/'); + expect(actualSourceMap.sourcesContent[0]).toContain('const greet'); + }); + + it(`should not emit source maps if the compiler didn't`, async () => { + createFile('lib/index.js', `export var noop = function () {};`); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + expect(readFile('lib-amd/index.js')).not.toContain('sourceMappingURL'); + expect(fs.existsSync(path.join(root, 'lib-amd/index.js.map'))).toBe(false); + }); + + it(`should throw an actionable error if there is no compiler output to transpile`, async () => { + await expect( + transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }), + ).rejects.toThrow(/cannot transpile "lib" -> "lib-amd".*tsc compilation needs to run first/s); + }); + + describe(`helpers`, () => { + /** + * v8 packages are maintenance-only, so the pipeline inlines the downlevel/module helpers + * rather than adding `@swc/helpers` as a new runtime dependency to every published package. + */ + it(`should inline helpers instead of importing them from @swc/helpers`, async () => { + const files = ['a.js', 'b.js', 'c.js']; + + for (const fileName of files) { + createFile(`lib/${fileName}`, esmOutput); + } + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib', module: 'es6', target: 'es5' }); + + for (const fileName of files) { + const actual = readFile(`lib/${fileName}`); + + // helper is inlined as a local function, not imported from `@swc/helpers` + expect(actual).not.toMatch(/from ["']@swc\/helpers/); + expect(actual).toMatch(/function _class_call_check\(/); + } + }); + + it(`should inline helpers in AMD output as well`, async () => { + createFile('lib/index.js', esmOutput); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + const actual = readFile('lib-amd/index.js'); + + // the AMD dependency list (everything up to the first `]`) declares no `@swc/helpers` module + const amdDependencies = actual.slice(0, actual.indexOf(']')); + expect(amdDependencies).not.toContain('@swc/helpers'); + expect(actual).toMatch(/function _class_call_check\(/); + }); + }); + + describe(`idempotency`, () => { + it(`should not transpile already transpiled in place output again`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.js.map', sourceMap); + + const first = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es5', + }); + const firstOutput = readFile('lib/index.js'); + + const second = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es5', + }); + + expect(first.transpiled).toEqual(['index.js']); + expect(second.transpiled).toEqual([]); + // the output (and its source map) is byte identical, it was not re-transformed nor re-chained + expect(readFile('lib/index.js')).toEqual(firstOutput); + }); + + it(`should transpile again when the compiler emitted new output`, async () => { + createFile('lib/index.js', esmOutput); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib', module: 'es6', target: 'es5' }); + + createFile('lib/index.js', `export const other = () => 'changed';`); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es5', + }); + + expect(result.transpiled).toEqual(['index.js']); + expect(readFile('lib/index.js')).toContain('changed'); + expect(findModernSyntax(readFile('lib/index.js'))).toEqual([]); + }); + + it(`should transpile again when the target changed`, async () => { + createFile('lib/index.js', esmOutput); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib', module: 'es6', target: 'es5' }); + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es2015', + }); + + expect(result.transpiled).toEqual(['index.js']); + }); + + it(`should not transpile unchanged derived output again, but restore deleted files`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/nested/other.js', esmOutput); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + fs.rmSync(path.join(root, 'lib-amd/nested/other.js')); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib-amd', + module: 'amd', + target: 'es5', + }); + + expect(result.transpiled).toEqual([path.join('nested', 'other.js')]); + expect(readFile('lib-amd/nested/other.js')).toMatch(/^define\(\[/); + }); + }); + + describe(`stale outputs`, () => { + it(`should prune derived output which has no compiler emitted counterpart anymore`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/removed.js', esmOutput); + + await transpileEmittedJs({ root, inputPath: 'lib', outputPath: 'lib-amd', module: 'amd', target: 'es5' }); + + expect(fs.existsSync(path.join(root, 'lib-amd/removed.js'))).toBe(true); + + fs.rmSync(path.join(root, 'lib/removed.js')); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib-amd', + module: 'amd', + target: 'es5', + }); + + expect(result.pruned).toEqual(['removed.js']); + expect(fs.existsSync(path.join(root, 'lib-amd/removed.js'))).toBe(false); + expect(fs.existsSync(path.join(root, 'lib-amd/index.js'))).toBe(true); + }); + + it(`should never prune in place output`, async () => { + createFile('lib/index.js', esmOutput); + + const result = await transpileEmittedJs({ + root, + inputPath: 'lib', + outputPath: 'lib', + module: 'es6', + target: 'es5', + }); + + expect(result.pruned).toEqual([]); + expect(fs.existsSync(path.join(root, 'lib/index.js'))).toBe(true); + }); + }); +}); diff --git a/scripts/tasks/src/swc/transpile.ts b/scripts/tasks/src/swc/transpile.ts new file mode 100644 index 0000000000000..27804641c5956 --- /dev/null +++ b/scripts/tasks/src/swc/transpile.ts @@ -0,0 +1,313 @@ +import { createHash } from 'node:crypto'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; + +import { type JscTarget, transform } from '@swc/core'; +import glob from 'glob'; + +import { postprocessOutput } from './utils'; + +export interface TranspileEmittedOptions { + /** + * Directory with already emitted `.js` files (relative to `root`), eg `lib`. + */ + inputPath: string; + /** + * Directory to write transpiled files to (relative to `root`). + * + * Setting it to the same value as `inputPath` transpiles the emitted output in place. + */ + outputPath: string; + /** + * Module format of the emitted output. + * + * Input is always ESM (`es6`) or CommonJS - `es6`/`commonjs` keep the module format as is, + * `amd` converts ESM output to AMD. + */ + module: 'es6' | 'commonjs' | 'amd'; + /** + * ECMAScript version that the output will be downleveled to. + */ + target: JscTarget; + /** + * Remove files within `outputPath` which have no counterpart in `inputPath` anymore. + * + * @defaultValue `true` for derived outputs (`inputPath !== outputPath`), `false` otherwise + */ + prune?: boolean; + /** + * @defaultValue process.cwd() + */ + root?: string; +} + +export interface TranspileEmittedResult { + /** + * Every `.js` file found within `inputPath`. + */ + files: string[]; + /** + * Files which were (re)transpiled by this invocation. + */ + transpiled: string[]; + /** + * Stale files removed from `outputPath`. + */ + pruned: string[]; +} + +/** + * Transpiles (downlevels / re-modularizes) JavaScript that has already been emitted by `tsc`. + * + * TypeScript 6 removed `target: 'es5'` and `module: 'amd'`, so the ES5/AMD artifacts that v8 + * packages publish cannot be produced by the compiler anymore. This mirrors what converged + * (v9) packages that ship AMD already do - `tsc` handles types, SWC handles the JS emit. + * + * NOTES: + * - source maps emitted by `tsc` are chained (`inputSourceMap`), so the final `.map` keeps + * pointing at the original `.ts` sources + * - `.swcrc` lookup is disabled on purpose, the whole configuration lives here so that every + * package built by the shared preset produces identical output + * - the transform is idempotent: a content addressed manifest (see {@link readManifest}) records + * what was produced from what, so already transpiled output is never transpiled again, even + * when the task is invoked repeatedly or only for a subset of module formats + */ +export async function transpileEmittedJs(options: TranspileEmittedOptions): Promise { + const { inputPath, outputPath, module, target, root = process.cwd(), prune = inputPath !== outputPath } = options; + + const absoluteInputPath = path.resolve(root, inputPath); + const absoluteOutputPath = path.resolve(root, outputPath); + + if (!fs.existsSync(absoluteInputPath)) { + throw new Error( + `swc: cannot transpile "${inputPath}" -> "${outputPath}", because "${absoluteInputPath}" doesn't exist. ` + + `This step runs on compiler emitted output, thus the tsc compilation needs to run first ` + + `(eg use "--module esm,amd" instead of "--module amd").`, + ); + } + + const fileNames = glob.sync('**/*.js', { cwd: absoluteInputPath, nodir: true }); + + const manifestPath = getManifestPath({ root, outputPath }); + const previousManifest = readManifest({ manifestPath, module, target }); + const manifest: Manifest = { version: manifestVersion, module, target, files: {} }; + const transpiled: string[] = []; + + await eachLimit(fileNames, concurrencyLimit, async fileName => { + const entry = await transpileFile({ + fileName, + absoluteInputPath, + absoluteOutputPath, + module, + target, + previousEntry: previousManifest?.files[fileName], + onTranspiled: () => transpiled.push(fileName), + }); + + manifest.files[fileName] = entry; + }); + + const pruned = prune ? pruneStaleOutputs({ absoluteOutputPath, fileNames }) : []; + + writeManifest(manifestPath, manifest); + + return { files: fileNames, transpiled, pruned }; +} + +const concurrencyLimit = Math.max(1, os.cpus().length); +const sourceMappingUrlRegex = /\n?\/\/# sourceMappingURL=\S*/g; +/** + * Bump whenever the emitted output changes (swc options, postprocessing, ...) so that stale + * manifests never mark outdated output as up to date. + */ +const manifestVersion = 2; + +interface ManifestEntry { + /** hash of the compiler emitted input (`.js` + `.js.map`) the output was created from */ + source: string; + /** hash of the transpiled output (`.js` + `.js.map`) */ + output: string; +} +interface Manifest { + version: number; + module: TranspileEmittedOptions['module']; + target: JscTarget; + files: Record; +} + +/** + * The manifest is a build artifact - it lives in the project's `node_modules/.cache`, which is + * ignored by both git and npm, so it can never leak into a published package. + */ +function getManifestPath(options: { root: string; outputPath: string }) { + const fileName = `${options.outputPath.replace(/[\\/:]/g, '-')}.json`; + + return path.join(options.root, 'node_modules', '.cache', 'fluentui-swc-transpile', fileName); +} + +function readManifest(options: { manifestPath: string; module: string; target: string }): Manifest | undefined { + try { + const manifest: Manifest = JSON.parse(fs.readFileSync(options.manifestPath, 'utf-8')); + + if ( + manifest.version !== manifestVersion || + manifest.module !== options.module || + manifest.target !== options.target + ) { + return undefined; + } + + return manifest; + } catch { + return undefined; + } +} + +function writeManifest(manifestPath: string, manifest: Manifest) { + fs.mkdirSync(path.dirname(manifestPath), { recursive: true }); + fs.writeFileSync(manifestPath, JSON.stringify(manifest), 'utf-8'); +} + +function hashContent(code: string, sourceMap: string | undefined) { + return createHash('sha256') + .update(code) + .update('\u0000') + .update(sourceMap ?? '') + .digest('hex'); +} + +async function transpileFile(options: { + fileName: string; + absoluteInputPath: string; + absoluteOutputPath: string; + module: TranspileEmittedOptions['module']; + target: JscTarget; + previousEntry: ManifestEntry | undefined; + onTranspiled: () => void; +}): Promise { + const { fileName, absoluteInputPath, absoluteOutputPath, module, target, previousEntry, onTranspiled } = options; + + const inputFilePath = path.join(absoluteInputPath, fileName); + const outputFilePath = path.join(absoluteOutputPath, fileName); + const isInPlace = inputFilePath === outputFilePath; + + const sourceCode = await fs.promises.readFile(inputFilePath, 'utf-8'); + const inputSourceMap = await readIfExists(`${inputFilePath}.map`); + const sourceHash = hashContent(sourceCode, inputSourceMap); + + if (previousEntry) { + // in place output: the file we just read is the output of a previous invocation + if (isInPlace && previousEntry.output === sourceHash) { + return previousEntry; + } + + // derived output: input is unchanged and the output produced from it is still on disk + if (!isInPlace && previousEntry.source === sourceHash) { + const outputCode = await readIfExists(outputFilePath); + const outputSourceMap = await readIfExists(`${outputFilePath}.map`); + + if (outputCode !== undefined && hashContent(outputCode, outputSourceMap) === previousEntry.output) { + return previousEntry; + } + } + } + + const result = await transform(sourceCode, { + filename: inputFilePath, + // the configuration is fully defined here - `.swcrc` of the package (if any) configures the source -> `lib` compilation, which is a different transformation + swcrc: false, + configFile: false, + module: { type: module }, + jsc: { + parser: { syntax: 'ecmascript' }, + target, + /** + * Helpers are inlined into every emitted file rather than imported from `@swc/helpers`. + * + * External helpers would add `@swc/helpers` as a new runtime dependency to every v8 + * (`ships-es5`) package. v8 is maintenance-only, and adding a runtime dependency to ~40 + * published maintenance packages is a contract change we deliberately avoid; inlining keeps + * the dependency graph unchanged at the cost of a small size increase (~13% of `lib`, + * ~21% of `lib-amd`) that is immaterial for frozen legacy artifacts. + */ + externalHelpers: false, + }, + sourceMaps: Boolean(inputSourceMap), + inputSourceMap, + outputPath: absoluteOutputPath, + }); + + // swc keeps the `//# sourceMappingURL` comment of the input in place, which for wrapped output (amd) ends up within the module factory + const code = postprocessOutput(result.code).replace(sourceMappingUrlRegex, ''); + + await fs.promises.mkdir(path.dirname(outputFilePath), { recursive: true }); + + onTranspiled(); + + if (!result.map) { + await fs.promises.writeFile(outputFilePath, code); + // a previous invocation might have emitted a map for this file + await fs.promises.rm(`${outputFilePath}.map`, { force: true }); + + return { source: sourceHash, output: hashContent(code, undefined) }; + } + + const sourceMapFileName = `${path.basename(fileName)}.map`; + const outputCode = `${code}\n//# sourceMappingURL=${sourceMapFileName}`; + + await fs.promises.writeFile(outputFilePath, outputCode); + await fs.promises.writeFile(`${outputFilePath}.map`, result.map); + + return { source: sourceHash, output: hashContent(outputCode, result.map) }; +} + +/** + * Removes `.js`/`.js.map` files from a derived output directory which have no counterpart in the + * compiler emitted input anymore (eg a source file was renamed or deleted between builds). + */ +function pruneStaleOutputs(options: { absoluteOutputPath: string; fileNames: string[] }) { + const { absoluteOutputPath, fileNames } = options; + + if (!fs.existsSync(absoluteOutputPath)) { + return []; + } + + const expected = new Set(fileNames); + const pruned: string[] = []; + + for (const fileName of glob.sync('**/*.js', { cwd: absoluteOutputPath, nodir: true })) { + if (expected.has(fileName)) { + continue; + } + + const staleFilePath = path.join(absoluteOutputPath, fileName); + + fs.rmSync(staleFilePath, { force: true }); + fs.rmSync(`${staleFilePath}.map`, { force: true }); + pruned.push(fileName); + } + + return pruned; +} + +async function readIfExists(filePath: string) { + try { + return await fs.promises.readFile(filePath, 'utf-8'); + } catch { + return undefined; + } +} + +async function eachLimit(items: T[], limit: number, iteratee: (item: T) => Promise) { + const queue = [...items]; + const workers = Array.from({ length: Math.min(limit, queue.length) }, async () => { + let item = queue.shift(); + while (item !== undefined) { + await iteratee(item); + item = queue.shift(); + } + }); + + await Promise.all(workers); +} diff --git a/scripts/tasks/src/ts.spec.ts b/scripts/tasks/src/ts.spec.ts new file mode 100644 index 0000000000000..b0d5becccb8dd --- /dev/null +++ b/scripts/tasks/src/ts.spec.ts @@ -0,0 +1,213 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { getJustArgv } from './argv'; +import { findSyntaxAboveTarget } from './ecma-syntax'; +import { ts } from './ts'; + +jest.mock('just-scripts', () => ({ + logger: { info: jest.fn(), warn: jest.fn(), error: jest.fn(), verbose: jest.fn() }, + tscTask: jest.fn((_options: unknown) => () => Promise.resolve()), +})); +jest.mock('./argv', () => ({ getJustArgv: jest.fn(() => ({})) })); + +const tscTask: jest.Mock = jest.requireMock('just-scripts').tscTask; +const getJustArgvMock = getJustArgv as jest.MockedFunction; + +describe(`ts`, () => { + const tmpRoot = path.join(__dirname, '../tmp'); + let root: string; + + const esmOutput = [ + `import { Base } from './base';`, + `export const greet = (name) => \`hello \${name}\`;`, + `export class Greeter extends Base {}`, + ].join('\n'); + const commonjsOutput = [ + `"use strict";`, + `Object.defineProperty(exports, "__esModule", { value: true });`, + `const greet = (name) => \`hello \${name}\`;`, + `exports.greet = greet;`, + ].join('\n'); + + function createFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(path.join(root, filePath)), { recursive: true }); + fs.writeFileSync(path.join(root, filePath), content, 'utf-8'); + } + function readFile(filePath: string) { + return fs.readFileSync(path.join(root, filePath), 'utf-8'); + } + function listGeneratedTsConfigs() { + return fs.readdirSync(root).filter(fileName => fileName.startsWith('tsconfig.__generated')); + } + + const originalCwd = process.cwd(); + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + root = fs.mkdtempSync(path.join(tmpRoot, 'ts-task-')); + // the tasks resolve every path (including the tsconfig passed to `tsc`) relative to the cwd + process.chdir(root); + getJustArgvMock.mockReturnValue({}); + tscTask.mockClear(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + process.chdir(originalCwd); + fs.rmSync(root, { recursive: true, force: true }); + }); + + describe(`compilation`, () => { + it(`should not use compiler options removed in TypeScript 6`, () => { + createFile('tsconfig.json', JSON.stringify({ compilerOptions: {} })); + createFile('package.json', JSON.stringify({ name: '@proj/one' })); + + ts.esm(); + ts.commonjs(); + + const [esmOptions, commonjsOptions] = tscTask.mock.calls.map(([options]) => options); + + expect(esmOptions).toEqual(expect.objectContaining({ outDir: 'lib', module: 'esnext' })); + expect(commonjsOptions).toEqual(expect.objectContaining({ outDir: 'lib-commonjs', module: 'commonjs' })); + + for (const options of [esmOptions, commonjsOptions]) { + // `target: es5` and `module: amd` were removed in TS 6 - both are produced by the swc based `downlevel`/`amd` tasks + expect(options.target).toBeUndefined(); + expect(options.module).not.toEqual('amd'); + } + }); + + it(`should compile projects which use path aliases for DX without them`, async () => { + createFile('tsconfig.json', JSON.stringify({ extends: '../../tsconfig.base.v8.json', compilerOptions: {} })); + createFile('package.json', JSON.stringify({ name: '@proj/one' })); + + const task = ts.commonjs(); + const [options] = tscTask.mock.calls[0]; + + expect(options.rootDir).toEqual('./src'); + expect(options.project).toMatch(/tsconfig\.__generated-no-path-aliases-build-.*-tsconfig\.json$/); + expect(JSON.parse(readFile(path.basename(options.project)))).toEqual({ + extends: './tsconfig.json', + compilerOptions: { paths: null }, + }); + + // the transient config is removed as soon as the compilation finished - not on process exit + await (task as unknown as () => Promise)(); + + expect(listGeneratedTsConfigs()).toEqual([]); + }); + + it(`should remove the transient tsconfig even if the compilation failed`, async () => { + createFile('tsconfig.json', JSON.stringify({ extends: '../../tsconfig.base.v8.json', compilerOptions: {} })); + createFile('package.json', JSON.stringify({ name: '@proj/one' })); + + tscTask.mockImplementationOnce(() => () => Promise.reject(new Error('tsc failed'))); + + const task = ts.commonjs(); + + await expect((task as unknown as () => Promise)()).rejects.toThrow('tsc failed'); + expect(listGeneratedTsConfigs()).toEqual([]); + }); + + it(`should remove the transient tsconfig for a thenable (non-native-Promise) task result`, async () => { + createFile('tsconfig.json', JSON.stringify({ extends: '../../tsconfig.base.v8.json', compilerOptions: {} })); + createFile('package.json', JSON.stringify({ name: '@proj/one' })); + + // a spec-compliant thenable which is not an `instanceof Promise` - eg what some task runners/zones return + const thenable = { then: (onFulfilled: (value: void) => void) => onFulfilled(undefined) }; + tscTask.mockImplementationOnce(() => () => thenable); + + const task = ts.commonjs(); + + await (task as unknown as () => Promise)(); + + expect(listGeneratedTsConfigs()).toEqual([]); + }); + }); + + describe(`#downlevel`, () => { + it(`should downlevel every compiled module output to ES5 in place`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib-commonjs/index.js', commonjsOutput); + + await ts.downlevel(); + + expect(readFile('lib/index.js')).toContain(`import { Base } from './base'`); + expect(findSyntaxAboveTarget(readFile('lib/index.js'), 'es5')).toEqual([]); + expect(readFile('lib-commonjs/index.js')).toContain('exports.greet'); + expect(findSyntaxAboveTarget(readFile('lib-commonjs/index.js'), 'es5')).toEqual([]); + }); + + it(`should skip module outputs which were not compiled`, async () => { + createFile('lib/index.js', esmOutput); + + await expect(ts.downlevel()).resolves.not.toThrow(); + + expect(fs.existsSync(path.join(root, 'lib-commonjs'))).toBe(false); + }); + + it(`should honour the --module flag`, async () => { + getJustArgvMock.mockReturnValue({ module: { esm: false, cjs: true, amd: false } }); + createFile('lib/index.js', esmOutput); + createFile('lib-commonjs/index.js', commonjsOutput); + + await ts.downlevel(); + + expect(readFile('lib/index.js')).toEqual(esmOutput); + expect(findSyntaxAboveTarget(readFile('lib-commonjs/index.js'), 'es5')).toEqual([]); + }); + + it(`should be idempotent`, async () => { + createFile('lib/index.js', esmOutput); + + await ts.downlevel(); + const firstRun = readFile('lib/index.js'); + + await ts.downlevel(); + + expect(readFile('lib/index.js')).toEqual(firstRun); + }); + }); + + describe(`#amd`, () => { + it(`should create ES5 AMD output including declarations from the ESM output`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.d.ts', `export declare const greet: (name: string) => string;`); + createFile('lib/nested/other.js', esmOutput); + createFile('lib/nested/other.d.ts', `export declare const other: string;`); + + await ts.amd(); + + expect(readFile('lib-amd/index.js')).toMatch(/^define\(\[/); + expect(findSyntaxAboveTarget(readFile('lib-amd/index.js'), 'es5')).toEqual([]); + expect(readFile('lib-amd/nested/other.js')).toMatch(/^define\(\[/); + // declarations are module format agnostic, so they are copied over as is + expect(readFile('lib-amd/index.d.ts')).toEqual(readFile('lib/index.d.ts')); + expect(readFile('lib-amd/nested/other.d.ts')).toEqual(readFile('lib/nested/other.d.ts')); + }); + + it(`should prune stale amd files and declarations`, async () => { + createFile('lib/index.js', esmOutput); + createFile('lib/index.d.ts', `export declare const greet: (name: string) => string;`); + createFile('lib/removed.js', esmOutput); + createFile('lib/removed.d.ts', `export declare const removed: string;`); + + await ts.amd(); + + fs.rmSync(path.join(root, 'lib/removed.js')); + fs.rmSync(path.join(root, 'lib/removed.d.ts')); + + await ts.amd(); + + expect(fs.existsSync(path.join(root, 'lib-amd/removed.js'))).toBe(false); + expect(fs.existsSync(path.join(root, 'lib-amd/removed.d.ts'))).toBe(false); + expect(fs.existsSync(path.join(root, 'lib-amd/index.js'))).toBe(true); + expect(fs.existsSync(path.join(root, 'lib-amd/index.d.ts'))).toBe(true); + }); + + it(`should throw if there is no ESM output to convert`, async () => { + await expect(ts.amd()).rejects.toThrow(/cannot transpile "lib" -> "lib-amd"/); + }); + }); +}); diff --git a/scripts/tasks/src/ts.ts b/scripts/tasks/src/ts.ts index 66f1ef2d8d404..5c5045e6f52dd 100644 --- a/scripts/tasks/src/ts.ts +++ b/scripts/tasks/src/ts.ts @@ -1,9 +1,12 @@ +import * as fs from 'fs'; import * as path from 'path'; -import { TscTaskOptions, logger, tscTask } from 'just-scripts'; +import glob from 'glob'; +import { TaskFunction, TscTaskOptions, logger, tscTask } from 'just-scripts'; import { getJustArgv } from './argv'; -import { getTsPathAliasesConfig, getTsPathAliasesConfigUsedOnlyForDx } from './utils'; +import { transpileEmittedJs } from './swc'; +import { createTsConfigWithoutPathAliases, getTsPathAliasesConfig, getTsPathAliasesConfigUsedOnlyForDx } from './utils'; const libPath = path.resolve(process.cwd(), 'lib'); const srcPath = path.resolve(process.cwd(), 'src'); @@ -11,7 +14,28 @@ const srcPath = path.resolve(process.cwd(), 'src'); const useTsBuildInfo = /[\\/]packages[\\/]fluentui[\\/]/.test(process.cwd()) && path.basename(process.cwd()) !== 'perf-test-northstar'; -function prepareTsTaskConfig(options: TscTaskOptions) { +const outputPaths = { esm: 'lib', commonjs: 'lib-commonjs', amd: 'lib-amd' } as const; + +/** + * ECMAScript version that packages flagged with the `ships-es5` project tag published before the + * TypeScript 6 migration. + * + * TypeScript 6 removed `target: 'es5'`, so this is applied by SWC on top of the compiler emitted + * output instead of by `tsc` itself. Which packages are on that baseline is explicit project + * metadata (`ships-es5`), never inferred - see `metadata-utils.ts#shipsES5`. + */ +const downlevelTarget = 'es5'; + +/** + * ECMAScript version of the `lib-amd` artifact. + * + * Pre TypeScript 6 the AMD output was emitted by a dedicated `tsc --target es5 --module amd` run, + * which overrode the `target` of the package tsconfig. Therefore `lib-amd` is ES5 for every package + * that ships it, even for the ones whose `lib`/`lib-commonjs` are on a modern baseline. + */ +const amdTarget = 'es5'; + +function prepareTsTaskConfig(options: TscTaskOptions): { options: TscTaskOptions; cleanup: () => void } { // docs say pretty is on by default, but it's actually disabled when tsc is run in a // non-TTY context (which is what just-scripts tscTask does) // https://github.com/nrwl/nx/issues/9069#issuecomment-1048028504 @@ -28,11 +52,12 @@ function prepareTsTaskConfig(options: TscTaskOptions) { if (isUsingPathAliasesForDx()) { logger.info(`📣 TSC: Project is using TS path aliases for DX. Disabling aliases for build.`); - options.baseUrl = '.'; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(tsConfigFileForCompilation, 'build'); + options.rootDir = './src'; - options.project = tsConfigFileForCompilation; + options.project = noPathAliasesConfig.path; - return options; + return { options, cleanup: noPathAliasesConfig.cleanup }; } const { isUsingTsSolutionConfigs, tsConfigFileNames, tsConfigs } = getTsPathAliasesConfig(); @@ -46,36 +71,170 @@ function prepareTsTaskConfig(options: TscTaskOptions) { options.project = tsConfigFileNames.lib; } - return options; + return { options, cleanup: noop }; +} + +function noop() { + /* nothing to clean up */ +} + +/** + * Detects both native `Promise`s and thenables (eg zone.js-wrapped promises, or any other + * spec-compliant thenable a task might return) - `instanceof Promise` alone misses those. + */ +function isThenable(value: unknown): value is PromiseLike { + return ( + (typeof value === 'object' || typeof value === 'function') && + value !== null && + typeof (value as { then?: unknown }).then === 'function' + ); +} + +/** + * Guarantees that transient build artifacts (the generated "no path aliases" tsconfig) are removed + * as soon as the compilation finished, instead of relying on the process exit hook only. + */ +function withCleanup(taskFn: TaskFunction, cleanup: () => void): TaskFunction { + return function tscWithCleanup(this: unknown, ...args: Parameters) { + let result; + + try { + result = (taskFn as (...taskArgs: Parameters) => unknown).apply(this, args); + } catch (err) { + cleanup(); + throw err; + } + + if (isThenable(result)) { + return result.then( + value => { + cleanup(); + return value; + }, + err => { + cleanup(); + throw err; + }, + ); + } + + cleanup(); + + return result; + } as TaskFunction; } export const ts = { commonjs: () => { - const options = prepareTsTaskConfig({ - outDir: 'lib-commonjs', + const { options, cleanup } = prepareTsTaskConfig({ + outDir: outputPaths.commonjs, module: 'commonjs', ...(useTsBuildInfo && { tsBuildInfoFile: '.commonjs.tsbuildinfo' }), }); - return tscTask(options); + return withCleanup(tscTask(options), cleanup); }, esm: () => { - const options = prepareTsTaskConfig({ - outDir: 'lib', + const { options, cleanup } = prepareTsTaskConfig({ + outDir: outputPaths.esm, module: 'esnext', }); // Use default tsbuildinfo for this variant - return tscTask(options); + return withCleanup(tscTask(options), cleanup); + }, + /** + * Downlevels compiler emitted `lib`(ESM) and `lib-commonjs`(CJS) output to the ES5 baseline that + * packages tagged `ships-es5` published before the TypeScript 6 migration. + * + * TypeScript 6 removed `target: 'es5'`, so the ES5 emit moved out of the compiler - `tsc` type checks and emits + * declarations + modern JS, SWC downlevels the JS, which is the very same split that converged packages shipping + * AMD (`@fluentui/react-portal-compat*`) already use. + */ + downlevel: async () => { + const moduleFlag = getJustArgv().module; + const moduleOutputs = [ + { module: 'es6', outputPath: outputPaths.esm, enabled: moduleFlag ? moduleFlag.esm : true }, + { module: 'commonjs', outputPath: outputPaths.commonjs, enabled: moduleFlag ? moduleFlag.cjs : true }, + ] as const; + + for (const { module, outputPath, enabled } of moduleOutputs) { + if (!enabled) { + continue; + } + + if (!fs.existsSync(path.resolve(process.cwd(), outputPath))) { + logger.info(`📣 SWC: "${outputPath}" doesn't exist. Skipping ${downlevelTarget} downlevel.`); + continue; + } + + const result = await transpileEmittedJs({ + inputPath: outputPath, + outputPath, + module, + target: downlevelTarget, + }); + + logger.info( + `📣 SWC: downleveled ${result.transpiled.length}/${result.files.length} files in "${outputPath}" to ${downlevelTarget}.`, + ); + } }, - amd: () => { - const options = prepareTsTaskConfig({ - target: 'es5', - outDir: 'lib-amd', + /** + * Creates the AMD (`lib-amd`) artifact from the emitted ESM output. + * + * TypeScript 6 removed `module: 'amd'` - and `moduleResolution: 'bundler'`, which v8 packages use, is not + * compatible with it either - so the module transform is done by SWC. Declaration files are module format + * agnostic, therefore they are copied over from `lib` instead of being re-emitted. + * + * `lib-amd` is a fully derived directory: files which don't exist in `lib` anymore are pruned, so partial + * or repeated invocations can never leave stale artifacts behind. + */ + amd: async () => { + const result = await transpileEmittedJs({ + inputPath: outputPaths.esm, + outputPath: outputPaths.amd, module: 'amd', - ...(useTsBuildInfo && { tsBuildInfoFile: '.amd.tsbuildinfo' }), + target: amdTarget, }); - - return tscTask(options); + const declarations = syncDeclarations(outputPaths.esm, outputPaths.amd); + + logger.info( + `📣 SWC: created "${outputPaths.amd}" from "${outputPaths.esm}" (${result.transpiled.length}/${ + result.files.length + } files, ${declarations.copied.length} declarations, ${ + result.pruned.length + declarations.pruned.length + } stale files pruned).`, + ); }, }; + +/** + * Copies declarations of `fromPath` over to `toPath` and removes the ones which don't exist in `fromPath` anymore. + */ +function syncDeclarations(fromPath: string, toPath: string) { + const absoluteFromPath = path.resolve(process.cwd(), fromPath); + const absoluteToPath = path.resolve(process.cwd(), toPath); + const fileNames = glob.sync('**/*.d.ts', { cwd: absoluteFromPath, nodir: true }); + + for (const fileName of fileNames) { + const destination = path.join(absoluteToPath, fileName); + + fs.mkdirSync(path.dirname(destination), { recursive: true }); + fs.copyFileSync(path.join(absoluteFromPath, fileName), destination); + } + + const expected = new Set(fileNames); + const pruned: string[] = []; + + for (const fileName of glob.sync('**/*.d.ts', { cwd: absoluteToPath, nodir: true })) { + if (expected.has(fileName)) { + continue; + } + + fs.rmSync(path.join(absoluteToPath, fileName), { force: true }); + pruned.push(fileName); + } + + return { copied: fileNames, pruned }; +} diff --git a/scripts/tasks/src/type-check-project.spec.ts b/scripts/tasks/src/type-check-project.spec.ts new file mode 100644 index 0000000000000..2f16ce59ad1ea --- /dev/null +++ b/scripts/tasks/src/type-check-project.spec.ts @@ -0,0 +1,145 @@ +import { spawnSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { main, typeCheckProject } from './type-check-project'; + +/** + * Behavioural regression test for the `--baseUrl .` replacement. + * + * Apps used to type check with `tsc -p . --noEmit --baseUrl .`, which made the workspace root + * relative `paths` of `tsconfig.base.*.json` unresolvable on purpose - so a dependency resolved to + * its published/built declarations instead of its sources. TypeScript 6 removed `baseUrl`. + */ +describe(`typeCheckProject`, () => { + const tmpRoot = path.join(__dirname, '../tmp'); + let root: string; + let appRoot: string; + + function createFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(path.join(root, filePath)), { recursive: true }); + fs.writeFileSync(path.join(root, filePath), content, 'utf-8'); + } + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + root = fs.mkdtempSync(path.join(tmpRoot, 'type-check-project-')); + appRoot = path.join(root, 'app'); + + createFile( + 'tsconfig.base.json', + JSON.stringify({ + compilerOptions: { + module: 'esnext', + moduleResolution: 'bundler', + skipLibCheck: true, + types: [], + noEmit: true, + paths: { '@proj/dep': ['./packages/dep/src/index.ts'] }, + }, + }), + ); + createFile('packages/dep/src/index.ts', `export const dep = 'from source';`); + createFile('app/tsconfig.json', JSON.stringify({ extends: '../tsconfig.base.json', include: ['src'] })); + createFile('app/src/index.ts', [`import { dep } from '@proj/dep';`, `export const value = dep;`].join('\n')); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + function runPlainTsc() { + return spawnSync(process.execPath, [require.resolve('typescript/lib/tsc.js'), '-p', '.', '--noEmit'], { + cwd: appRoot, + encoding: 'utf-8', + }); + } + + function listGeneratedTsConfigs() { + return fs.readdirSync(appRoot).filter(fileName => fileName.startsWith('tsconfig.__generated')); + } + + it(`should type check against workspace sources when path aliases are on (control)`, () => { + const actual = runPlainTsc(); + + expect(actual.stdout).toEqual(''); + expect(actual.status).toEqual(0); + }); + + it(`should turn path aliases off, so imports resolve to built declarations instead of sources`, () => { + const exitCode = typeCheckProject({ cwd: appRoot }); + + expect(exitCode).not.toEqual(0); + expect(listGeneratedTsConfigs()).toEqual([]); + }); + + it(`should pass once the dependency is resolvable without path aliases`, () => { + fs.mkdirSync(path.join(appRoot, 'node_modules/@proj/dep'), { recursive: true }); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/package.json'), + JSON.stringify({ name: '@proj/dep', version: '1.0.0', types: './index.d.ts', main: './index.js' }), + 'utf-8', + ); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/index.d.ts'), + `export declare const dep: string;`, + 'utf-8', + ); + + expect(typeCheckProject({ cwd: appRoot })).toEqual(0); + expect(listGeneratedTsConfigs()).toEqual([]); + }); + + describe(`#main`, () => { + const originalCwd = process.cwd(); + const originalExitCode = process.exitCode; + + afterEach(() => { + process.chdir(originalCwd); + process.exitCode = originalExitCode; + }); + + it.each(['-p', '--project'])(`should reject "%s" without a value instead of silently falling back`, flag => { + expect(() => main([flag])).toThrow(/"(-p|--project)" requires a value/); + }); + + it(`should not throw the argv-validation error when "-p"/"--project" is given a value`, () => { + fs.mkdirSync(path.join(appRoot, 'node_modules/@proj/dep'), { recursive: true }); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/package.json'), + JSON.stringify({ name: '@proj/dep', version: '1.0.0', types: './index.d.ts', main: './index.js' }), + 'utf-8', + ); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/index.d.ts'), + `export declare const dep: string;`, + 'utf-8', + ); + + process.chdir(appRoot); + + expect(() => main(['-p', 'tsconfig.json'])).not.toThrow(); + expect(process.exitCode).toEqual(0); + expect(listGeneratedTsConfigs()).toEqual([]); + }); + + it(`should default to "tsconfig.json" when no "-p"/"--project" flag is given`, () => { + fs.mkdirSync(path.join(appRoot, 'node_modules/@proj/dep'), { recursive: true }); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/package.json'), + JSON.stringify({ name: '@proj/dep', version: '1.0.0', types: './index.d.ts', main: './index.js' }), + 'utf-8', + ); + fs.writeFileSync( + path.join(appRoot, 'node_modules/@proj/dep/index.d.ts'), + `export declare const dep: string;`, + 'utf-8', + ); + + process.chdir(appRoot); + + expect(() => main([])).not.toThrow(); + expect(process.exitCode).toEqual(0); + }); + }); +}); diff --git a/scripts/tasks/src/type-check-project.ts b/scripts/tasks/src/type-check-project.ts new file mode 100644 index 0000000000000..1a41143d2d6fb --- /dev/null +++ b/scripts/tasks/src/type-check-project.ts @@ -0,0 +1,60 @@ +import { spawnSync } from 'node:child_process'; +import * as path from 'node:path'; + +import { createTsConfigWithoutPathAliases } from './utils'; + +export interface TypeCheckProjectOptions { + /** + * tsconfig to type check, relative to `cwd`. + * + * @defaultValue 'tsconfig.json' + */ + project?: string; + /** + * @defaultValue process.cwd() + */ + cwd?: string; +} + +/** + * Type checks a project **without** TS path aliases, so imports resolve to the built declarations + * of workspace dependencies instead of their sources. + * + * This used to be `tsc -p . --noEmit --baseUrl .`: `baseUrl` was overridden to the project folder, + * which made the workspace root relative `paths` of `tsconfig.base.*.json` unresolvable. TypeScript 6 + * removed `baseUrl` and resolves `paths` relative to the config file declaring them, so the only + * supported opt out is `"paths": null` - which can be expressed in a config file only, hence this wrapper. + */ +export function typeCheckProject(options: TypeCheckProjectOptions = {}): number { + const { project = 'tsconfig.json', cwd = process.cwd() } = options; + + const noPathAliasesConfig = createTsConfigWithoutPathAliases(path.resolve(cwd, project), 'type-check'); + + try { + const result = spawnSync( + process.execPath, + [require.resolve('typescript/lib/tsc.js'), '-p', noPathAliasesConfig.path, '--pretty', '--noEmit'], + { cwd, stdio: 'inherit' }, + ); + + if (result.error) { + throw result.error; + } + + return result.status ?? 1; + } finally { + noPathAliasesConfig.cleanup(); + } +} + +export function main(argv: string[] = process.argv.slice(2)) { + const projectFlagIndex = argv.findIndex(arg => arg === '-p' || arg === '--project'); + + if (projectFlagIndex !== -1 && argv[projectFlagIndex + 1] === undefined) { + throw new Error(`"${argv[projectFlagIndex]}" requires a value (eg "-p tsconfig.json")`); + } + + const project = projectFlagIndex === -1 ? undefined : argv[projectFlagIndex + 1]; + + process.exitCode = typeCheckProject({ project }); +} diff --git a/scripts/tasks/src/type-check.ts b/scripts/tasks/src/type-check.ts index 46ce1d34146e0..0c252c90bd2bb 100644 --- a/scripts/tasks/src/type-check.ts +++ b/scripts/tasks/src/type-check.ts @@ -5,7 +5,7 @@ import { logger } from 'just-scripts'; // eslint-disable-next-line import/no-extraneous-dependencies import { exec } from 'just-scripts-utils'; -import { type TsConfig, getTsPathAliasesConfig } from './utils'; +import { type TsConfig, createTsConfigWithoutPathAliases, getTsPathAliasesConfig } from './utils'; export function typeCheck(): Promise | undefined { const { isUsingTsSolutionConfigs, tsConfigs } = getTsPathAliasesConfig(); @@ -24,9 +24,13 @@ export function typeCheck(): Promise | undefined { const tsConfigsRefs = getTsConfigs(config, { spec: false, e2e: false }); const asyncQueue: Array> = []; + const cleanupQueue: Array<() => void> = []; for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --baseUrl . --noEmit`; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(ref, 'type-check'); + cleanupQueue.push(noPathAliasesConfig.cleanup); + + const program = `tsc -p ${noPathAliasesConfig.path} --pretty --noEmit`; asyncQueue.push(exec(program)); } @@ -38,6 +42,9 @@ export function typeCheck(): Promise | undefined { .catch(err => { console.error(err.stdout); process.exit(1); + }) + .finally(() => { + cleanupQueue.forEach(cleanup => cleanup()); }); } @@ -51,6 +58,7 @@ export async function typeCheckWithConfigOverride( const tsConfigsRefs = getTsConfigs(rootTsConfig, { spec: false, e2e: false }); const configs: { [path: string]: Record } = {}; + const cleanupQueue: Array<() => void> = []; for (const ref of tsConfigsRefs) { const refPath = path.join(cwd, ref); @@ -65,7 +73,10 @@ export async function typeCheckWithConfigOverride( const asyncQueue: Array> = []; for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --baseUrl . --noEmit`; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(ref, 'type-check'); + cleanupQueue.push(noPathAliasesConfig.cleanup); + + const program = `tsc -p ${noPathAliasesConfig.path} --pretty --noEmit`; asyncQueue.push(exec(program)); } @@ -79,6 +90,8 @@ export async function typeCheckWithConfigOverride( // set exit code to 1 to exit process naturally and allow finally block to run process.exitCode = 1; } finally { + cleanupQueue.forEach(cleanup => cleanup()); + const entries = Object.entries(configs); for (const [configPath, config] of entries) { fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8'); diff --git a/scripts/tasks/src/utils.spec.ts b/scripts/tasks/src/utils.spec.ts index 3759e077bb02d..d9a2f6a8aa07a 100644 --- a/scripts/tasks/src/utils.spec.ts +++ b/scripts/tasks/src/utils.spec.ts @@ -1,8 +1,11 @@ +import { execFileSync } from 'node:child_process'; +import * as fs from 'node:fs'; +import { pathToFileURL } from 'node:url'; import * as path from 'path'; import { workspaceRoot } from '@nx/devkit'; -import { getTsPathAliasesApiExtractorConfig } from './utils'; +import { createTsConfigWithoutPathAliases, getTsPathAliasesApiExtractorConfig } from './utils'; type DeepPartial = Partial<{ [P in keyof T]: DeepPartial }>; describe(`utils`, () => { @@ -43,9 +46,7 @@ describe(`utils`, () => { definitionsRootPath: 'dist/for/types', }); - expect(actual.overrideTsconfig.compilerOptions).toEqual( - expect.objectContaining({ paths: undefined, baseUrl: '.' }), - ); + expect(actual.overrideTsconfig.compilerOptions).toEqual(expect.objectContaining({ paths: undefined })); }); // This is not used unless api-extractor resolves resolving workspace d.ts packages - see https://github.com/microsoft/rushstack/pull/3321, https://github.com/microsoft/rushstack/pull/3339 @@ -68,4 +69,151 @@ describe(`utils`, () => { ); }); }); + describe(`#createTsConfigWithoutPathAliases`, () => { + const tmpRoot = path.join(__dirname, '../tmp'); + let projectRoot: string; + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + projectRoot = fs.mkdtempSync(path.join(tmpRoot, 'no-path-aliases-')); + fs.writeFileSync( + path.join(projectRoot, 'tsconfig.lib.json'), + JSON.stringify({ compilerOptions: { outDir: './lib' }, include: ['src'] }), + 'utf-8', + ); + }); + + afterEach(() => { + fs.rmSync(projectRoot, { recursive: true, force: true }); + }); + + function listGenerated() { + return fs.readdirSync(projectRoot).filter(fileName => fileName.startsWith('tsconfig.__generated')); + } + + it(`should extend the original config and null path aliases`, () => { + const actual = createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.lib.json'), 'type-check'); + + expect(path.dirname(actual.path)).toEqual(projectRoot); + expect(path.basename(actual.path)).toMatch( + /^tsconfig\.__generated-no-path-aliases-type-check-\d+-\d+-[a-f0-9]+-tsconfig\.lib\.json$/, + ); + expect(JSON.parse(fs.readFileSync(actual.path, 'utf-8'))).toEqual({ + extends: './tsconfig.lib.json', + compilerOptions: { paths: null }, + }); + + actual.cleanup(); + }); + + it(`should create a unique file per invocation, so concurrent tsc runs don't race`, () => { + const first = createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.lib.json'), 'type-check'); + const second = createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.lib.json'), 'type-check'); + + expect(first.path).not.toEqual(second.path); + expect(listGenerated()).toHaveLength(2); + + first.cleanup(); + + // cleaning up one must not remove the other one + expect(fs.existsSync(second.path)).toBe(true); + + second.cleanup(); + + expect(listGenerated()).toEqual([]); + }); + + it(`should be idempotent on repeated cleanup`, () => { + const actual = createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.lib.json'), 'build'); + + actual.cleanup(); + + expect(() => actual.cleanup()).not.toThrow(); + }); + + it(`should throw if the config to extend doesn't exist`, () => { + expect(() => createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.nope.json'), 'build')).toThrow( + /Cannot disable TS path aliases .* doesn't exist/, + ); + }); + + /** + * `packages/web-components` and `packages/charts/chart-web-components` ship their own copy of + * this helper (they must not depend on the `just` based v8 build tooling). The duplication is + * intentional, their behaviour must not drift. + */ + it.each([ + 'packages/web-components/scripts/tsconfig-utils.js', + 'packages/charts/chart-web-components/scripts/tsconfig-utils.js', + ])(`should be behaviourally aligned with %s`, helperPath => { + const moduleUrl = pathToFileURL(path.join(workspaceRoot, helperPath)).href; + const tsConfigPath = path.join(projectRoot, 'tsconfig.lib.json'); + const script = [ + `import { createTsConfigWithoutPathAliases } from ${JSON.stringify(moduleUrl)};`, + `import fs from 'node:fs';`, + `const first = createTsConfigWithoutPathAliases(${JSON.stringify(tsConfigPath)}, 'type-check');`, + `const second = createTsConfigWithoutPathAliases(${JSON.stringify(tsConfigPath)}, 'type-check');`, + `const content = JSON.parse(fs.readFileSync(first.path, 'utf-8'));`, + `first.cleanup();`, + `let error = null;`, + `try { createTsConfigWithoutPathAliases(${JSON.stringify( + path.join(projectRoot, 'tsconfig.nope.json'), + )}, 'build'); } catch (err) { error = err.message; }`, + `console.log(JSON.stringify({`, + ` unique: first.path !== second.path,`, + ` fileName: first.path.split('/').pop(),`, + ` content,`, + ` firstRemoved: !fs.existsSync(first.path),`, + ` secondKept: fs.existsSync(second.path),`, + ` error,`, + ` listeners: { exit: process.listenerCount('exit'), sigint: process.listenerCount('SIGINT') },`, + `}));`, + // the leftover config must be removed by the process exit hook + ].join('\n'); + + const output = execFileSync(process.execPath, ['--input-type=module', '-e', script], { encoding: 'utf-8' }); + const actual = JSON.parse(output.trim().split('\n').pop() as string); + + expect(actual.unique).toBe(true); + expect(actual.fileName).toMatch( + /^tsconfig\.__generated-no-path-aliases-type-check-\d+-\d+-[a-f0-9]+-tsconfig\.lib\.json$/, + ); + expect(actual.content).toEqual({ extends: './tsconfig.lib.json', compilerOptions: { paths: null } }); + expect(actual.firstRemoved).toBe(true); + expect(actual.secondKept).toBe(true); + expect(actual.error).toMatch(/Cannot disable TS path aliases .* doesn't exist/); + expect(actual.listeners).toEqual({ exit: 1, sigint: 1 }); + // everything is cleaned up once the process exited + expect(listGenerated()).toEqual([]); + }); + + it(`should register one process listener at most, no matter how many configs are created`, () => { + const created: Array<{ path: string; cleanup: () => void }> = []; + const countOwnListeners = () => ({ + exit: process.listeners('exit').filter(listener => listener.name === 'cleanupTransientTsConfigs').length, + sigint: process.listeners('SIGINT').filter(listener => listener.name === 'cleanupTransientTsConfigsOnSignal') + .length, + }); + const before = countOwnListeners(); + + // fresh module instance, so the (module scoped) listener registration happens within this test + jest.isolateModules(() => { + const utils: typeof import('./utils') = require('./utils'); + + for (let i = 0; i < 20; i++) { + created.push(utils.createTsConfigWithoutPathAliases(path.join(projectRoot, 'tsconfig.lib.json'), 'stress')); + } + }); + + const after = countOwnListeners(); + + expect(listGenerated()).toHaveLength(20); + expect(after.exit - before.exit).toEqual(1); + expect(after.sigint - before.sigint).toEqual(1); + + created.forEach(config => config.cleanup()); + + expect(listGenerated()).toEqual([]); + }); + }); }); diff --git a/scripts/tasks/src/utils.ts b/scripts/tasks/src/utils.ts index 252b1de22a523..46a645fd7d56f 100644 --- a/scripts/tasks/src/utils.ts +++ b/scripts/tasks/src/utils.ts @@ -1,4 +1,5 @@ import { execSync } from 'child_process'; +import * as crypto from 'crypto'; import * as fs from 'fs'; import * as path from 'path'; @@ -183,7 +184,6 @@ export function getTsPathAliasesApiExtractorConfig(options: { * */ paths: undefined, - baseUrl: '.', }, }; @@ -221,3 +221,84 @@ export interface TsConfig { exclude?: string[]; references?: Array<{ path: string }>; } + +/** + * All transient configs created by this module which have not been cleaned up yet. + * + * Registering them in one place keeps the number of process listeners constant - one listener per + * module - no matter how many `tsc` invocations a task performs. + */ +const pendingTransientTsConfigs = new Set(); +let transientTsConfigsCounter = 0; +let processListenersRegistered = false; + +function removeTransientTsConfig(generatedPath: string) { + pendingTransientTsConfigs.delete(generatedPath); + fs.rmSync(generatedPath, { force: true }); +} + +function cleanupTransientTsConfigs() { + for (const generatedPath of [...pendingTransientTsConfigs]) { + removeTransientTsConfig(generatedPath); + } +} + +function registerProcessListeners() { + if (processListenersRegistered) { + return; + } + + processListenersRegistered = true; + + process.on('exit', cleanupTransientTsConfigs); + + // node does not run `exit` listeners when a process is terminated by a signal, + // so clean up explicitly and re-raise to keep the default termination semantics + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.once(signal, cleanupTransientTsConfigsOnSignal); + } +} + +function cleanupTransientTsConfigsOnSignal(signal: NodeJS.Signals) { + cleanupTransientTsConfigs(); + process.kill(process.pid, signal); +} + +/** + * Creates a transient tsconfig, next to `tsConfigPath`, which turns TS path aliases off + * (`"paths": null`) for a single `tsc` invocation and returns its path. + * + * TypeScript 6 deprecates `baseUrl`, which used to be (ab)used as `tsc --baseUrl .` to make the + * workspace root relative `paths` entries unresolvable. TypeScript 6 resolves `paths` relative to + * the config file that declares them, so nulling `paths` is now the only supported way to opt a + * compilation out of path aliases - and it cannot be expressed via CLI flags, only via a config file. + * + * NOTES: + * - the generated config lives next to the original one, so every relative path + * (`extends`/`include`/`outDir`/`rootDir`/`references`) keeps resolving identically + * - the file name is unique per process and invocation, so parallel/concurrent `tsc` runs + * (which this repo does per project and per tsconfig reference) can never delete each other's config + */ +export function createTsConfigWithoutPathAliases(tsConfigPath: string, purpose: string) { + if (!fs.existsSync(tsConfigPath)) { + throw new Error(`Cannot disable TS path aliases for "${tsConfigPath}", because the file doesn't exist.`); + } + + const configFileName = path.basename(tsConfigPath); + const uniqueId = `${process.pid}-${transientTsConfigsCounter++}-${crypto.randomBytes(4).toString('hex')}`; + const generatedPath = path.join( + path.dirname(tsConfigPath), + `tsconfig.__generated-no-path-aliases-${purpose}-${uniqueId}-${configFileName}`, + ); + + fs.writeFileSync( + generatedPath, + JSON.stringify({ extends: `./${configFileName}`, compilerOptions: { paths: null } }, null, 2), + 'utf-8', + ); + + pendingTransientTsConfigs.add(generatedPath); + registerProcessListeners(); + + return { path: generatedPath, cleanup: () => removeTransientTsConfig(generatedPath) }; +} diff --git a/scripts/tasks/src/verify-packaging.spec.ts b/scripts/tasks/src/verify-packaging.spec.ts new file mode 100644 index 0000000000000..71f9e8f051a5f --- /dev/null +++ b/scripts/tasks/src/verify-packaging.spec.ts @@ -0,0 +1,299 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import { verifyPackaging } from './verify-packaging'; + +jest.mock('node:child_process', () => ({ spawnSync: jest.fn() })); + +const spawnSync: jest.Mock = jest.requireMock('node:child_process').spawnSync; + +describe(`verifyPackaging`, () => { + const tmpRoot = path.join(__dirname, '../tmp'); + const originalCwd = process.cwd(); + let root: string; + + const es5Esm = [ + `import { Base } from "./base";`, + `export var Greeter = /*#__PURE__*/ function (Base) { return Base; }(Base);`, + ].join('\n'); + const es5CommonJs = [`"use strict";`, `var _base = require("./base");`, `exports.greeter = _base.Base;`].join('\n'); + const es5Amd = [ + `define(["require", "exports", "./base"], function (require, exports, _base) {`, + ` "use strict";`, + ` exports.greeter = _base.Base;`, + `});`, + ].join('\n'); + const declaration = `export declare const greeter: string;`; + + function createFile(filePath: string, content: string) { + fs.mkdirSync(path.dirname(path.join(root, filePath)), { recursive: true }); + fs.writeFileSync(path.join(root, filePath), content, 'utf-8'); + } + + function setup( + options: { + tags?: string[]; + target?: string; + files?: Record; + /** files reported by `npm pack --dry-run` on top of the ones written to disk */ + extraPackedFiles?: string[]; + omitFromPack?: string[]; + dependencies?: Record; + } = {}, + ) { + const { + tags = ['v8', 'ships-es5'], + target = 'es2015', + extraPackedFiles = [], + omitFromPack = [], + dependencies, + } = options; + const files = options.files ?? { + 'lib/index.js': es5Esm, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + 'lib-amd/index.js': es5Amd, + 'lib-amd/index.d.ts': declaration, + }; + + createFile( + 'package.json', + JSON.stringify({ name: '@proj/one', version: '1.0.0', main: 'lib-commonjs/index.js', dependencies }), + ); + createFile('project.json', JSON.stringify({ name: 'one', tags })); + createFile('tsconfig.json', JSON.stringify({ compilerOptions: { target } })); + createFile('CHANGELOG.md', '# changelog'); + createFile('README.md', '# readme'); + createFile('LICENSE', 'MIT'); + createFile('dist/index.d.ts', declaration); + + for (const [filePath, content] of Object.entries(files)) { + createFile(filePath, content); + } + + const packedFiles = [ + 'LICENSE', + 'README.md', + 'CHANGELOG.md', + 'package.json', + 'dist/index.d.ts', + ...Object.keys(files), + ...extraPackedFiles, + ].filter(filePath => !omitFromPack.includes(filePath)); + + spawnSync.mockReturnValue({ + output: ['', `${packedFiles.map(filePath => `npm notice 1.2kB ${filePath}`).join('\n')}\n`, ''], + }); + } + + beforeEach(() => { + fs.mkdirSync(tmpRoot, { recursive: true }); + root = fs.mkdtempSync(path.join(tmpRoot, 'verify-packaging-')); + process.chdir(root); + }); + + afterEach(() => { + process.chdir(originalCwd); + fs.rmSync(root, { recursive: true, force: true }); + spawnSync.mockReset(); + }); + + it(`should pass for artifacts which match the published contract`, () => { + setup(); + + expect(() => verifyPackaging({ production: true })).not.toThrow(); + }); + + it(`should fail if the ES5 downlevel did not run`, () => { + setup({ + files: { + 'lib/index.js': `export const greeter = () => 'hi';`, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow(/"lib\/index.js" is emitted for "es5"/); + }); + + it(`should verify the compiler target of packages which are not on the ES5 baseline`, () => { + const es2019 = `export const greeter = () => 'hi';`; + + setup({ + tags: ['v8'], + target: 'ES2019', + files: { + 'lib/index.js': es2019, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).not.toThrow(); + + setup({ + tags: ['v8'], + target: 'ES2019', + files: { + 'lib/index.js': `export const greeter = other?.value;`, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow(/is emitted for "es2019"/); + }); + + it(`should fail if the AMD artifact is not AMD wrapped`, () => { + setup({ + files: { + 'lib/index.js': es5Esm, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + 'lib-amd/index.js': es5CommonJs, + 'lib-amd/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: true })).toThrow( + /"lib-amd\/index.js" is emitted as "amd" module, got "commonjs"/, + ); + }); + + it(`should fail if the AMD artifact does not mirror the ESM one`, () => { + setup({ + files: { + 'lib/index.js': es5Esm, + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + 'lib-amd/index.js': es5Amd, + 'lib-amd/index.d.ts': declaration, + 'lib-amd/stale.js': es5Amd, + 'lib-amd/stale.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: true })).toThrow(/"lib-amd" mirrors "lib".*stale.js/s); + }); + + it(`should fail if an emitted helper import cannot be resolved`, () => { + setup({ + dependencies: { '@swc/helpers': '^0.5.23' }, + files: { + 'lib/index.js': [ + `import { _ as _call_super } from "@swc/helpers/_/__not_a_helper__";`, + `export var Greeter = function () {};`, + ].join('\n'), + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow( + /every runtime helper imported by "lib" is provided by the declared "@swc\/helpers" dependency/, + ); + }); + + it(`should fail if the package imports runtime helpers without declaring "@swc/helpers"`, () => { + setup({ + files: { + 'lib/index.js': [ + `import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";`, + `export var Greeter = function () {};`, + ].join('\n'), + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow( + /"lib" imports @swc\/helpers runtime helpers.*but the package does not declare "@swc\/helpers" as a dependency/s, + ); + }); + + it(`should fail if the declared "@swc/helpers" range does not accept the resolved helper version`, () => { + setup({ + // the installed/resolved copy of @swc/helpers (0.5.23) is verified to provide `_class_call_check`, + // but this range only accepts the "0.4.x" line - a consumer resolving this range would never + // actually get the version that was verified to provide the imported helper + dependencies: { '@swc/helpers': '^0.4.0' }, + files: { + 'lib/index.js': [ + `import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";`, + `export var Greeter = function () {};`, + ].join('\n'), + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow( + /declared "@swc\/helpers" dependency \("\^0\.4\.0"\) permits versions older than "0\.5\.23"/, + ); + }); + + it(`should accept helper imports which the declared dependency provides`, () => { + setup({ + dependencies: { '@swc/helpers': '^0.5.23' }, + files: { + 'lib/index.js': [ + `import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";`, + `export var Greeter = function () {};`, + ].join('\n'), + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).not.toThrow(); + }); + + it(`should fail if the declared range permits helper versions older than the required floor`, () => { + setup({ + dependencies: { '@swc/helpers': '^0.5.1' }, + files: { + 'lib/index.js': [ + `import { _ as _class_call_check } from "@swc/helpers/_/_class_call_check";`, + `export var Greeter = function () {};`, + ].join('\n'), + 'lib/index.d.ts': declaration, + 'lib-commonjs/index.js': es5CommonJs, + 'lib-commonjs/index.d.ts': declaration, + }, + }); + + expect(() => verifyPackaging({ production: false })).toThrow( + /declared "@swc\/helpers" dependency \("\^0\.5\.1"\) permits versions older than "0\.5\.23"/, + ); + }); + + it(`should fail if a module ships without its declaration`, () => { + setup({ omitFromPack: ['lib-commonjs/index.d.ts'] }); + + expect(() => verifyPackaging({ production: true })).toThrow( + /every published "lib-commonjs" module ships its declaration counterpart/, + ); + }); + + it(`should not verify artifacts of private packages`, () => { + setup({ + files: { + 'lib/index.js': `export const greeter = () => 'hi';`, + 'lib-commonjs/index.js': es5CommonJs, + }, + }); + createFile('package.json', JSON.stringify({ name: '@proj/one', version: '1.0.0', private: true })); + + expect(() => verifyPackaging({ production: true })).not.toThrow(); + }); +}); diff --git a/scripts/tasks/src/verify-packaging.ts b/scripts/tasks/src/verify-packaging.ts index eb00716372202..7cf7f236f9c7e 100644 --- a/scripts/tasks/src/verify-packaging.ts +++ b/scripts/tasks/src/verify-packaging.ts @@ -1,11 +1,14 @@ import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import path from 'node:path'; import micromatch from 'micromatch'; +import * as semver from 'semver'; +import * as ts from 'typescript'; import type { JustArgs } from './argv'; +import { type EsTarget, type ModuleShape, detectModuleShape, findSyntaxAboveTarget } from './ecma-syntax'; /** * @see https://docs.npmjs.com/cli/v10/commands/npm-publish#files-included-in-package @@ -22,10 +25,26 @@ const rootConfigFiles = [ ]; const nonProdAssets = ['assets/', 'docs/*', 'temp/*', 'bundle-size/*', '.storybook/*', 'stories/*']; +/** + * Amount of files per artifact folder which are parsed to verify the emitted syntax/module shape. + * + * The whole file list is verified for coverage (every `.js` has its declaration, `lib-amd` mirrors + * `lib`), only the (evenly spread) sample is parsed - packages like `@fluentui/react` ship + * thousands of files, parsing all of them on every CI run would not pay off. + */ +const artifactSampleSize = 40; + +/** + * eg `import { _ } from "@swc/helpers/_/_class_call_check"` / `require("@swc/helpers/_/_export_star")` + */ +const runtimeHelperSpecifierRegex = /["'](@swc\/helpers\/[^"']+)["']/g; + interface Options extends Partial {} export function verifyPackaging(options: Options) { const cwd = process.cwd(); - const packageJSON: { private?: boolean } = JSON.parse(readFileSync(path.join(cwd, 'package.json'), 'utf-8')); + const packageJSON: { private?: boolean; dependencies?: Record } = JSON.parse( + readFileSync(path.join(cwd, 'package.json'), 'utf-8'), + ); // no need to check if package is not being published yet if (packageJSON.private) { @@ -50,6 +69,7 @@ export function verifyPackaging(options: Options) { const shipsAMD = isV8package || tags.indexOf('ships-amd') !== -1; const shipsBundle = tags.indexOf('ships-bundle') !== -1; const shipsUmd = tags.indexOf('ships-umd') !== -1; + const shipsES5 = tags.indexOf('ships-es5') !== -1; const platform = { web: tags.indexOf('platform:web') !== -1, node: tags.indexOf('platform:node') !== -1 }; // shared assertions @@ -94,4 +114,305 @@ export function verifyPackaging(options: Options) { if (options.production && shipsAMD) { assert.ok(micromatch(processedResultArr, 'lib-amd/**/*.(js|map)').length, 'ships amd'); } + + verifyArtifacts({ + cwd, + packedFiles: processedResultArr, + isV8package, + shipsES5, + dependencies: packageJSON.dependencies ?? {}, + }); +} + +/** + * Verifies the actual content of the published JavaScript artifacts. + * + * TypeScript 6 removed `target: 'es5'` and `module: 'amd'`, so `lib`/`lib-commonjs`/`lib-amd` are + * not emitted by `tsc` alone anymore - a SWC post processing step downlevels the compiler output + * and re-modularizes it. Asserting on the file list only would not notice if that step silently + * stopped running, ran with the wrong target or left stale files behind. + */ +function verifyArtifacts(options: { + cwd: string; + packedFiles: string[]; + isV8package: boolean; + shipsES5: boolean; + dependencies: Record; +}) { + const { cwd, packedFiles, isV8package, shipsES5, dependencies } = options; + + if (!isV8package) { + return; + } + + const compilerTarget = readCompilerTarget(cwd); + const artifacts = [ + { dir: 'lib', target: shipsES5 ? ('es5' as const) : compilerTarget, shape: 'esm' as const }, + { dir: 'lib-commonjs', target: shipsES5 ? ('es5' as const) : compilerTarget, shape: 'commonjs' as const }, + /** + * pre TS6 `lib-amd` was emitted by a dedicated `tsc --target es5 --module amd` run which + * overrode the package `target`, so it is ES5 for every package which ships it + */ + { dir: 'lib-amd', target: 'es5' as const, shape: 'amd' as const }, + ]; + + const libFiles = publishedFiles(packedFiles, 'lib'); + + for (const artifact of artifacts) { + const jsFiles = publishedFiles(packedFiles, artifact.dir); + + if (jsFiles.length === 0) { + continue; + } + + verifyDeclarationCoverage({ packedFiles, dir: artifact.dir, jsFiles }); + verifyRuntimeHelpers({ cwd, dir: artifact.dir, jsFiles, dependencies }); + + if (artifact.dir === 'lib-amd' && libFiles.length > 0) { + assert.deepEqual( + jsFiles, + libFiles, + `"lib-amd" mirrors "lib" - stale or missing AMD files: ${JSON.stringify( + symmetricDifference(jsFiles, libFiles), + )}`, + ); + } + + for (const fileName of sample(jsFiles, artifactSampleSize)) { + const filePath = path.join(cwd, artifact.dir, fileName); + + if (!existsSync(filePath)) { + continue; + } + + const code = readFileSync(filePath, 'utf-8'); + + assert.deepEqual( + findSyntaxAboveTarget(code, artifact.target, filePath).map(feature => `${feature.name}(${feature.minTarget})`), + [], + `"${artifact.dir}/${fileName}" is emitted for "${artifact.target}"`, + ); + + const actualShape = detectModuleShape(code, filePath); + const expectedShapes = allowedShapes({ + artifactDir: artifact.dir, + expected: artifact.shape, + cwd, + fileName, + }); + + assert.ok( + expectedShapes.includes(actualShape), + `"${artifact.dir}/${fileName}" is emitted as "${expectedShapes.join('|')}" module, got "${actualShape}"`, + ); + } + } +} + +/** + * Modules without any import/export are emitted as plain scripts by every module transform, so + * they are a valid shape everywhere. AMD wrapping is required only if the ESM counterpart is a + * real module. + */ +function allowedShapes(options: { + artifactDir: string; + expected: ModuleShape; + cwd: string; + fileName: string; +}): ModuleShape[] { + const { artifactDir, expected, cwd, fileName } = options; + + if (artifactDir !== 'lib-amd') { + return [expected, 'script']; + } + + const esmFilePath = path.join(cwd, 'lib', fileName); + + if (existsSync(esmFilePath) && detectModuleShape(readFileSync(esmFilePath, 'utf-8'), esmFilePath) === 'esm') { + return ['amd']; + } + + return ['amd', 'script']; +} + +/** + * Every helper the emitted artifacts import must be provided by the `@swc/helpers` version the + * package declares. + * + * `@swc/core` grows/renames its helper set over time (eg `_create_super` -> `_call_super`), so the + * emitted output and the declared runtime dependency can silently drift apart - the artifact would + * then fail with `MODULE_NOT_FOUND` in a consumer's app instead of during the build. + * + * Resolving the imported specifiers only tells us that *some* installed copy of `@swc/helpers` + * provides them - in a workspace every package's dependency range is (mostly) hoisted to a single + * installed copy, so a regression of one package's declared range (eg back to `^0.5.1`) would stay + * unnoticed as long as some other package still pulls a newer one in. To catch that, the declared + * range's lower bound must be at least 0.5.23, the first repository-supported version that provides + * `_call_super` and the other helpers emitted by the current transform. The installed version is + * checked separately against the declared range. When SWC starts emitting a helper introduced after + * 0.5.23, raise this floor together with the repository-wide dependency range. + */ +function verifyRuntimeHelpers(options: { + cwd: string; + dir: string; + jsFiles: string[]; + dependencies: Record; +}) { + const { cwd, dir, jsFiles, dependencies } = options; + const helperSpecifiers = new Set(); + + for (const fileName of jsFiles) { + const filePath = path.join(cwd, dir, fileName); + + if (!existsSync(filePath)) { + continue; + } + + for (const match of readFileSync(filePath, 'utf-8').matchAll(runtimeHelperSpecifierRegex)) { + helperSpecifiers.add(match[1]); + } + } + + if (helperSpecifiers.size === 0) { + return; + } + + const declaredRange = dependencies['@swc/helpers']; + + assert.ok( + declaredRange, + `"${dir}" imports @swc/helpers runtime helpers (${[...helperSpecifiers].join(', ')}), ` + + `but the package does not declare "@swc/helpers" as a dependency`, + ); + + const unresolvable = [...helperSpecifiers].filter(specifier => { + try { + require.resolve(specifier, { paths: [cwd] }); + return false; + } catch { + return true; + } + }); + + assert.deepEqual( + unresolvable, + [], + `every runtime helper imported by "${dir}" is provided by the declared "@swc/helpers" dependency ` + + `(raise the declared version range if the emitted helper set moved on)`, + ); + + const resolvedPackageJsonPath = require.resolve('@swc/helpers/package.json', { paths: [cwd] }); + const resolvedVersion: string = JSON.parse(readFileSync(resolvedPackageJsonPath, 'utf-8')).version; + const minimumHelpersVersion = '0.5.23'; + const minimumDeclaredVersion = semver.minVersion(declaredRange); + + assert.ok( + minimumDeclaredVersion && semver.gte(minimumDeclaredVersion, minimumHelpersVersion), + `"${dir}"'s declared "@swc/helpers" dependency ("${declaredRange}") permits versions older than ` + + `"${minimumHelpersVersion}", which do not provide every helper emitted by the current SWC transform. ` + + `Raise the declared range floor to at least "${minimumHelpersVersion}".`, + ); + + assert.ok( + semver.satisfies(resolvedVersion, declaredRange, { includePrerelease: true }), + `"${dir}"'s declared "@swc/helpers" dependency ("${declaredRange}") does not accept "${resolvedVersion}" - ` + + `the installed version that was just verified to provide every imported helper. Widen the declared range so ` + + `it actually covers the resolved "@swc/helpers" version.`, + ); +} + +function verifyDeclarationCoverage(options: { packedFiles: string[]; dir: string; jsFiles: string[] }) { + const { packedFiles, dir, jsFiles } = options; + const declarations = new Set( + packedFiles + .filter(filePath => filePath.startsWith(`${dir}/`) && filePath.endsWith('.d.ts')) + .map(filePath => filePath.slice(dir.length + 1)), + ); + + const missing = jsFiles.filter(fileName => !declarations.has(fileName.replace(/\.js$/, '.d.ts'))); + + assert.deepEqual(missing, [], `every published "${dir}" module ships its declaration counterpart`); +} + +function publishedFiles(packedFiles: string[], dir: string) { + return packedFiles + .filter(filePath => filePath.startsWith(`${dir}/`) && filePath.endsWith('.js')) + .map(filePath => filePath.slice(dir.length + 1)) + .sort(); +} + +function symmetricDifference(a: string[], b: string[]) { + const setA = new Set(a); + const setB = new Set(b); + + return [...a.filter(item => !setB.has(item)), ...b.filter(item => !setA.has(item))]; +} + +/** + * Evenly spread selection so that the sample covers the whole (alphabetically sorted) file list. + */ +function sample(files: string[], size: number) { + if (files.length <= size) { + return files; + } + + const step = files.length / size; + const picked = new Set(); + + if (files.includes('index.js')) { + picked.add('index.js'); + } + + for (let i = 0; i < size; i++) { + picked.add(files[Math.floor(i * step)]); + } + + return [...picked]; +} + +const esTargetByScriptTargetName: Record = { + ES5: 'es5', + ES2015: 'es2015', + ES2016: 'es2016', + ES2017: 'es2017', + ES2018: 'es2018', + ES2019: 'es2019', + ES2020: 'es2020', + ES2021: 'es2021', + ES2022: 'es2022', +}; + +/** + * ECMAScript version the package compiles to. Anything newer than what this check knows about + * (ES2023+, ESNext) is verified as ES2022, which is the newest baseline it can assert on. + */ +function readCompilerTarget(cwd: string): EsTarget { + const configFileName = ['tsconfig.lib.json', 'tsconfig.json'].find(fileName => existsSync(path.join(cwd, fileName))); + + if (!configFileName) { + return 'es2022'; + } + + const configFilePath = path.join(cwd, configFileName); + const { config, error } = ts.readConfigFile(configFilePath, ts.sys.readFile); + + if (error || !config) { + return 'es2022'; + } + + const parsed = ts.parseJsonConfigFileContent( + config, + // file globbing is not needed, only the resolved `compilerOptions` + { ...ts.sys, readDirectory: () => [] }, + cwd, + undefined, + configFilePath, + ); + const target = parsed.options.target; + + if (target === undefined) { + return 'es2022'; + } + + return esTargetByScriptTargetName[ts.ScriptTarget[target] as string] ?? 'es2022'; } diff --git a/scripts/tasks/tsconfig.lib.json b/scripts/tasks/tsconfig.lib.json index b1af26e356f4c..d1357e408e2c9 100644 --- a/scripts/tasks/tsconfig.lib.json +++ b/scripts/tasks/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/tasks/tsconfig.spec.json b/scripts/tasks/tsconfig.spec.json index a0a0008c224b9..ef1b255ef0863 100644 --- a/scripts/tasks/tsconfig.spec.json +++ b/scripts/tasks/tsconfig.spec.json @@ -1,10 +1,11 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, - "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"] + "include": ["**/*.spec.ts", "**/*.test.ts", "**/*.d.ts"], + "files": ["../../typings/find-free-port/index.d.ts"] } diff --git a/scripts/test-ssr/src/utils/esbuild-plugin.test.ts b/scripts/test-ssr/src/utils/esbuild-plugin.test.ts new file mode 100644 index 0000000000000..9fb6e7b766aa7 --- /dev/null +++ b/scripts/test-ssr/src/utils/esbuild-plugin.test.ts @@ -0,0 +1,281 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import * as os from 'node:os'; +import { join } from 'node:path'; + +import type { OnResolveArgs, OnResolveResult, Plugin, PluginBuild } from 'esbuild'; +import * as ts from 'typescript'; + +import { tsConfigPathsPlugin } from './esbuild-plugin'; + +/** + * Mirrors the repository topology that {@link tsConfigPathsPlugin} has to support: + * + * ``` + * /tsconfig.base.json <- declares `paths`, no `baseUrl` + * /packages/pkg/tsconfig.json <- extends the base config + * /packages/pkg/dist/ssr-tests <- esbuild `cwd`, several levels below the config + * /packages/dep/src/index.ts <- alias target, outside the child config directory + * ``` + */ +function prepareFixture( + overrides: { + baseConfig?: Record; + packageConfig?: Record; + } = {}, +) { + // written under the OS temp directory (never inside a source tree) so an interrupted run cannot leave + // fixture files behind for git/tsconfig to pick up + const root = mkdtempSync(join(os.tmpdir(), 'esbuild-plugin-workspace-')); + const packageRoot = join(root, 'packages', 'pkg'); + const distDirectory = join(packageRoot, 'dist', 'ssr-tests'); + const dependencyRoot = join(root, 'packages', 'dep'); + + mkdirSync(distDirectory, { recursive: true }); + mkdirSync(join(dependencyRoot, 'src'), { recursive: true }); + + writeFileSync(join(dependencyRoot, 'src', 'index.ts'), 'export const dep = 1;', 'utf-8'); + + writeFileSync( + join(root, 'tsconfig.base.json'), + JSON.stringify( + overrides.baseConfig ?? { + compilerOptions: { + target: 'ES2019', + module: 'esnext', + moduleResolution: 'bundler', + paths: { + '@proj/dep': ['./packages/dep/src/index.ts'], + }, + }, + }, + null, + 2, + ), + 'utf-8', + ); + + writeFileSync( + join(packageRoot, 'tsconfig.json'), + JSON.stringify( + overrides.packageConfig ?? { + extends: '../../tsconfig.base.json', + compilerOptions: { noEmit: true }, + include: [], + files: [], + }, + null, + 2, + ), + 'utf-8', + ); + + return { + cleanup: () => rmSync(root, { recursive: true, force: true }), + paths: { root, packageRoot, distDirectory, dependencyRoot }, + }; +} + +/** + * Runs a plugin's `setup` and returns the registered `onResolve` callback. + */ +function getResolver(plugin: Plugin) { + let resolver: ((args: OnResolveArgs) => OnResolveResult | null) | undefined; + + const onResolve: PluginBuild['onResolve'] = (_options, callback) => { + resolver = callback as (args: OnResolveArgs) => OnResolveResult | null; + }; + + plugin.setup({ onResolve } as unknown as PluginBuild); + + if (!resolver) { + throw new Error('plugin did not register an onResolve callback'); + } + + return (importPath: string) => resolver!({ path: importPath } as OnResolveArgs); +} + +describe('tsConfigPathsPlugin', () => { + let fixture: ReturnType | undefined; + + afterEach(() => { + fixture?.cleanup(); + fixture = undefined; + }); + + it('resolves aliases declared in an extended base config that has no baseUrl', () => { + fixture = prepareFixture(); + + const resolve = getResolver(tsConfigPathsPlugin({ cwd: fixture.paths.distDirectory })); + + expect(resolve('@proj/dep')).toEqual({ + path: join(fixture.paths.dependencyRoot, 'src', 'index.ts'), + }); + }); + + it('ignores specifiers that are not path aliases', () => { + fixture = prepareFixture(); + + const resolve = getResolver(tsConfigPathsPlugin({ cwd: fixture.paths.distDirectory })); + + expect(resolve('react')).toBeNull(); + expect(resolve('./relative')).toBeNull(); + }); + + it('resolves aliases against baseUrl when the config still declares one', () => { + fixture = prepareFixture({ + baseConfig: { + compilerOptions: { + paths: { + '@proj/dep': ['./dep/src/index.ts'], + }, + }, + }, + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { baseUrl: '..' }, + include: [], + files: [], + }, + }); + + const resolve = getResolver(tsConfigPathsPlugin({ cwd: fixture.paths.distDirectory })); + + expect(resolve('@proj/dep')).toEqual({ + path: join(fixture.paths.dependencyRoot, 'src', 'index.ts'), + }); + }); + + it('throws when a path alias maps to multiple targets', () => { + fixture = prepareFixture({ + baseConfig: { + compilerOptions: { + paths: { + '@proj/dep': ['./packages/dep/src/index.ts', './packages/dep/src/other.ts'], + }, + }, + }, + }); + + const distDirectory = fixture.paths.distDirectory; + + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/Multiple TS path mappings are not supported/); + }); + + it('throws with formatted diagnostics when the config extends a missing file', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.does-not-exist.json', + include: [], + files: [], + }, + }); + + const distDirectory = fixture.paths.distDirectory; + + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/Failed to parse/); + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow( + /error TS5083: Cannot read file .*tsconfig\.does-not-exist\.json/, + ); + }); + + it('throws with formatted diagnostics when the config declares an unknown compiler option', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { noEmit: true, thisOptionDoesNotExist: true }, + include: [], + files: [], + }, + }); + + const distDirectory = fixture.paths.distDirectory; + + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/Failed to parse/); + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/thisOptionDoesNotExist/); + }); + + it('throws with formatted diagnostics when a compiler option has an invalid value', () => { + fixture = prepareFixture({ + packageConfig: { + extends: '../../tsconfig.base.json', + compilerOptions: { moduleResolution: 'not-a-resolution-mode' }, + include: [], + files: [], + }, + }); + + const distDirectory = fixture.paths.distDirectory; + + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/Failed to parse/); + expect(() => tsConfigPathsPlugin({ cwd: distDirectory })).toThrow(/moduleResolution/); + }); +}); + +/** + * `ts.getParsedCommandLineOfConfigFile(...).errors` is misleadingly named: TypeScript can also place + * `Warning`/`Suggestion` category diagnostics in that array, which must not fail path alias resolution - + * only genuine `Error` category diagnostics may. `typescript`'s own exports are frozen, so the function is + * wrapped in a `jest.fn()` (rather than `jest.spyOn`, which cannot redefine a non-configurable property). + */ +jest.mock('typescript', () => { + const actual = jest.requireActual('typescript'); + return { ...actual, getParsedCommandLineOfConfigFile: jest.fn(actual.getParsedCommandLineOfConfigFile) }; +}); + +describe('tsConfigPathsPlugin - non-error config diagnostics', () => { + let fixture: ReturnType | undefined; + const getParsedCommandLineOfConfigFileMockFn = ts.getParsedCommandLineOfConfigFile as jest.MockedFunction< + typeof ts.getParsedCommandLineOfConfigFile + >; + const actualGetParsedCommandLineOfConfigFile = + jest.requireActual('typescript').getParsedCommandLineOfConfigFile; + + afterEach(() => { + fixture?.cleanup(); + fixture = undefined; + getParsedCommandLineOfConfigFileMockFn.mockImplementation(actualGetParsedCommandLineOfConfigFile); + }); + + function createDiagnostic(category: ts.DiagnosticCategory, messageText: string): ts.Diagnostic { + return { + category, + code: 9999, + file: undefined, + start: undefined, + length: undefined, + messageText, + }; + } + + /** + * Parses the real fixture config, then swaps in a synthetic `errors` array so the diagnostic category + * filtering can be exercised directly, without depending on a real world config that happens to produce + * a non-error diagnostic (none of the fixtures above do). + */ + function stubParsedConfigErrors(errors: ts.Diagnostic[]) { + getParsedCommandLineOfConfigFileMockFn.mockImplementation((...args) => { + const parsedConfig = actualGetParsedCommandLineOfConfigFile(...args); + return parsedConfig && { ...parsedConfig, errors }; + }); + } + + it('does not throw when only a warning/suggestion category diagnostic is reported', () => { + fixture = prepareFixture(); + stubParsedConfigErrors([ + createDiagnostic(ts.DiagnosticCategory.Warning, 'a harmless warning'), + createDiagnostic(ts.DiagnosticCategory.Suggestion, 'a helpful suggestion'), + ]); + + expect(() => tsConfigPathsPlugin({ cwd: fixture!.paths.distDirectory })).not.toThrow(); + }); + + it('still throws for a real error even when a warning/suggestion diagnostic is also present', () => { + fixture = prepareFixture(); + stubParsedConfigErrors([ + createDiagnostic(ts.DiagnosticCategory.Warning, 'a harmless warning'), + createDiagnostic(ts.DiagnosticCategory.Error, 'a real configuration error'), + ]); + + expect(() => tsConfigPathsPlugin({ cwd: fixture!.paths.distDirectory })).toThrow(/a real configuration error/); + }); +}); diff --git a/scripts/test-ssr/src/utils/esbuild-plugin.ts b/scripts/test-ssr/src/utils/esbuild-plugin.ts index 8d51bb93e9db2..6957a736d3799 100644 --- a/scripts/test-ssr/src/utils/esbuild-plugin.ts +++ b/scripts/test-ssr/src/utils/esbuild-plugin.ts @@ -1,7 +1,13 @@ import * as path from 'node:path'; import type { Plugin } from 'esbuild'; -import { loadConfig } from 'tsconfig-paths'; +import * as ts from 'typescript'; + +const diagnosticsHost: ts.FormatDiagnosticsHost = { + getCanonicalFileName: fileName => fileName, + getCurrentDirectory: () => ts.sys.getCurrentDirectory(), + getNewLine: () => ts.sys.newLine, +}; function assertPathAliasesSetup(paths: Record): never | void { for (const [key, mapping] of Object.entries(paths)) { @@ -13,14 +19,78 @@ function assertPathAliasesSetup(paths: Record): never | void { } } -export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { - const tsConfig = loadConfig(options.cwd); +function createConfigParseError(configFilePath: string, diagnostics: readonly ts.Diagnostic[]): Error { + return new Error( + [ + `Failed to parse "${configFilePath}".`, + ``, + ts.formatDiagnostics(diagnostics, diagnosticsHost).trimEnd(), + ``, + `Path aliases cannot be resolved from a config that does not parse - fix the config above.`, + ].join('\n'), + ); +} + +/** + * Resolves `compilerOptions.paths` and the directory they have to be resolved against. + * + * Since TypeScript 6 `baseUrl` is deprecated, so `compilerOptions.paths` entries are resolved relative to + * the config file that declares them. TypeScript exposes that directory on the parsed options as the + * internal `pathsBasePath` and resolves mappings against `baseUrl ?? pathsBasePath` + * (see `getPathsBasePath` in the TypeScript module resolver), which is mirrored here. + * + * Parse diagnostics are fatal: silently continuing would fall back to `node_modules` resolution and + * produce a bundle built against published packages instead of workspace sources. + */ +function loadPathAliases(cwd: string) { + const configFilePath = ts.findConfigFile(cwd, ts.sys.fileExists); - if (tsConfig.resultType === 'failed') { - throw new Error(tsConfig.message); + if (!configFilePath) { + throw new Error(`No tsconfig.json found for "${cwd}"`); } - const pathAliases = tsConfig.paths; + const parsedConfig = ts.getParsedCommandLineOfConfigFile(configFilePath, {}, { + ...ts.sys, + onUnRecoverableConfigFileDiagnostic: diagnostic => { + throw createConfigParseError(configFilePath, [diagnostic]); + }, + } as ts.ParseConfigFileHost); + + if (!parsedConfig) { + throw new Error(`Unable to parse "${configFilePath}"`); + } + + // `parsedConfig.errors` is misleadingly named: TypeScript can also put `Warning`/`Suggestion` category + // diagnostics in it (e.g. deprecated compiler option notices), which must not fail the build. + const configErrors = parsedConfig.errors.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error); + + if (configErrors.length > 0) { + throw createConfigParseError(configFilePath, configErrors); + } + + const options = parsedConfig.options as ts.CompilerOptions & { pathsBasePath?: string }; + const paths = (options.paths ?? {}) as Record; + // eslint-disable-next-line @typescript-eslint/no-deprecated -- `baseUrl` is deprecated but still honoured by TS, and configs in the wild may still set it + const pathAliasesBasePath = options.baseUrl ?? options.pathsBasePath; + + if (Object.keys(paths).length > 0 && !pathAliasesBasePath) { + throw new Error( + [ + `"compilerOptions.paths" declared in "${configFilePath}" cannot be resolved.`, + `TypeScript resolves path mappings against "baseUrl" or, since TypeScript 6, against the directory of the config file that declares them.`, + `Neither is available for this config.`, + ].join('\n'), + ); + } + + return { + paths, + pathAliasesBasePath: pathAliasesBasePath ?? path.dirname(configFilePath), + }; +} + +export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { + const { paths: pathAliases, pathAliasesBasePath } = loadPathAliases(options.cwd); assertPathAliasesSetup(pathAliases); @@ -34,7 +104,7 @@ export function tsConfigPathsPlugin(options: { cwd: string }): Plugin { return null; } - const absoluteImportPath = path.join(tsConfig.absoluteBaseUrl, pathMapping[0]); + const absoluteImportPath = path.resolve(pathAliasesBasePath, pathMapping[0]); return { path: absoluteImportPath }; }); diff --git a/scripts/test-ssr/tsconfig.lib.json b/scripts/test-ssr/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/test-ssr/tsconfig.lib.json +++ b/scripts/test-ssr/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/test-ssr/tsconfig.spec.json b/scripts/test-ssr/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/test-ssr/tsconfig.spec.json +++ b/scripts/test-ssr/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/triage-bot/tsconfig.lib.json b/scripts/triage-bot/tsconfig.lib.json index 040fb0e3c58a1..b3b5bfd36c8ef 100644 --- a/scripts/triage-bot/tsconfig.lib.json +++ b/scripts/triage-bot/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/triage-bot/tsconfig.spec.json b/scripts/triage-bot/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/triage-bot/tsconfig.spec.json +++ b/scripts/triage-bot/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/ts-node/src/register.js b/scripts/ts-node/src/register.js index 3dd2ee26c7bbf..028986dabfb7b 100644 --- a/scripts/ts-node/src/register.js +++ b/scripts/ts-node/src/register.js @@ -8,4 +8,18 @@ tsNode.register({ // swc: true, // remove this once `swc` will start working again transpileOnly: true, + + /** + * `skipProject` makes ts-node fall back to its own defaults, which include the `node10` + * module resolution TypeScript 6 errors on (TS5107). These options mirror the ones the + * `just-task` patch applies for the very same reason - the sources loaded here are executed + * directly by node, so they are emitted as CommonJS, and `bundler` keeps the extensionless + * relative/`index` resolution `node10` provided. + */ + compilerOptions: { + target: 'ES2022', + module: 'CommonJS', + moduleResolution: 'bundler', + esModuleInterop: true, + }, }); diff --git a/scripts/ts-node/tsconfig.lib.json b/scripts/ts-node/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/ts-node/tsconfig.lib.json +++ b/scripts/ts-node/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/ts-node/tsconfig.spec.json b/scripts/ts-node/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/ts-node/tsconfig.spec.json +++ b/scripts/ts-node/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/update-release-notes/tsconfig.lib.json b/scripts/update-release-notes/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/update-release-notes/tsconfig.lib.json +++ b/scripts/update-release-notes/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/update-release-notes/tsconfig.spec.json b/scripts/update-release-notes/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/update-release-notes/tsconfig.spec.json +++ b/scripts/update-release-notes/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/utils/tsconfig.lib.json b/scripts/utils/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/utils/tsconfig.lib.json +++ b/scripts/utils/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/utils/tsconfig.spec.json b/scripts/utils/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/utils/tsconfig.spec.json +++ b/scripts/utils/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/scripts/webpack/src/storybook-webpack.config.js b/scripts/webpack/src/storybook-webpack.config.js index 71199d1402e4b..49c7989c2cce0 100644 --- a/scripts/webpack/src/storybook-webpack.config.js +++ b/scripts/webpack/src/storybook-webpack.config.js @@ -26,9 +26,12 @@ const createStorybookWebpackConfig = config => { { loader: require.resolve('ts-loader'), options: { - transpileOnly: true, - experimentalWatchApi: true, configFile: path.join(process.cwd(), 'tsconfig.json'), + experimentalWatchApi: true, + // Storybook resolves examples and workspace packages to source. TypeScript 6 reports + // rootDir diagnostics for each file before ts-loader's transpile-only compilation. + ignoreDiagnostics: [5011, 6059], + transpileOnly: true, }, }, ], diff --git a/scripts/webpack/src/webpack-resources.js b/scripts/webpack/src/webpack-resources.js index c73e01ef823e1..17b66e017906a 100644 --- a/scripts/webpack/src/webpack-resources.js +++ b/scripts/webpack/src/webpack-resources.js @@ -189,9 +189,10 @@ const api = { * Note that this assumes a base directory (for serving and output) of `dist/demo`. * @param {Partial} customConfig - partial custom webpack config, merged into the full config * @param {string} [outputFolder] - output folder (package-relative) if not `dist/demo` + * @param {{ typescriptConfigFile?: string }} [options] - serve-specific TypeScript options * @returns {WebpackServeConfig} */ - createServeConfig(customConfig, outputFolder = 'dist/demo') { + createServeConfig(customConfig, outputFolder = 'dist/demo', options = {}) { const outputPath = path.join(process.cwd(), outputFolder); return merge( { @@ -228,7 +229,12 @@ const api = { use: { loader: 'ts-loader', options: { + configFile: options.typescriptConfigFile, experimentalWatchApi: true, + // ForkTsChecker validates the repository-wide source program. ts-loader transpiles + // each aliased source file independently, where TypeScript 6 reports misleading + // rootDir diagnostics before loader compiler overrides are applied. + ignoreDiagnostics: [5011, 6059], transpileOnly: true, }, }, @@ -267,7 +273,17 @@ const api = { }, plugins: [ - ...(process.env.TF_BUILD || process.env.SKIP_TYPECHECK ? [] : [new ForkTsCheckerWebpackPlugin()]), + ...(process.env.TF_BUILD || process.env.SKIP_TYPECHECK + ? [] + : [ + new ForkTsCheckerWebpackPlugin({ + typescript: { + configFile: options.typescriptConfigFile, + // Serve configs resolve workspace packages to source, so the checker program spans the repository. + configOverwrite: { compilerOptions: { rootDir: gitRoot } }, + }, + }), + ]), ...(process.env.TF_BUILD ? [] : [new webpack.ProgressPlugin({})]), ], diff --git a/scripts/webpack/tsconfig.lib.json b/scripts/webpack/tsconfig.lib.json index f513783ef5307..a14ac71c25366 100644 --- a/scripts/webpack/tsconfig.lib.json +++ b/scripts/webpack/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "noEmit": false, "lib": ["ES2019"], "outDir": "../../dist/out-tsc", diff --git a/scripts/webpack/tsconfig.spec.json b/scripts/webpack/tsconfig.spec.json index a0a0008c224b9..822155cf0d845 100644 --- a/scripts/webpack/tsconfig.spec.json +++ b/scripts/webpack/tsconfig.spec.json @@ -1,8 +1,8 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "module": "CommonJS", - "moduleResolution": "Node10", + "module": "NodeNext", + "moduleResolution": "NodeNext", "outDir": "dist", "types": ["jest", "node"] }, diff --git a/starter-templates/src/react-components-vite/package.json b/starter-templates/src/react-components-vite/package.json index 924a82ddc8f5b..33ce6e2ee9b57 100644 --- a/starter-templates/src/react-components-vite/package.json +++ b/starter-templates/src/react-components-vite/package.json @@ -18,7 +18,7 @@ "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", "@vitejs/plugin-react": "^4.3.4", - "typescript": "~5.7.3", + "typescript": "~6.0.3", "vite": "^6.0.5" } } diff --git a/starter-templates/src/react-components-vite/tsconfig.app.json b/starter-templates/src/react-components-vite/tsconfig.app.json index 358ca9ba93f08..0778dc31aca9f 100644 --- a/starter-templates/src/react-components-vite/tsconfig.app.json +++ b/starter-templates/src/react-components-vite/tsconfig.app.json @@ -3,12 +3,12 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", + "lib": ["ES2020", "DOM"], + "module": "nodenext", "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", + "moduleResolution": "nodenext", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", diff --git a/starter-templates/src/react-components-vite/tsconfig.node.json b/starter-templates/src/react-components-vite/tsconfig.node.json index db0becc8b033a..afdf3d1284b07 100644 --- a/starter-templates/src/react-components-vite/tsconfig.node.json +++ b/starter-templates/src/react-components-vite/tsconfig.node.json @@ -3,11 +3,11 @@ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", "target": "ES2022", "lib": ["ES2023"], - "module": "ESNext", + "module": "nodenext", "skipLibCheck": true, /* Bundler mode */ - "moduleResolution": "bundler", + "moduleResolution": "nodenext", "allowImportingTsExtensions": true, "isolatedModules": true, "moduleDetection": "force", diff --git a/tools/eslint-rules/rules/__fixtures__/base-hook-no-forbidden-runtime/tsconfig.json b/tools/eslint-rules/rules/__fixtures__/base-hook-no-forbidden-runtime/tsconfig.json index a01e6c8cb8594..63bb12cc8cf37 100644 --- a/tools/eslint-rules/rules/__fixtures__/base-hook-no-forbidden-runtime/tsconfig.json +++ b/tools/eslint-rules/rules/__fixtures__/base-hook-no-forbidden-runtime/tsconfig.json @@ -1,14 +1,13 @@ { "compilerOptions": { "target": "ES2022", - "module": "ESNext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "lib": ["ES2022", "DOM"], "strict": true, "esModuleInterop": true, "skipLibCheck": true, "noEmit": true, - "baseUrl": ".", "paths": { "watched-pkg": ["./stubs/watched-pkg/index.ts"], "watched-pkg/*": ["./stubs/watched-pkg/*"], diff --git a/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.spec.ts b/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.spec.ts index 9421f2e1785da..0aa623a90aec9 100644 --- a/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.spec.ts +++ b/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.spec.ts @@ -1,6 +1,7 @@ import * as path from 'node:path'; import { RuleTester } from '@typescript-eslint/rule-tester'; -import { rule, RULE_NAME } from './base-hook-no-forbidden-runtime'; +import * as ts from 'typescript'; +import { getModuleResolutionCache, rule, RULE_NAME } from './base-hook-no-forbidden-runtime'; const FIXTURE_ROOT = path.join(__dirname, '__fixtures__/base-hook-no-forbidden-runtime'); const TYPED_FILENAME = 'src/test.ts'; @@ -416,3 +417,50 @@ typedRuleTester.run(`${RULE_NAME} (typed)`, rule, { }, ], }); + +// --------------------------------------------------------------------------- +// Module resolution cache +// +// TypeScript 6 does not populate the program level resolution cache for `moduleResolution: bundler`, +// so the rule resolves imports on demand. Because it walks the whole import graph of every linted +// file, those resolutions must be cached - otherwise each import hits the file system again. +// --------------------------------------------------------------------------- +describe(`${RULE_NAME}/moduleResolutionCache`, () => { + const configPath = path.join(FIXTURE_ROOT, 'tsconfig.json'); + + function createProgram() { + const configFile = ts.readConfigFile(configPath, ts.sys.readFile); + const parsed = ts.parseJsonConfigFileContent(configFile.config, ts.sys, FIXTURE_ROOT, undefined, configPath); + + return ts.createProgram({ rootNames: parsed.fileNames, options: parsed.options }); + } + + it(`should reuse a single cache per program`, () => { + const program = createProgram(); + + expect(getModuleResolutionCache(program)).toBe(getModuleResolutionCache(program)); + expect(getModuleResolutionCache(createProgram())).not.toBe(getModuleResolutionCache(program)); + }); + + it(`should not hit the file system for an already resolved specifier`, () => { + const program = createProgram(); + const options = program.getCompilerOptions(); + const cache = getModuleResolutionCache(program); + const containingFile = path.join(FIXTURE_ROOT, 'src/test.ts'); + + const fileExistsSpy = jest.spyOn(ts.sys, 'fileExists'); + + const first = ts.resolveModuleName('watched-pkg', containingFile, options, ts.sys, cache); + const fileSystemCallsAfterFirst = fileExistsSpy.mock.calls.length; + + const second = ts.resolveModuleName('watched-pkg', containingFile, options, ts.sys, cache); + + expect(first.resolvedModule?.resolvedFileName).toEqual(second.resolvedModule?.resolvedFileName); + expect(first.resolvedModule?.resolvedFileName).toContain('stubs/watched-pkg/index.ts'); + expect(fileSystemCallsAfterFirst).toBeGreaterThan(0); + // second resolution is served from the cache + expect(fileExistsSpy.mock.calls.length).toEqual(fileSystemCallsAfterFirst); + + fileExistsSpy.mockRestore(); + }); +}); diff --git a/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.ts b/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.ts index d836462110315..2b22b585a4ffc 100644 --- a/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.ts +++ b/tools/eslint-rules/rules/base-hook-no-forbidden-runtime.ts @@ -630,6 +630,12 @@ function resolveModule( specifier: string, literal: ts.StringLiteralLike, ): string | undefined { + const mode = ( + ts as unknown as { + getModeForUsageLocation?: (file: ts.SourceFile, usage: ts.StringLiteralLike) => ts.ResolutionMode; + } + ).getModeForUsageLocation?.(sourceFile, literal); + const getResolvedModule = ( program as unknown as { getResolvedModule?: ( @@ -639,18 +645,64 @@ function resolveModule( ) => { resolvedModule?: ts.ResolvedModuleFull } | undefined; } ).getResolvedModule; - if (typeof getResolvedModule !== 'function') { - return undefined; + + const cached = + typeof getResolvedModule === 'function' + ? getResolvedModule.call(program, sourceFile, specifier, mode)?.resolvedModule?.resolvedFileName + : undefined; + + if (cached) { + return cached; } - const mode = ( - ts as unknown as { - getModeForUsageLocation?: (file: ts.SourceFile, usage: ts.StringLiteralLike) => ts.ResolutionMode; - } - ).getModeForUsageLocation?.(sourceFile, literal); + /** + * The program level resolution cache is not populated for every resolution mode - notably + * `moduleResolution: bundler` (used across this workspace since the TypeScript 6 migration) + * leaves `getResolvedModule` empty. Fall back to resolving on demand with the very same + * compiler options so the rule keeps following imports. + * + * The rule walks the whole import graph of every linted file, so the same specifier is resolved + * over and over again. Resolving hits the file system, therefore it goes through a + * `ts.ModuleResolutionCache` which is shared by every resolution made against the same program. + */ + const compilerOptions = program.getCompilerOptions(); + + return ts.resolveModuleName( + specifier, + sourceFile.fileName, + compilerOptions, + ts.sys, + getModuleResolutionCache(program, compilerOptions), + undefined, + mode, + ).resolvedModule?.resolvedFileName; +} + +/** + * One module resolution cache per program (which eslint creates once per tsconfig/lint run). + * Keyed weakly so it is released together with the program. + */ +const moduleResolutionCaches = new WeakMap(); + +export function getModuleResolutionCache( + program: ts.Program, + compilerOptions: ts.CompilerOptions = program.getCompilerOptions(), +): ts.ModuleResolutionCache { + const cached = moduleResolutionCaches.get(program); + + if (cached) { + return cached; + } + + const cache = ts.createModuleResolutionCache( + program.getCurrentDirectory(), + fileName => (ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase()), + compilerOptions, + ); + + moduleResolutionCaches.set(program, cache); - const resolutionResult = getResolvedModule.call(program, sourceFile, specifier, mode); - return resolutionResult?.resolvedModule?.resolvedFileName; + return cache; } /** diff --git a/tools/react-integration-tester/README.md b/tools/react-integration-tester/README.md index 85fbba42bd5ee..3b09844aa9958 100644 --- a/tools/react-integration-tester/README.md +++ b/tools/react-integration-tester/README.md @@ -143,17 +143,17 @@ If you omit a property, the builtin default is used (both for the command and th | ---------- | ------------------------------------------------ | ----------------------------------------- | | test | `jest --runInBand` | `jest.config.js` | | type-check | `tsc -p tsconfig.lib.json --pretty` | `tsconfig.lib.json` | -| e2e | `cypress run` (exact args may vary per template) | `cypress.config.ts` (falls back to `.js`) | +| e2e | `cypress run` (exact args may vary per template) | `cypress.config.js` | ### Config file resolution & fallback When deciding whether to expose a script in the prepared project, RIT checks for the existence of the associated config file in the origin project: 1. If your override provides `configPath`, that exact file must exist (otherwise the script is skipped). -2. If no override is provided for `e2e` or `test`, RIT first looks for the TypeScript form (`cypress.config.ts`, `jest.config.ts`) only where applicable; for Jest we default to `jest.config.js`. For Cypress we attempt `cypress.config.ts` and, if missing, fall back to `cypress.config.js` automatically. +2. If no override is provided for `e2e` or `test`, RIT uses the JavaScript form (`cypress.config.js`, `jest.config.js`). Cypress configs have to be JavaScript because Cypress loads them through its own bundled `ts-node`, which is not TypeScript 6 compatible (see `scripts/cypress/src/base.config.js`). 3. For TypeScript (`type-check`), the presence of the referenced tsconfig (default `tsconfig.lib.json` unless you override) controls whether the `type-check` script is added. -This means you can run in projects that still use `.js` Cypress config files without adding an override. +This means you can point at a differently named config file by adding an override. ### Minimal config example (override command only) diff --git a/tools/react-integration-tester/package.json b/tools/react-integration-tester/package.json index 2bad13b5a207e..a74fc5ec979f0 100644 --- a/tools/react-integration-tester/package.json +++ b/tools/react-integration-tester/package.json @@ -9,7 +9,7 @@ "rit": "./bin/rit.js" }, "dependencies": { - "@swc/helpers": "~0.5.1", + "@swc/helpers": "^0.5.23", "ejs": "^3.1.10", "yargs": "^17.7.2" } diff --git a/tools/react-integration-tester/project.json b/tools/react-integration-tester/project.json index bcf60a5578d79..d3ddda85ea877 100644 --- a/tools/react-integration-tester/project.json +++ b/tools/react-integration-tester/project.json @@ -3,6 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "tools/react-integration-tester/src", "projectType": "library", + "implicitDependencies": ["!eslint-plugin"], "release": { "version": { "generatorOptions": { diff --git a/tools/react-integration-tester/src/__tests__/cli.e2e.test.ts b/tools/react-integration-tester/src/__tests__/cli.e2e.test.ts index cd8bdeddc910d..b375cb8471ec6 100644 --- a/tools/react-integration-tester/src/__tests__/cli.e2e.test.ts +++ b/tools/react-integration-tester/src/__tests__/cli.e2e.test.ts @@ -62,7 +62,7 @@ function createProject( writeFileSync(join(rootDir, 'jest.config.js'), 'module.exports = {};'); } if (options?.withCypress) { - writeFileSync(join(rootDir, 'cypress.config.ts'), 'export default {};'); + writeFileSync(join(rootDir, 'cypress.config.js'), 'module.exports = {};'); } } @@ -187,7 +187,6 @@ describe('rit CLI e2e', () => { expect(readJSON(join(ritProjectPath, 'tsconfig.json'))).toMatchInlineSnapshot(` Object { "compilerOptions": Object { - "baseUrl": ".", "isolatedModules": true, "jsx": "react", "lib": Array [ @@ -195,7 +194,7 @@ describe('rit CLI e2e', () => { "DOM", ], "module": "esnext", - "moduleResolution": "node", + "moduleResolution": "bundler", "noEmit": true, "paths": Object { "react": Array [ @@ -350,19 +349,23 @@ describe('rit CLI e2e', () => { `); // and cypress config since origin had jest setup - const cypressConfigPath = join(ritProjectPath, 'cypress.config.ts'); + const cypressConfigPath = join(ritProjectPath, 'cypress.config.js'); expect(existsSync(cypressConfigPath)).toBe(true); const cyContent = readFile(cypressConfigPath); - expect(cyContent).toContain("import baseConfig from '../../../../proj/cypress.config"); + expect(cyContent).toContain("require('../../../../proj/cypress.config"); // check whole template expect(cyContent).toMatchInlineSnapshot(` - "import { join, resolve } from 'node:path'; - import baseConfig from '../../../../proj/cypress.config.ts'; + "// @ts-check + + const { join, resolve } = require('node:path'); + + const baseConfig = require('../../../../proj/cypress.config.js'); // Resolve dependencies from the shared react-version root folder (injected by CLI) const usedNodeModulesPath = join(__dirname, '..', 'node_modules'); + /** @type {import('@fluentui/scripts-cypress').BaseConfig} */ const config = { ...baseConfig }; const specs = [ @@ -380,7 +383,7 @@ describe('rit CLI e2e', () => { 'react-dom': resolve(usedNodeModulesPath, './react-dom'), }; - export default config; + module.exports = config; " `); }); diff --git a/tools/react-integration-tester/src/__tests__/setup.test.ts b/tools/react-integration-tester/src/__tests__/setup.test.ts index 5485a322a4d84..ffb67aad48c94 100644 --- a/tools/react-integration-tester/src/__tests__/setup.test.ts +++ b/tools/react-integration-tester/src/__tests__/setup.test.ts @@ -35,6 +35,24 @@ async function loadModule() { }); } +describe('toPosixPath()', () => { + afterEach(() => { + jest.resetModules(); + }); + + test('converts backslash separators to forward slashes', async () => { + const { toPosixPath } = await loadModule(); + + expect(toPosixPath('..\\..\\packages\\dep\\src\\index.ts')).toBe('../../packages/dep/src/index.ts'); + }); + + test('is a no-op for already POSIX-separated paths', async () => { + const { toPosixPath } = await loadModule(); + + expect(toPosixPath('../../packages/dep/src/index.ts')).toBe('../../packages/dep/src/index.ts'); + }); +}); + describe('setup()', () => { let fs: TempFs; @@ -153,7 +171,7 @@ describe('setup()', () => { JSON.stringify({ include: ['src/index.ts'], compilerOptions: { target: 'ES2020', lib: ['ES2020', 'DOM'] } }), ); writeFileSync(join(fs.tempDir, 'jest.config.js'), 'module.exports = {};'); - writeFileSync(join(fs.tempDir, 'cypress.config.ts'), 'export default {};'); + writeFileSync(join(fs.tempDir, 'cypress.config.js'), 'module.exports = {};'); mockGitRoot(fs.tempDir); @@ -189,7 +207,7 @@ describe('setup()', () => { 'tsconfig.json', 'jest.config.js', '.swcrc', - 'cypress.config.ts', + 'cypress.config.js', 'tsconfig.cy.json', 'package.json', ]; @@ -396,4 +414,71 @@ describe('setup()', () => { // Ensure we attempted installation under react root expect(runCmdMock).toHaveBeenCalled(); }); + + test('normalizes generated tsconfig paths to POSIX separators even when path.relative returns Windows-style backslashes', async () => { + fs = new TempFs('rit-setup-windows-separators'); + + const originPkg = { name: '@scope/origin-proj' }; + writeFileSync(join(fs.tempDir, 'package.json'), JSON.stringify(originPkg)); + writeFileSync( + join(fs.tempDir, 'tsconfig.lib.json'), + JSON.stringify({ + include: ['src/index.ts'], + exclude: ['src/custom-override/**'], + compilerOptions: { target: 'ES2020', lib: ['ES2020', 'DOM'] }, + }), + ); + + mockGitRoot(fs.tempDir); + + // Simulate `path.relative`/`path.join` running on Windows (backslash separated results), while + // every other `node:path` API (used for real filesystem access in this module and `shared.ts`) + // keeps its actual POSIX behaviour so the fixture files on disk are still resolved correctly. + jest.doMock('node:path', () => { + const actual = jest.requireActual('node:path'); + return { + ...actual, + relative: (...args: [string, string]) => + actual + .relative(...args) + .split('/') + .join('\\'), + }; + }); + + let runCmdMock: any; + jest.doMock('../shared', () => { + const actual = jest.requireActual('../shared'); + runCmdMock = jest.fn().mockResolvedValue(undefined); + return { ...actual, runCmd: runCmdMock }; + }); + + const { setup } = await loadModule(); + + const args: Required = { + react: 18, + configPath: '', + run: [], + verbose: false, + cleanup: true, + cwd: fs.tempDir, + prepareOnly: true, + noInstall: false, + installDeps: false, + projectId: '', + force: false, + }; + + const project = await setup(args, logger); + + const generatedTsConfigRaw = readFileSync(join(project.projectPath, 'tsconfig.json'), 'utf-8'); + // The raw file content must be valid JSON (a stray backslash would produce an invalid escape). + const generated = JSON.parse(generatedTsConfigRaw); + expect(generatedTsConfigRaw).not.toContain('\\'); + expect(generated.extends).toBe('../../../../tsconfig.lib.json'); + expect(generated.include).toEqual(['../../../../src/index.ts']); + expect(generated.exclude).toEqual(['../../../../src/custom-override/**']); + + expect(runCmdMock).toHaveBeenCalled(); + }); }); diff --git a/tools/react-integration-tester/src/files/cypress.config.ts.template b/tools/react-integration-tester/src/files/cypress.config.js.template similarity index 77% rename from tools/react-integration-tester/src/files/cypress.config.ts.template rename to tools/react-integration-tester/src/files/cypress.config.js.template index cf2b5523a0862..269212aafdf1b 100644 --- a/tools/react-integration-tester/src/files/cypress.config.ts.template +++ b/tools/react-integration-tester/src/files/cypress.config.js.template @@ -1,9 +1,13 @@ -import { join, resolve } from 'node:path'; -import baseConfig from '<%= relativePathToProjectRoot %>/<%= cypress.pathToProjectConfig %>'; +// @ts-check + +const { join, resolve } = require('node:path'); + +const baseConfig = require('<%= relativePathToProjectRoot %>/<%= cypress.pathToProjectConfig %>'); // Resolve dependencies from the shared react-version root folder (injected by CLI) const usedNodeModulesPath = join(__dirname, '<%= usedNodeModulesDirRelative %>', 'node_modules'); +/** @type {import('@fluentui/scripts-cypress').BaseConfig} */ const config = { ...baseConfig }; const specs = [ @@ -21,4 +25,4 @@ config.component.devServer.webpackConfig.resolve.alias = { 'react-dom': resolve(usedNodeModulesPath, './react-dom'), }; -export default config; +module.exports = config; diff --git a/tools/react-integration-tester/src/files/tsconfig.json.template b/tools/react-integration-tester/src/files/tsconfig.json.template index 5e7cb7e136d64..45432cd5b7a60 100644 --- a/tools/react-integration-tester/src/files/tsconfig.json.template +++ b/tools/react-integration-tester/src/files/tsconfig.json.template @@ -13,7 +13,6 @@ "skipLibCheck": true, "types": [], "typeRoots": ["<%= usedNodeModulesDirRelative %>/node_modules/@types"], - "baseUrl": ".", "paths": { "react/jsx-runtime": ["<%= usedNodeModulesDirRelative %>/node_modules/@types/react/jsx-runtime.d.ts"], "react": ["<%= usedNodeModulesDirRelative %>/node_modules/@types/react/index.d.ts"], diff --git a/tools/react-integration-tester/src/setup.ts b/tools/react-integration-tester/src/setup.ts index 8831f0409981c..fb0fa1b0d4b5a 100644 --- a/tools/react-integration-tester/src/setup.ts +++ b/tools/react-integration-tester/src/setup.ts @@ -21,6 +21,24 @@ function findGitRoot(cwd: string) { return output.toString().trim(); } + +/** + * Normalizes a `path.relative`/`path.join` result to POSIX (`/`) separators. + * + * These relative paths get embedded verbatim into generated `tsconfig.json` `extends`/`include`/ + * `exclude` entries and `require(...)` module specifiers inside the RIT templates (e.g. + * `` `${relativePathToProjectRoot}/tsconfig.json` ``). On Windows `path.relative`/`path.join` return + * backslash-separated strings, so splicing them straight into those templates produces backslashes + * mixed with the templates' hardcoded forward slashes - an invalid escape sequence in the generated + * JSON and a silently mangled string literal in the generated JS. Both `require` and TypeScript's + * module resolution accept POSIX separators on every platform (including Windows), so normalizing + * unconditionally here - rather than branching on the *host* OS running this code - keeps the + * generated output, and its snapshots, identical regardless of which OS produced them. + */ +export function toPosixPath(value: string): string { + return value.split('\\').join('/'); +} + /** * Generate a unique name for running CLI commands * @param prefix @@ -231,7 +249,7 @@ function prepareTsConfigTemplate(options: { const filesDefaults = [ // TODO: this should be actually transformed from origin provided `compilerOptions.types` // ATM this is hardcoded and coupled to fluent repo - join(relative(options.projectPath, options.workspaceRoot), 'typings/static-assets/index.d.ts'), + toPosixPath(join(relative(options.projectPath, options.workspaceRoot), 'typings/static-assets/index.d.ts')), ]; const tsConfig: TsConfig = parseJson(join(options.projectRoot, options.projectTsConfigPath)); @@ -242,16 +260,16 @@ function prepareTsConfigTemplate(options: { // if user config has exclude, always use those const excludePaths = tsConfig?.exclude ?? excludeDefaults; const relativeExcludePaths = excludePaths.map(excludePath => { - return relative(options.projectPath, join(options.projectRoot, excludePath)); + return toPosixPath(relative(options.projectPath, join(options.projectRoot, excludePath))); }); const relativeIncludePaths = tsConfig.include?.map(includePath => { - return relative(options.projectPath, join(options.projectRoot, includePath)); + return toPosixPath(relative(options.projectPath, join(options.projectRoot, includePath))); }); const relativeFilesPaths = (tsConfig.files ?? []) .map(includePath => { - return relative(options.projectPath, join(options.projectRoot, includePath)); + return toPosixPath(relative(options.projectPath, join(options.projectRoot, includePath))); }) .concat(filesDefaults); @@ -260,7 +278,15 @@ function prepareTsConfigTemplate(options: { const target = tsConfig.compilerOptions?.target ?? 'ES2019'; const lib = tsConfig.compilerOptions?.lib ?? ['ES2019', 'DOM']; - const moduleResolution = tsConfig.compilerOptions?.moduleResolution ?? 'node'; + /** + * NOTE: `bundler` mirrors the workspace wide default (`tsconfig.base.json`). It has to be a + * modern resolution mode - TypeScript 6 rejects the previous `node`/node10 default with TS5107. + * + * The generated config declares no `baseUrl` for the very same reason (TS5101): TypeScript 6 + * resolves `paths` relative to the config file which declares them, which is exactly what + * `baseUrl: '.'` used to express here. + */ + const moduleResolution = tsConfig.compilerOptions?.moduleResolution ?? 'bundler'; return { pathToProjectConfig: options.projectTsConfigPath, @@ -428,10 +454,13 @@ export async function setup( react, configPath: options.configPath, tmpl: { - relativePathToProjectRoot: relative(projectPath, projectRoot), - relativePathToWorkspaceRoot: relative(projectPath, workspaceRoot), + // POSIX-normalized: these are spliced verbatim into generated JSON (`extends`) and JS + // `require(...)` string literals, where a raw Windows backslash is either an invalid JSON + // escape or silently drops characters in a JS string literal. + relativePathToProjectRoot: toPosixPath(relative(projectPath, projectRoot)), + relativePathToWorkspaceRoot: toPosixPath(relative(projectPath, workspaceRoot)), // path from project to its react root (where shared node_modules live) - usedNodeModulesDirRelative: relative(projectPath, reactRootPath), + usedNodeModulesDirRelative: toPosixPath(relative(projectPath, reactRootPath)), projectName: createdProjectName, react, tsconfig: prepareTsConfigTemplate({ @@ -479,13 +508,13 @@ export async function setup( renderTemplateToFile(join(__dirname, 'files', '.swcrc.template'), metadata.tmpl, join(projectPath, '.swcrc')); } - // 3) Create cypress.config.ts and tsconfig.cy.json from template with EJS (only if origin project has Cypress setup) + // 3) Create cypress.config.js and tsconfig.cy.json from template with EJS (only if origin project has Cypress setup) if (existsSync(join(projectRoot, templatePrepared.configs['e2e']))) { useCommands['e2e'] = templatePrepared.commands['e2e']; renderTemplateToFile( - join(__dirname, 'files', 'cypress.config.ts.template'), + join(__dirname, 'files', 'cypress.config.js.template'), metadata.tmpl, - join(projectPath, 'cypress.config.ts'), + join(projectPath, 'cypress.config.js'), ); renderTemplateToFile( join(__dirname, 'files', 'tsconfig.cy.json.template'), diff --git a/tools/react-integration-tester/src/shared.ts b/tools/react-integration-tester/src/shared.ts index 6b24e90cef83a..5dedbec59e76f 100644 --- a/tools/react-integration-tester/src/shared.ts +++ b/tools/react-integration-tester/src/shared.ts @@ -123,7 +123,7 @@ export function getMergedTemplate( commands: Record; dependencies: Record; }>(builtinPath), - configs: { e2e: 'cypress.config.ts', test: 'jest.config.js', 'type-check': 'tsconfig.lib.json' } as Record< + configs: { e2e: 'cypress.config.js', test: 'jest.config.js', 'type-check': 'tsconfig.lib.json' } as Record< SupportedCommand, string >, diff --git a/tools/react-integration-tester/tsconfig.spec.json b/tools/react-integration-tester/tsconfig.spec.json index 1275f148a18cf..a67620a423219 100644 --- a/tools/react-integration-tester/tsconfig.spec.json +++ b/tools/react-integration-tester/tsconfig.spec.json @@ -2,8 +2,18 @@ "extends": "./tsconfig.json", "compilerOptions": { "outDir": "../../dist/out-tsc", - "module": "commonjs", - "moduleResolution": "node10", + /** + * Jest resolves modules like a bundler (extensionless relative specifiers, `main`/`exports` + * conditions), and `@swc/jest` rewrites `import()` to `require()`. + * + * Paired `NodeNext` - which is what the other node executed configs of this repo use - is + * therefore not applicable here: it errors with TS2835 on the extensionless dynamic imports + * (`import('../cli')` in `src/__tests__/*.test.ts`) and adding the `.js` extension it asks for + * would break those very tests, because jest resolves the request against `cli.ts` sources. + * `bundler` is the modern (non deprecated) resolution which models jest's resolver. + */ + "module": "nodenext", + "moduleResolution": "nodenext", "types": ["jest", "node"] }, "include": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts", "src/**/*.d.ts"] diff --git a/tools/storybook-llms-extractor/package.json b/tools/storybook-llms-extractor/package.json index e8b9c34456db8..9fba7e31e7dd4 100644 --- a/tools/storybook-llms-extractor/package.json +++ b/tools/storybook-llms-extractor/package.json @@ -15,7 +15,7 @@ "lib-commonjs" ], "dependencies": { - "@swc/helpers": "^0.5.1", + "@swc/helpers": "^0.5.23", "playwright": "^1.55.1", "turndown": "^7.2.0", "turndown-plugin-gfm": "^1.0.2", diff --git a/tools/storybook-llms-extractor/project.json b/tools/storybook-llms-extractor/project.json index 685e5dca413dd..e053ae8d25fd4 100644 --- a/tools/storybook-llms-extractor/project.json +++ b/tools/storybook-llms-extractor/project.json @@ -3,6 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "tools/storybook-llms-extractor/src", "projectType": "library", + "implicitDependencies": ["!eslint-plugin"], "tags": ["platform:any", "tools"], "targets": { "build": { diff --git a/tools/storybook-llms-extractor/tsconfig.json b/tools/storybook-llms-extractor/tsconfig.json index 6678d07b5039b..3c745566fa55e 100644 --- a/tools/storybook-llms-extractor/tsconfig.json +++ b/tools/storybook-llms-extractor/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + // `jest.config.ts` is loaded through ts-node, which resolves this config and not `tsconfig.spec.json`. + // TypeScript 6 defaults `types` to `[]`, so Node globals have to be requested explicitly here. + "types": ["node"], "target": "ES2020", "noEmit": true, "isolatedModules": true, diff --git a/tools/visual-regression-assert/package.json b/tools/visual-regression-assert/package.json index 47ef964d4a214..c74564f89f981 100644 --- a/tools/visual-regression-assert/package.json +++ b/tools/visual-regression-assert/package.json @@ -15,7 +15,7 @@ "lib-commonjs" ], "dependencies": { - "@swc/helpers": "^0.5.1", + "@swc/helpers": "^0.5.23", "pixelmatch": "^7.1.0", "pngjs": "^7.0.0", "ejs": "^3.1.10", diff --git a/tools/visual-regression-assert/project.json b/tools/visual-regression-assert/project.json index 9f459306142d2..78dd8193e0a90 100644 --- a/tools/visual-regression-assert/project.json +++ b/tools/visual-regression-assert/project.json @@ -3,6 +3,7 @@ "$schema": "../../node_modules/nx/schemas/project-schema.json", "sourceRoot": "tools/visual-regression-assert/src", "projectType": "library", + "implicitDependencies": ["!eslint-plugin"], "tags": ["platform:any", "tools"], "targets": { "build": { diff --git a/tools/visual-regression-assert/tsconfig.json b/tools/visual-regression-assert/tsconfig.json index 6678d07b5039b..3c745566fa55e 100644 --- a/tools/visual-regression-assert/tsconfig.json +++ b/tools/visual-regression-assert/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + // `jest.config.ts` is loaded through ts-node, which resolves this config and not `tsconfig.spec.json`. + // TypeScript 6 defaults `types` to `[]`, so Node globals have to be requested explicitly here. + "types": ["node"], "target": "ES2020", "noEmit": true, "isolatedModules": true, diff --git a/tools/visual-regression-utilities/package.json b/tools/visual-regression-utilities/package.json index d6c806d3ae801..e56dfd506f7c5 100644 --- a/tools/visual-regression-utilities/package.json +++ b/tools/visual-regression-utilities/package.json @@ -14,7 +14,7 @@ "dependencies": { "@fluentui/react-provider": "^9.22.18", "@fluentui/react-theme": "^9.2.1", - "@swc/helpers": "^0.5.1" + "@swc/helpers": "^0.5.23" }, "peerDependencies": { "@storybook/react": "^9.1.17", diff --git a/tools/visual-regression-utilities/tsconfig.json b/tools/visual-regression-utilities/tsconfig.json index e79f06ab2ee48..7eead7b1b9cf3 100644 --- a/tools/visual-regression-utilities/tsconfig.json +++ b/tools/visual-regression-utilities/tsconfig.json @@ -1,6 +1,9 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { + // `jest.config.ts` is loaded through ts-node, which resolves this config and not `tsconfig.spec.json`. + // TypeScript 6 defaults `types` to `[]`, so Node globals have to be requested explicitly here. + "types": ["node"], "target": "ES2019", "noEmit": true, "isolatedModules": true, diff --git a/tools/workspace-plugin/project.json b/tools/workspace-plugin/project.json index 986b13318eada..2147d0110fcaf 100644 --- a/tools/workspace-plugin/project.json +++ b/tools/workspace-plugin/project.json @@ -42,6 +42,15 @@ "options": { "cwd": "{projectRoot}" } + }, + "test": { + "inputs": [ + "default", + "^production", + "{workspaceRoot}/jest.preset.js", + "{workspaceRoot}/scripts/tasks/src/dts-rollup.ts", + "{workspaceRoot}/scripts/tasks/src/dts-rollup.spec.ts" + ] } } } diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json index 25052e8384499..46c23e0dfeac9 100644 --- a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "moduleResolution": "Node", + "moduleResolution": "nodenext", + "module": "nodenext", "target": "ES2019", "skipLibCheck": true, "pretty": true diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json index 7b3b02933dd52..9f6c505d26a5c 100644 --- a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/proj/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./dist/out-tsc", "declaration": true, "types": [] diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.json index 623d618870145..4ff329a9dd59a 100644 --- a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.json +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "moduleResolution": "Node", + "moduleResolution": "nodenext", + "module": "nodenext", "target": "ES2019", "jsx": "react", "skipLibCheck": true, diff --git a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.lib.json b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.lib.json index e7a7f01e96b00..7190197615281 100644 --- a/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.lib.json +++ b/tools/workspace-plugin/src/executors/build/__fixtures__/executor/libs/react-compiler-proj/tsconfig.lib.json @@ -1,6 +1,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { + "rootDir": "./src", "outDir": "./dist/out-tsc", "declaration": true, "types": [] diff --git a/tools/workspace-plugin/src/executors/build/executor.spec.ts b/tools/workspace-plugin/src/executors/build/executor.spec.ts index e43d815289d8a..eb61b4b247fa2 100644 --- a/tools/workspace-plugin/src/executors/build/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/build/executor.spec.ts @@ -65,6 +65,7 @@ jest.mock('node:fs/promises', () => { jest.mock('../../utils', () => { return { + ...jest.requireActual('../../utils'), measureStart: jest.fn(), measureEnd: jest.fn(), }; diff --git a/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts b/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts index 64c701c30ea57..8e9d1dce65b1c 100644 --- a/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/generate-api/executor.spec.ts @@ -1,4 +1,4 @@ -import { type ExecutorContext, serializeJson } from '@nx/devkit'; +import { type ExecutorContext, logger, serializeJson } from '@nx/devkit'; import { Extractor, type IExtractorInvokeOptions, @@ -169,10 +169,16 @@ describe('GenerateApi Executor', () => { const output = await executor(options, context); - expect(execSyncMock.mock.calls.flat()).toEqual([ - `tsc -p ${paths.projRoot}/tsconfig.lib.json --pretty --emitDeclarationOnly --baseUrl ${paths.projRoot}`, - { stdio: 'inherit' }, - ]); + const [tscCommand, tscOptions] = execSyncMock.mock.calls.flat(); + + // the transient config which turns path aliases off is unique per invocation + expect(tscCommand).toMatch( + new RegExp( + `^tsc -p ${paths.projRoot}/tsconfig\\.__generated-no-path-aliases-generate-api-\\d+-\\d+-[a-f0-9]+-tsconfig\\.lib\\.json --pretty --emitDeclarationOnly$`, + ), + ); + expect(tscOptions).toEqual({ stdio: 'inherit' }); + expect(readdirSync(paths.projRoot).filter(fileName => fileName.startsWith('tsconfig.__generated'))).toEqual([]); const [extractorConfig, extractorArgs] = ExtractorInvokeSpy.mock.calls.flat() as [ ExtractorConfig, @@ -180,7 +186,6 @@ describe('GenerateApi Executor', () => { ]; expect((extractorConfig.overrideTsconfig as TsConfig).compilerOptions).toEqual({ - baseUrl: '.', declarationDir: 'dts', emitDeclarationOnly: true, isolatedModules: false, @@ -493,3 +498,181 @@ describe('GenerateApi Executor – export subpath resolution', () => { expect(output.success).toBe(true); }); }); + +// ───────────────────────────────────────────────────────────────────────────── +// Self contained `.d.ts` rollup guard +// ───────────────────────────────────────────────────────────────────────────── + +describe('GenerateApi Executor – self contained rollup guard', () => { + const selfContainedRollup = `export declare const Foo: number;\nexport { }\n`; + const brokenRollup = `export declare const Foo: import('./types').Foo;\nexport { }\n`; + + let loggerErrorSpy: jest.SpyInstance; + + beforeEach(() => { + loggerErrorSpy = jest.spyOn(logger, 'error').mockImplementation(() => { + /* silence expected error output */ + }); + }); + + afterEach(() => { + cleanup(); + jest.restoreAllMocks(); + }); + + /** + * Creates a package fixture whose primary api-extractor config emits explicit rollup variants, plus an + * optional `./utils` export subpath entry. + */ + function prepareRollupFixture(config: { + dtsRollup: Record; + namedExport?: string; + rollupContents: Record; + }) { + const { paths, context } = prepareFixture('valid', {}); + const { projRoot } = paths; + + const exports: Record = { + '.': { types: './dist/index.d.ts', import: './lib/index.js' }, + }; + if (config.namedExport) { + exports[`./${config.namedExport}`] = { + types: `./dist/${config.namedExport}/index.d.ts`, + import: `./lib/${config.namedExport}/index.js`, + }; + } + + writeFileSync( + join(projRoot, 'package.json'), + serializeJson({ name: '@proj/proj', types: 'dist/index.d.ts', exports }), + 'utf-8', + ); + + writeFileSync( + join(projRoot, 'config', 'api-extractor.json'), + serializeJson({ + mainEntryPointFilePath: '../dts/src/index.d.ts', + apiReport: { enabled: false }, + docModel: { enabled: false }, + dtsRollup: { enabled: true, ...config.dtsRollup }, + tsdocMetadata: { enabled: false }, + }), + 'utf-8', + ); + + execSyncMock.mockImplementation(() => { + mkdirSync(join(projRoot, 'dts', 'src'), { recursive: true }); + writeFileSync(join(projRoot, 'dts', 'src', 'index.d.ts'), 'export const root: 1;', 'utf-8'); + if (config.namedExport) { + mkdirSync(join(projRoot, 'dts', 'src', config.namedExport), { recursive: true }); + writeFileSync(join(projRoot, 'dts', 'src', config.namedExport, 'index.d.ts'), 'export const util: 1;', 'utf-8'); + } + }); + + // api-extractor is mocked, so emit the rollups it would have written + jest.spyOn(Extractor, 'invoke').mockImplementation(() => { + for (const [relativePath, contents] of Object.entries(config.rollupContents)) { + const filePath = join(projRoot, relativePath); + mkdirSync(join(filePath, '..'), { recursive: true }); + writeFileSync(filePath, contents, 'utf-8'); + } + return { succeeded: true } as ExtractorResult; + }); + + return { paths, context }; + } + + it('fails when the untrimmed rollup imports an unpublished relative module', async () => { + const { context } = prepareRollupFixture({ + dtsRollup: { untrimmedFilePath: '/dist/index.d.ts' }, + rollupContents: { 'dist/index.d.ts': brokenRollup }, + }); + + const output = await executor(options, context); + + expect(output.success).toBe(false); + expect(loggerErrorSpy.mock.calls.flat().join('\n')).toEqual( + expect.stringContaining('api-extractor | BROKEN TYPE DECLARATION ROLLUP'), + ); + expect(loggerErrorSpy.mock.calls.flat().join('\n')).toEqual(expect.stringContaining('- ./types')); + }); + + it('fails when a trimmed rollup variant imports an unpublished relative module', async () => { + const { context } = prepareRollupFixture({ + dtsRollup: { + untrimmedFilePath: '/dist/index.d.ts', + publicTrimmedFilePath: '/dist/index.public.d.ts', + }, + rollupContents: { + 'dist/index.d.ts': selfContainedRollup, + 'dist/index.public.d.ts': brokenRollup, + }, + }); + + const output = await executor(options, context); + + expect(output.success).toBe(false); + expect(loggerErrorSpy.mock.calls.flat().join('\n')).toEqual( + expect.stringContaining(join('dist', 'index.public.d.ts') + ' imports modules that are not published'), + ); + }); + + it('fails when an export subpath rollup imports an unpublished relative module', async () => { + const { context } = prepareRollupFixture({ + dtsRollup: { untrimmedFilePath: '/dist/index.d.ts' }, + namedExport: 'utils', + rollupContents: { + 'dist/index.d.ts': selfContainedRollup, + 'dist/utils/index.d.ts': brokenRollup, + }, + }); + + const output = await executor({ ...options, exportSubpaths: true }, context); + + expect(output.success).toBe(false); + expect(loggerErrorSpy.mock.calls.flat().join('\n')).toEqual( + expect.stringContaining(join('dist', 'utils', 'index.d.ts') + ' imports modules that are not published'), + ); + }); + + it('succeeds when every generated rollup variant is self contained', async () => { + const { context } = prepareRollupFixture({ + dtsRollup: { + untrimmedFilePath: '/dist/index.d.ts', + publicTrimmedFilePath: '/dist/index.public.d.ts', + betaTrimmedFilePath: '/dist/index.beta.d.ts', + }, + namedExport: 'utils', + rollupContents: { + 'dist/index.d.ts': selfContainedRollup, + 'dist/index.public.d.ts': selfContainedRollup, + 'dist/index.beta.d.ts': selfContainedRollup, + 'dist/utils/index.d.ts': selfContainedRollup, + }, + }); + + const output = await executor({ ...options, exportSubpaths: true }, context); + + expect(loggerErrorSpy).not.toHaveBeenCalled(); + expect(output.success).toBe(true); + }); + + it('surfaces api-extractor diagnostics and skips the rollup guard when extraction fails', async () => { + const { context } = prepareRollupFixture({ + dtsRollup: { untrimmedFilePath: '/dist/index.d.ts' }, + rollupContents: { 'dist/index.d.ts': brokenRollup }, + }); + + jest.spyOn(Extractor, 'invoke').mockImplementation(() => { + return { succeeded: false, errorCount: 2, warningCount: 1 } as ExtractorResult; + }); + + const output = await executor(options, context); + + expect(output.success).toBe(false); + expect(loggerErrorSpy).toHaveBeenCalledWith('API Extractor completed with 2 errors and 1 warnings'); + expect(loggerErrorSpy.mock.calls.flat().join('\n')).not.toEqual( + expect.stringContaining('BROKEN TYPE DECLARATION ROLLUP'), + ); + }); +}); diff --git a/tools/workspace-plugin/src/executors/generate-api/executor.ts b/tools/workspace-plugin/src/executors/generate-api/executor.ts index bf0961e8f62cd..596d4606cd768 100644 --- a/tools/workspace-plugin/src/executors/generate-api/executor.ts +++ b/tools/workspace-plugin/src/executors/generate-api/executor.ts @@ -6,7 +6,8 @@ import { Extractor, ExtractorConfig, type IConfigFile } from '@microsoft/api-ext import type { GenerateApiExecutorSchema } from './schema'; import type { PackageJson, TsConfig } from '../../types'; -import { measureEnd, measureStart } from '../../utils'; +import { createTsConfigWithoutPathAliases, measureEnd, measureStart } from '../../utils'; +import { assertSelfContainedDtsRollups } from './lib/dts-rollup'; import { isCI, verboseLog } from './lib/shared'; import { getExportSubpathConfigs } from './lib/utils'; @@ -33,8 +34,14 @@ async function runGenerateApi(options: NormalizedOptions, context: ExecutorConte return false; } + /** + * Shared across every `Extractor.invoke` of this run so that a rollup emitted by more than one entry + * point config is only parsed once. + */ + const scannedRollupPaths = new Set(); + // Run primary api-extractor config - if (!apiExtractor({ configPath: options.config }, options, context)) { + if (!apiExtractor({ configPath: options.config }, options, context, scannedRollupPaths)) { return false; } @@ -43,7 +50,7 @@ async function runGenerateApi(options: NormalizedOptions, context: ExecutorConte const subpathConfigs = getExportSubpathConfigs(options); for (const configObject of subpathConfigs) { verboseLog(`Running api-extractor for export subpath entry: ${configObject.mainEntryPointFilePath}`); - if (!apiExtractor({ configObject }, options, context)) { + if (!apiExtractor({ configObject }, options, context, scannedRollupPaths)) { return false; } } @@ -96,14 +103,9 @@ function normalizeOptions(schema: GenerateApiExecutorSchema, context: ExecutorCo } function generateTypeDeclarations(options: NormalizedOptions) { - const cmd = [ - 'tsc', - `-p ${options.tsConfigPathForCompilation}`, - '--pretty', - '--emitDeclarationOnly', - // turn off path aliases. - `--baseUrl ${options.projectAbsolutePath}`, - ].join(' '); + // turn off path aliases. + const noPathAliasesConfig = createTsConfigWithoutPathAliases(options.tsConfigPathForCompilation, 'generate-api'); + const cmd = ['tsc', `-p ${noPathAliasesConfig.path}`, '--pretty', '--emitDeclarationOnly'].join(' '); verboseLog(`Emitting '.d.ts' files via: "${cmd}"`); @@ -113,6 +115,8 @@ function generateTypeDeclarations(options: NormalizedOptions) { } catch (err) { logger.error(err); return false; + } finally { + noPathAliasesConfig.cleanup(); } } @@ -120,6 +124,7 @@ function apiExtractor( configSource: { configPath: string } | { configObject: IConfigFile }, options: NormalizedOptions, context: ExecutorContext, + scannedRollupPaths: Set, ) { const { rawConfig, fullPath } = resolveConfigSource(); @@ -141,16 +146,33 @@ function apiExtractor( showDiagnostics: options.diagnostics, }); - if (extractorResult.succeeded) { - verboseLog(`API Extractor completed successfully`); - return true; + if (!extractorResult.succeeded) { + // surface the actionable api-extractor diagnostics first - the rollup guard below reports a different + // class of problem and must not mask them + logger.error( + `API Extractor completed with ${extractorResult.errorCount} errors` + + ` and ${extractorResult.warningCount} warnings`, + ); + return false; } - logger.error( - `API Extractor completed with ${extractorResult.errorCount} errors` + - ` and ${extractorResult.warningCount} warnings`, - ); - return false; + verboseLog(`API Extractor completed successfully`); + + return assertGeneratedRollups(); + + /** + * A `.d.ts` rollup is published as a single self contained file, so api-extractor "succeeding" is not + * enough - every rollup variant it emitted has to be verified to not reference unpublished modules. + */ + function assertGeneratedRollups(): boolean { + try { + assertSelfContainedDtsRollups(extractorConfig, { scannedFilePaths: scannedRollupPaths }); + return true; + } catch (err) { + logger.error(err instanceof Error ? err.message : String(err)); + return false; + } + } /** * Resolves the config source into a raw IConfigFile and the full path used for token resolution. @@ -226,10 +248,6 @@ function getTsConfigForApiExtractor(options: { * */ paths: undefined, - /** - * Turn off path aliases. - */ - baseUrl: '.', }, }; diff --git a/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts b/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts new file mode 100644 index 0000000000000..d2362aecf5cf4 --- /dev/null +++ b/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts @@ -0,0 +1,307 @@ +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import * as os from 'node:os'; +import { join } from 'node:path'; + +import { workspaceRoot } from '@nx/devkit'; + +import { + type DtsRollupConfig, + assertSelfContainedDtsRollups, + findRelativeImportsInDtsRollup, + getGeneratedDtsRollupPaths, +} from './dts-rollup'; + +/** + * ⚠️ SHARED SOURCE — this spec exists byte identical next to both copies of `dts-rollup.ts`. + * See the module doc comment in `dts-rollup.ts` for why the module cannot be extracted into a library. + */ + +describe(`shared source parity`, () => { + const sharedSources = [ + ['tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts', 'scripts/tasks/src/dts-rollup.ts'], + [ + 'tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.spec.ts', + 'scripts/tasks/src/dts-rollup.spec.ts', + ], + ] as const; + + it.each(sharedSources)(`'%s' should stay byte identical with '%s'`, (a, b) => { + expect(readFileSync(join(workspaceRoot, a), 'utf-8')).toEqual(readFileSync(join(workspaceRoot, b), 'utf-8')); + }); +}); + +describe(`findRelativeImportsInDtsRollup`, () => { + it(`should return no violations for a self contained rollup`, () => { + const rollup = [ + `import type { ESLint } from 'eslint';`, + `import { RuleModule } from '@typescript-eslint/utils/ts-eslint';`, + `import * as React_2 from 'react';`, + ``, + `export declare const Overflow: React_2.ForwardRefExoticComponent;`, + ``, + `export { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should report relative static imports`, () => { + const rollup = [ + `import { BreadcrumbProps as BreadcrumbProps_2 } from './Breadcrumb.types';`, + `import type { ButtonProps } from '@fluentui/react-button';`, + `import { FieldState as FieldState_2 } from '..';`, + `import defaultExport from '.';`, + `import '../side-effect';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types', '..', '.', '../side-effect']); + }); + + it(`should report relative re-exports and deduplicate specifiers`, () => { + const rollup = [ + `export { RuleOptions } from './rules/enforce-use-client';`, + `export type { RuleOptions as RuleOptions_2 } from './rules/enforce-use-client';`, + `export * from './rules/enforce-use-client';`, + `export * as rules from './rules';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./rules/enforce-use-client', './rules']); + }); + + it(`should report inline import() types on exported declarations`, () => { + const rollup = [ + `export declare const BreadcrumbProvider: Provider;`, + `export declare function useField(): import("./contexts/FieldContext").FieldState;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types', './contexts/FieldContext']); + }); + + it(`should report inline import() types on declarations that are not exported`, () => { + const rollup = [ + `declare const internalContext: import('./internal/Context').ContextValue;`, + `declare type Internal = {`, + ` nested: Array;`, + `};`, + `export { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./internal/Context', '../shared/types']); + }); + + it(`should report relative specifiers spread over multiple lines`, () => { + const rollup = [ + `import {`, + ` OverflowItemProps,`, + ` OverflowProps`, + `} from './Overflow.types';`, + ``, + `export declare const useOverflow: () => import(`, + ` '../hooks/useOverflowContext'`, + `).OverflowContextValue;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Overflow.types', '../hooks/useOverflowContext']); + }); + + it(`should report 'import x = require()' specifiers`, () => { + const rollup = [`import legacy = require('./legacy');`, `import external = require('lodash');`].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./legacy']); + }); + + it(`should report relative module augmentations`, () => { + const rollup = [ + `declare module './augmented' {`, + ` interface Extra { }`, + `}`, + `declare module '@fluentui/react-theme' {`, + ` interface Theme { }`, + `}`, + `declare module Namespaced { }`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./augmented']); + }); + + it(`should report specifiers that use the Windows path separator`, () => { + const rollup = [ + String.raw`import { Foo } from '.\\Foo.types';`, + String.raw`export declare const bar: import('..\\shared\\types').Bar;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([String.raw`.\Foo.types`, String.raw`..\shared\types`]); + }); + + it(`should not report package specifiers, 'node:' builtins, or URLs`, () => { + const rollup = [ + `import { compiler } from 'markdown-to-jsx';`, + `import type { Options } from '@fluentui/react-utilities';`, + `import prettier from 'prettier/parser-html.js';`, + `import node from 'node:path';`, + `import url from 'https://example.com/module.js';`, + `import protocolRelative from '//example.com/module.js';`, + `export declare const theme: import('@fluentui/react-theme').Theme;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should report absolute filesystem specifiers`, () => { + const rollup = [ + `import posix from '/opt/generated/types';`, + String.raw`import windows from 'C:\\generated\\types';`, + `export declare const alt: import('/opt/generated/alt').Alt;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([ + '/opt/generated/types', + String.raw`C:\generated\types`, + '/opt/generated/alt', + ]); + }); + + it(`should report triple-slash reference path directives`, () => { + const rollup = [ + `/// `, + `/// `, + `/// `, + `export declare const Foo: number;`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual(['./Breadcrumb.types.d.ts', '/opt/generated/types.d.ts']); + }); + + it(`should not report string literal types that look like relative or absolute specifiers`, () => { + const rollup = [ + `export declare const numberFormat: '.2f';`, + `export declare type Separator = '.' | '..' | './';`, + `export declare function format(spec: '.2f' | '.0%'): string;`, + `export declare const from: "from './types'";`, + `export declare const doc: {`, + ` value: '../not-an-import';`, + `};`, + `export declare const rootPath: '/opt/generated/types';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); + + it(`should not report relative specifiers that only appear inside comments`, () => { + const rollup = [ + `/**`, + ` * @example`, + ` * import { Foo } from './types';`, + ` */`, + `export declare const Foo: number;`, + `// export * from '../internal';`, + ].join('\n'); + + expect(findRelativeImportsInDtsRollup(rollup)).toEqual([]); + }); +}); + +describe(`getGeneratedDtsRollupPaths / assertSelfContainedDtsRollups`, () => { + let projectFolder: string; + + const selfContained = `export declare const Foo: number;\nexport { }\n`; + const broken = `export declare const Foo: import('./types').Foo;\nexport { }\n`; + + function createConfig(overrides: Partial = {}): DtsRollupConfig { + return { + projectFolder, + rollupEnabled: true, + untrimmedFilePath: '', + alphaTrimmedFilePath: '', + betaTrimmedFilePath: '', + publicTrimmedFilePath: '', + ...overrides, + }; + } + + function writeRollup(fileName: string, contents: string) { + const filePath = join(projectFolder, fileName); + writeFileSync(filePath, contents, 'utf-8'); + return filePath; + } + + beforeEach(() => { + // written under the OS temp directory (never inside a source tree) so an interrupted run cannot leave + // fixture files behind for git/tsconfig to pick up + projectFolder = mkdtempSync(join(os.tmpdir(), 'dts-rollup-')); + }); + + afterEach(() => { + rmSync(projectFolder, { recursive: true, force: true }); + }); + + it(`should return no paths when the rollup is disabled`, () => { + const untrimmedFilePath = writeRollup('index.d.ts', broken); + const config = createConfig({ rollupEnabled: false, untrimmedFilePath }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should skip configured rollups that were not emitted`, () => { + const config = createConfig({ untrimmedFilePath: join(projectFolder, 'missing.d.ts') }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should collect every enabled rollup variant that exists`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', selfContained), + publicTrimmedFilePath: writeRollup('index.public.d.ts', selfContained), + betaTrimmedFilePath: writeRollup('index.beta.d.ts', selfContained), + alphaTrimmedFilePath: join(projectFolder, 'index.alpha.d.ts'), + }); + + expect(getGeneratedDtsRollupPaths(config)).toEqual([ + join(projectFolder, 'index.d.ts'), + join(projectFolder, 'index.public.d.ts'), + join(projectFolder, 'index.beta.d.ts'), + ]); + expect(() => assertSelfContainedDtsRollups(config)).not.toThrow(); + }); + + it(`should throw for a trimmed rollup even when the untrimmed rollup is self contained`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', selfContained), + publicTrimmedFilePath: writeRollup('index.public.d.ts', broken), + }); + + expect(() => assertSelfContainedDtsRollups(config)).toThrow(/index\.public\.d\.ts imports modules/); + expect(() => assertSelfContainedDtsRollups(config)).toThrow(/- \.\/types/); + }); + + it(`should report every violating rollup variant in a single error`, () => { + const config = createConfig({ + untrimmedFilePath: writeRollup('index.d.ts', broken), + betaTrimmedFilePath: writeRollup('index.beta.d.ts', broken), + }); + + let message = ''; + try { + assertSelfContainedDtsRollups(config); + } catch (err) { + message = (err as Error).message; + } + + expect(message).toContain('index.d.ts imports modules'); + expect(message).toContain('index.beta.d.ts imports modules'); + }); + + it(`should not scan the same rollup twice within one run`, () => { + const untrimmedFilePath = writeRollup('index.d.ts', broken); + const scannedFilePaths = new Set(); + const config = createConfig({ untrimmedFilePath }); + + expect(() => assertSelfContainedDtsRollups(config, { scannedFilePaths })).toThrow(/BROKEN TYPE DECLARATION ROLLUP/); + expect(scannedFilePaths).toEqual(new Set([untrimmedFilePath])); + + // a second entry point config emitting the same rollup is a no-op + expect(() => assertSelfContainedDtsRollups(config, { scannedFilePaths })).not.toThrow(); + }); +}); diff --git a/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts b/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts new file mode 100644 index 0000000000000..f63521927c847 --- /dev/null +++ b/tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts @@ -0,0 +1,223 @@ +import * as fs from 'node:fs'; +import * as path from 'node:path'; + +import type { ExtractorConfig } from '@microsoft/api-extractor'; +import { workspaceRoot } from '@nx/devkit'; +import * as ts from 'typescript'; + +/** + * ⚠️ SHARED SOURCE — this module exists byte identical in two places: + * + * - `tools/workspace-plugin/src/executors/generate-api/lib/dts-rollup.ts` (Nx `generate-api` executor, v9) + * - `scripts/tasks/src/dts-rollup.ts` (legacy `just-scripts` api-extractor task, v8) + * + * It cannot be extracted into a shared library because `tools/workspace-plugin` must not depend on any + * project within the monorepo - `tools/workspace-plugin/scripts/check-dep-graph.js` fails the build if it + * does. The spec next to each copy asserts that the two never diverge. + */ + +/** + * The subset of {@link ExtractorConfig} that describes the `.d.ts` rollup outputs. + * + * Declared structurally so the guard can be exercised without constructing a full api-extractor config. + */ +export type DtsRollupConfig = Pick< + ExtractorConfig, + | 'projectFolder' + | 'rollupEnabled' + | 'untrimmedFilePath' + | 'alphaTrimmedFilePath' + | 'betaTrimmedFilePath' + | 'publicTrimmedFilePath' +>; + +type DtsRollupFilePathKey = Exclude; + +/** + * Every rollup variant api-extractor can emit, in the order violations are reported. + */ +const rollupFilePathKeys: readonly DtsRollupFilePathKey[] = [ + 'untrimmedFilePath', + 'publicTrimmedFilePath', + 'betaTrimmedFilePath', + 'alphaTrimmedFilePath', +]; + +/** + * Mirrors TypeScript's `isExternalModuleNameRelative`, which also accepts the Windows path separator. + */ +function isRelativeModuleSpecifier(moduleSpecifier: string): boolean { + return moduleSpecifier === '.' || moduleSpecifier === '..' || /^\.\.?[/\\]/.test(moduleSpecifier); +} + +/** + * Detects an absolute filesystem path - as opposed to a package specifier (`@fluentui/react-theme`), a + * `node:` builtin, or a URL (`https://...`) - none of which are portable outside of the machine/environment + * that generated the rollup. + */ +function isAbsoluteFilesystemModuleSpecifier(moduleSpecifier: string): boolean { + // POSIX absolute path, e.g. '/opt/generated/types'. A leading '//' is excluded because it denotes a + // protocol relative URL (`//example.com/...`), not a filesystem path. + if (moduleSpecifier.startsWith('/') && !moduleSpecifier.startsWith('//')) { + return true; + } + + // Windows absolute path with a drive letter, e.g. 'C:\generated\types' or 'C:/generated/types'. A URL + // scheme such as 'https:' never matches because schemes are more than one character before the colon. + return /^[A-Za-z]:[\\/]/.test(moduleSpecifier); +} + +/** + * Finds module specifiers that a `.d.ts` rollup must never contain. + * + * A rollup is published as a single self contained file, so every relative or absolute-filesystem + * specifier within it points to a module that does not exist for consumers. + * + * This happens when TypeScript declaration output references a type through an inline `import('./module')` + * type: API Extractor resolves such references as external packages whenever a modern `moduleResolution` + * (`bundler`, `node16`, `nodenext`) is used, instead of inlining the referenced declaration. It can also + * happen through a triple-slash `/// ` directive, which API Extractor + * does not rewrite either. + * The fix belongs in the source file - annotate the exported value with a type that is imported statically. + * + * The rollup is parsed with the TypeScript compiler instead of scanned with a regular expression so that + * only real module specifiers are reported - a string literal *type* such as `'.'` or `'.2f'` is not a + * module specifier and must not be flagged. + * + * @see https://github.com/microsoft/rushstack/issues/3335 + */ +export function findRelativeImportsInDtsRollup(rollupContents: string): string[] { + const sourceFile = ts.createSourceFile( + 'dts-rollup.d.ts', + rollupContents, + ts.ScriptTarget.Latest, + /* setParentNodes */ false, + ts.ScriptKind.TS, + ); + const moduleSpecifiers = new Set(); + + // `/// ` - collected separately by the parser, not part of the AST + for (const reference of sourceFile.referencedFiles) { + addIfNotSelfContained(reference.fileName); + } + + visit(sourceFile); + + return [...moduleSpecifiers]; + + function visit(node: ts.Node): void { + collectModuleSpecifier(node); + ts.forEachChild(node, visit); + } + + function collectModuleSpecifier(node: ts.Node): void { + // `import ... from './module'`, `export ... from './module'`, `export * from './module'` + if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) { + addIfNotSelfContainedNode(node.moduleSpecifier); + return; + } + + // `import('./module').Foo` - anywhere in a type position, exported or not + if (ts.isImportTypeNode(node)) { + addIfNotSelfContainedNode(ts.isLiteralTypeNode(node.argument) ? node.argument.literal : undefined); + return; + } + + // `import Foo = require('./module')` + if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) { + addIfNotSelfContainedNode(node.moduleReference.expression); + return; + } + + // `declare module './module' { ... }` + if (ts.isModuleDeclaration(node)) { + addIfNotSelfContainedNode(node.name); + } + } + + function addIfNotSelfContainedNode(node: ts.Node | undefined): void { + if (!node || !ts.isStringLiteralLike(node)) { + return; + } + + addIfNotSelfContained(node.text); + } + + function addIfNotSelfContained(moduleSpecifier: string): void { + if (isRelativeModuleSpecifier(moduleSpecifier) || isAbsoluteFilesystemModuleSpecifier(moduleSpecifier)) { + moduleSpecifiers.add(moduleSpecifier); + } + } +} + +/** + * Resolves every `.d.ts` rollup that api-extractor was configured to emit and that exists on disk. + * + * A single config can emit up to four variants (untrimmed + public/beta/alpha trimmed); variants that were + * not configured are normalized to an empty path by api-extractor. + */ +export function getGeneratedDtsRollupPaths(extractorConfig: DtsRollupConfig): string[] { + if (!extractorConfig.rollupEnabled) { + return []; + } + + const rollupPaths = new Set(); + + for (const key of rollupFilePathKeys) { + const filePath = extractorConfig[key]; + + if (filePath && fs.existsSync(filePath)) { + rollupPaths.add(path.normalize(filePath)); + } + } + + return [...rollupPaths]; +} + +/** + * Fails when any generated `.d.ts` rollup imports a module that is not published alongside it. + * + * @param extractorConfig - the config api-extractor was invoked with + * @param options - `scannedFilePaths` is shared across invocations within a single api-extractor run so + * that a rollup emitted by more than one entry point config is parsed only once + */ +export function assertSelfContainedDtsRollups( + extractorConfig: DtsRollupConfig, + options: { scannedFilePaths?: Set } = {}, +): void { + const { scannedFilePaths } = options; + const violations: Array<{ filePath: string; moduleSpecifiers: string[] }> = []; + + for (const filePath of getGeneratedDtsRollupPaths(extractorConfig)) { + if (scannedFilePaths?.has(filePath)) { + continue; + } + scannedFilePaths?.add(filePath); + + const moduleSpecifiers = findRelativeImportsInDtsRollup(fs.readFileSync(filePath, 'utf-8')); + + if (moduleSpecifiers.length > 0) { + violations.push({ filePath, moduleSpecifiers }); + } + } + + if (violations.length === 0) { + return; + } + + throw new Error( + [ + `api-extractor | BROKEN TYPE DECLARATION ROLLUP:`, + ...violations.flatMap(violation => [ + ` ${path.relative(workspaceRoot, violation.filePath)} imports modules that are not published:`, + ...violation.moduleSpecifiers.map(moduleSpecifier => ` - ${moduleSpecifier}`), + ]), + ``, + ` This happens when declaration output references a type through an inline \`import('./module')\` type.`, + ` 🛠 FIX: annotate the affected export in ${path.relative( + workspaceRoot, + extractorConfig.projectFolder, + )} with a statically imported type.`, + ].join('\n'), + ); +} diff --git a/tools/workspace-plugin/src/executors/type-check/executor.spec.ts b/tools/workspace-plugin/src/executors/type-check/executor.spec.ts index b8333b51d799b..22b53067eb4cb 100644 --- a/tools/workspace-plugin/src/executors/type-check/executor.spec.ts +++ b/tools/workspace-plugin/src/executors/type-check/executor.spec.ts @@ -42,6 +42,23 @@ jest.mock('node:util', () => { __asyncExecMock: asyncExecMock, }; }); +jest.mock('../../utils', () => { + return { + ...jest.requireActual('../../utils'), + // the real implementation writes a transient tsconfig next to the project config, which + // does not exist in this unit test - assert on the generated file name instead + createTsConfigWithoutPathAliases: jest.fn((tsConfigPath: string, purpose: string) => { + const { basename, dirname, join } = jest.requireActual('node:path'); + return { + path: join( + dirname(tsConfigPath), + `tsconfig.__generated-no-path-aliases-${purpose}--${basename(tsConfigPath)}`, + ), + cleanup: jest.fn(), + }; + }), + }; +}); jest.mock('@nx/devkit', () => { return { ...jest.requireActual('@nx/devkit'), @@ -77,8 +94,8 @@ describe('TypeCheck Executor', () => { const output = await executor(options, mockContext); expect(promisifyCallMock.mock.calls.flat()).toEqual([ - 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl /root/libs/my-lib', - 'tsc -p /root/libs/my-lib/tsconfig.spec.json --pretty --noEmit --baseUrl /root/libs/my-lib', + 'tsc -p /root/libs/my-lib/tsconfig.__generated-no-path-aliases-type-check--tsconfig.lib.json --pretty --noEmit', + 'tsc -p /root/libs/my-lib/tsconfig.__generated-no-path-aliases-type-check--tsconfig.spec.json --pretty --noEmit', ]); expect(output.success).toBe(true); @@ -95,7 +112,7 @@ describe('TypeCheck Executor', () => { const output = await executor({ ...options, excludeProject: { spec: true, e2e: false } }, mockContext); expect(promisifyCallMock.mock.calls.flat()).toEqual([ - 'tsc -p /root/libs/my-lib/tsconfig.lib.json --pretty --noEmit --baseUrl /root/libs/my-lib', + 'tsc -p /root/libs/my-lib/tsconfig.__generated-no-path-aliases-type-check--tsconfig.lib.json --pretty --noEmit', ]); expect(output.success).toBe(true); diff --git a/tools/workspace-plugin/src/executors/type-check/executor.ts b/tools/workspace-plugin/src/executors/type-check/executor.ts index 33673193983a0..f599019669028 100644 --- a/tools/workspace-plugin/src/executors/type-check/executor.ts +++ b/tools/workspace-plugin/src/executors/type-check/executor.ts @@ -5,7 +5,7 @@ import { promisify } from 'node:util'; import { exec } from 'node:child_process'; import { type TypeCheckExecutorSchema } from './schema'; -import { measureEnd, measureStart } from '../../utils'; +import { createTsConfigWithoutPathAliases, measureEnd, measureStart } from '../../utils'; const asyncExec = promisify(exec); @@ -41,9 +41,13 @@ async function runTypeCheck(options: NormalizedOptions, context: ExecutorContext const tsConfigsRefs = getTsConfigs(baseTsConfig, projectRootAbsolutePath, options.excludeProject); const asyncQueue = []; + const cleanupQueue: Array<() => void> = []; for (const ref of tsConfigsRefs) { - const program = `tsc -p ${ref} --pretty --noEmit --baseUrl ${projectRootAbsolutePath}`; + const noPathAliasesConfig = createTsConfigWithoutPathAliases(ref, 'type-check'); + cleanupQueue.push(noPathAliasesConfig.cleanup); + + const program = `tsc -p ${noPathAliasesConfig.path} --pretty --noEmit`; verboseLog(`Running "${program}"`); @@ -57,6 +61,9 @@ async function runTypeCheck(options: NormalizedOptions, context: ExecutorContext .catch(err => { console.error(err.stdout); return false; + }) + .finally(() => { + cleanupQueue.forEach(cleanup => cleanup()); }); } diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.js__tmpl__ b/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.js__tmpl__ new file mode 100644 index 0000000000000..0196edf605afa --- /dev/null +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.js__tmpl__ @@ -0,0 +1,5 @@ +// @ts-check + +const { baseConfig } = require('@fluentui/scripts-cypress'); + +module.exports = baseConfig; diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ b/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ deleted file mode 100644 index ca52cf041bbf2..0000000000000 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/files/cypress.config.ts__tmpl__ +++ /dev/null @@ -1,3 +0,0 @@ -import { baseConfig } from '@fluentui/scripts-cypress'; - -export default baseConfig; diff --git a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts index 8086ebc0f7c73..e1f00676bb3c5 100644 --- a/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts +++ b/tools/workspace-plugin/src/generators/cypress-component-configuration/index.spec.ts @@ -23,7 +23,7 @@ describe(`cypress-component-configuration`, () => { await generator(tree, { project }); - expect(tree.exists('apps/app-one/cypress.config.ts')).toBe(false); + expect(tree.exists('apps/app-one/cypress.config.js')).toBe(false); }); it(`should setup cypress component testing for existing project`, async () => { @@ -32,10 +32,12 @@ describe(`cypress-component-configuration`, () => { await generator(tree, { project }); - expect(tree.read('packages/one/cypress.config.ts', 'utf-8')).toMatchInlineSnapshot(` - "import { baseConfig } from '@fluentui/scripts-cypress'; + expect(tree.read('packages/one/cypress.config.js', 'utf-8')).toMatchInlineSnapshot(` + "// @ts-check - export default baseConfig; + const { baseConfig } = require('@fluentui/scripts-cypress'); + + module.exports = baseConfig; " `); expect(readJson(tree, 'packages/one/tsconfig.json').references).toEqual( diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts index e5ae740281b08..5b1d218669006 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/index.ts @@ -235,13 +235,17 @@ async function stableRelease(tree: Tree, options: NormalizedSchema & { isSplitPr } return (_tree: Tree) => { - installPackagesTask(tree, true); + if (!options.skipInstall) { + installPackagesTask(tree, true); + } generateChangefileTask(tree, newPackage.npmName, { message: 'feat: release stable', changeType: 'minor' }); generateChangefileTask(tree, suiteNpmProjectName, { message: `feat: add ${newPackage.npmName} to suite`, changeType: 'minor', }); - generateApiMarkdownTask(tree, suiteProjectName); + if (!options.skipGenerateApi) { + generateApiMarkdownTask(tree, suiteProjectName); + } }; async function updateProjectsThatUsedPreviewPackage() { diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/schema.json b/tools/workspace-plugin/src/generators/prepare-initial-release/schema.json index 89cff067882dc..38f9cb34ad2e3 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/schema.json +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/schema.json @@ -37,6 +37,16 @@ } ] } + }, + "skipInstall": { + "type": "boolean", + "description": "Skip installing dependencies after preparing the release", + "default": false + }, + "skipGenerateApi": { + "type": "boolean", + "description": "Skip generating API documentation after preparing a stable release", + "default": false } }, "required": ["project", "phase"] diff --git a/tools/workspace-plugin/src/generators/prepare-initial-release/schema.ts b/tools/workspace-plugin/src/generators/prepare-initial-release/schema.ts index 07b25f9267f63..1e27771b0ce73 100644 --- a/tools/workspace-plugin/src/generators/prepare-initial-release/schema.ts +++ b/tools/workspace-plugin/src/generators/prepare-initial-release/schema.ts @@ -14,4 +14,12 @@ export interface ReleasePackageGeneratorSchema { * Phase of npm release life cycle for fluent v9 core package */ phase: 'preview' | 'stable' | 'compat'; + /** + * Skip installing dependencies after preparing the release + */ + skipInstall?: boolean; + /** + * Skip generating API documentation after preparing a stable release + */ + skipGenerateApi?: boolean; } diff --git a/tools/workspace-plugin/src/generators/react-library/index.ts b/tools/workspace-plugin/src/generators/react-library/index.ts index f70a75d90469d..92752d08aa4fd 100644 --- a/tools/workspace-plugin/src/generators/react-library/index.ts +++ b/tools/workspace-plugin/src/generators/react-library/index.ts @@ -40,6 +40,10 @@ export default async function (tree: Tree, schema: ReactLibraryGeneratorSchema) await formatFiles(tree); + if (schema.skipInstall) { + return; + } + return () => { installPackagesTask( tree, diff --git a/tools/workspace-plugin/src/generators/react-library/schema.json b/tools/workspace-plugin/src/generators/react-library/schema.json index 0576cdcbd353e..c85c77253e72e 100644 --- a/tools/workspace-plugin/src/generators/react-library/schema.json +++ b/tools/workspace-plugin/src/generators/react-library/schema.json @@ -38,6 +38,11 @@ "x-prompt": { "message": "What kind of react-components library do you wanna create?" } + }, + "skipInstall": { + "type": "boolean", + "description": "Skip installing dependencies after generating the library", + "default": false } }, "required": ["name", "owner"] diff --git a/tools/workspace-plugin/src/generators/react-library/schema.ts b/tools/workspace-plugin/src/generators/react-library/schema.ts index b15e11fbb0d5c..cd9de6cd196af 100644 --- a/tools/workspace-plugin/src/generators/react-library/schema.ts +++ b/tools/workspace-plugin/src/generators/react-library/schema.ts @@ -18,4 +18,8 @@ export interface ReactLibraryGeneratorSchema { * v9 library kind either embracing converged patterns(standard) or using griffel only with old framework patterns(compat) */ kind?: 'standard' | 'compat'; + /** + * Skip installing dependencies after generating the library + */ + skipInstall?: boolean; } diff --git a/tools/workspace-plugin/src/generators/tsconfig-base-all/index.spec.ts b/tools/workspace-plugin/src/generators/tsconfig-base-all/index.spec.ts index b9b89e7745285..85a362f73d168 100644 --- a/tools/workspace-plugin/src/generators/tsconfig-base-all/index.spec.ts +++ b/tools/workspace-plugin/src/generators/tsconfig-base-all/index.spec.ts @@ -20,16 +20,16 @@ describe('tsconfig-base-all generator', () => { writeJson(tree, '/tsconfig.base.v8.json', { compilerOptions: { paths: { - '@proj/v8-one': ['packages/v8-one/src/index.ts'], - '@proj/v8-two': ['packages/v8-two/src/index.ts'], + '@proj/v8-one': ['./packages/v8-one/src/index.ts'], + '@proj/v8-two': ['./packages/v8-two/src/index.ts'], }, }, }); writeJson(tree, '/tsconfig.base.json', { compilerOptions: { paths: { - '@proj/one': ['packages/one/src/index.ts'], - '@proj/two': ['packages/two/src/index.ts'], + '@proj/one': ['./packages/one/src/index.ts'], + '@proj/two': ['./packages/two/src/index.ts'], }, }, }); @@ -42,21 +42,21 @@ describe('tsconfig-base-all generator', () => { expect(baseAllJson).toMatchInlineSnapshot(` Object { "compilerOptions": Object { - "baseUrl": ".", "isolatedModules": true, - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "paths": Object { "@proj/one": Array [ - "packages/one/src/index.ts", + "./packages/one/src/index.ts", ], "@proj/two": Array [ - "packages/two/src/index.ts", + "./packages/two/src/index.ts", ], "@proj/v8-one": Array [ - "packages/v8-one/src/index.ts", + "./packages/v8-one/src/index.ts", ], "@proj/v8-two": Array [ - "packages/v8-two/src/index.ts", + "./packages/v8-two/src/index.ts", ], }, "preserveConstEnums": true, @@ -73,13 +73,29 @@ describe('tsconfig-base-all generator', () => { `); }); + it('should not contain a `baseUrl` or legacy Node module resolution, and should keep aliases relative', async () => { + await generator(tree, options); + const { compilerOptions } = readJson(tree, '/tsconfig.base.all.json'); + + expect(compilerOptions.baseUrl).toBeUndefined(); + expect(compilerOptions.moduleResolution).not.toMatch(/^node$/i); + expect(compilerOptions.moduleResolution).not.toMatch(/^node10$/i); + expect(compilerOptions.moduleResolution).not.toMatch(/^classic$/i); + + for (const targets of Object.values(compilerOptions.paths) as string[][]) { + for (const target of targets) { + expect(target.startsWith('./') || target.startsWith('../')).toBe(true); + } + } + }); + describe(`--verify`, () => { it(`should pass if base all config is up to date`, async () => { expect.assertions(1); await generator(tree, {}); updateJson(tree, '/tsconfig.base.json', json => { - json.compilerOptions.paths['@proj/three'] = ['packages/three/src/index.ts']; + json.compilerOptions.paths['@proj/three'] = ['./packages/three/src/index.ts']; return json; }); @@ -93,7 +109,7 @@ describe('tsconfig-base-all generator', () => { await generator(tree, {}); updateJson(tree, '/tsconfig.base.json', json => { - json.compilerOptions.paths['@proj/three'] = ['packages/three/src/index.ts']; + json.compilerOptions.paths['@proj/three'] = ['./packages/three/src/index.ts']; return json; }); diff --git a/tools/workspace-plugin/src/generators/tsconfig-base-all/lib/utils.ts b/tools/workspace-plugin/src/generators/tsconfig-base-all/lib/utils.ts index 9bc9c3e0730a3..591a0e75d53fd 100644 --- a/tools/workspace-plugin/src/generators/tsconfig-base-all/lib/utils.ts +++ b/tools/workspace-plugin/src/generators/tsconfig-base-all/lib/utils.ts @@ -17,7 +17,8 @@ export function createPathAliasesConfig(tree: Tree) { const tsConfigBase = '.'; const mergedTsConfig = { compilerOptions: { - moduleResolution: 'node', + moduleResolution: 'nodenext', + module: 'nodenext', skipLibCheck: true, typeRoots: ['node_modules/@types', './typings'], isolatedModules: true, @@ -25,7 +26,9 @@ export function createPathAliasesConfig(tree: Tree) { sourceMap: true, pretty: true, rootDir: tsConfigBase, - baseUrl: tsConfigBase, + // NOTE: no `baseUrl` - TypeScript 6+ resolves `paths` relative to this config file's + // directory, so every entry in `paths` (merged from the v8/v9 base configs below) must + // already be an explicitly relative path (e.g. prefixed with `./`). paths: { ...baseConfigs.v8.compilerOptions.paths, ...baseConfigs.v9.compilerOptions.paths, diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts index f459806682872..cb50574b274fd 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.spec.ts @@ -366,10 +366,10 @@ describe(`workspace-plugin`, () => { name: '@proj/proj', private: true, } satisfies Partial), - 'proj/library/cypress.config.ts': ` - import { baseConfig } from '@proj/cypress'; + 'proj/library/cypress.config.js': ` + const { baseConfig } = require('@proj/cypress'); - export default baseConfig; + module.exports = baseConfig; `, }); @@ -463,7 +463,7 @@ describe(`workspace-plugin`, () => { } }`, // create the referenced tsconfig so the logic sees it as present - 'proj/library/cypress.config.ts': 'export default {}', + 'proj/library/cypress.config.js': 'module.exports = {}', 'proj/library/tsconfig.baz.json': '{}', // also create a jest.config.js to show that rit.config.js takes precedence 'proj/library/jest-custom.config.js': 'module.exports = {}', diff --git a/tools/workspace-plugin/src/plugins/workspace-plugin.ts b/tools/workspace-plugin/src/plugins/workspace-plugin.ts index 7af58ba2f027e..7a21e44c224fb 100644 --- a/tools/workspace-plugin/src/plugins/workspace-plugin.ts +++ b/tools/workspace-plugin/src/plugins/workspace-plugin.ts @@ -535,7 +535,7 @@ function buildE2eTarget( config: TaskBuilderConfig, ): TargetConfiguration | null { const hasCypress = - existsSync(join(projectRoot, 'cypress.config.ts')) && existsSync(join(projectRoot, 'tsconfig.cy.json')); + existsSync(join(projectRoot, 'cypress.config.js')) && existsSync(join(projectRoot, 'tsconfig.cy.json')); const hasPlaywright = existsSync(join(projectRoot, 'playwright.config.ts')) && (existsSync(join(projectRoot, 'tsconfig.e2e.json')) || @@ -553,7 +553,7 @@ function buildE2eTarget( }, inputs: [ 'default', - '{projectRoot}/cypress.config.ts', + '{projectRoot}/cypress.config.js', '!{projectRoot}/**/?(*.)+cy.[jt]s?(x)?', { externalDependencies: ['cypress', '@cypress/react'] }, ], @@ -862,7 +862,7 @@ function buildReactIntegrationTesterProjectConfiguration( ): { hasTypeCheck: boolean; hasE2E: boolean; hasTest: boolean } { const defaults = { hasTypeCheck: storybookAdjacent || libraryWithStoriesAdj, - hasE2E: existsSync(join(projectRootPath, 'cypress.config.ts')) && !storybookAdjacent, + hasE2E: existsSync(join(projectRootPath, 'cypress.config.js')) && !storybookAdjacent, hasTest: (existsSync(join(projectRootPath, 'jest.config.js')) || existsSync(join(projectRootPath, 'jest.config.ts'))) && !storybookAdjacent, diff --git a/tools/workspace-plugin/src/utils.spec.ts b/tools/workspace-plugin/src/utils.spec.ts index 0c21c33ae216e..7b5780e11d066 100644 --- a/tools/workspace-plugin/src/utils.spec.ts +++ b/tools/workspace-plugin/src/utils.spec.ts @@ -1,6 +1,13 @@ import { logger, Tree } from '@nx/devkit'; import { createTreeWithEmptyWorkspace } from 'nx/src/devkit-testing-exports'; -import { getWorkspaceConfig, getProjectNameWithoutScope, printUserLogs } from './utils'; +import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'; +import { basename, dirname, join } from 'node:path'; +import { + createTsConfigWithoutPathAliases, + getWorkspaceConfig, + getProjectNameWithoutScope, + printUserLogs, +} from './utils'; describe(`utils`, () => { // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -75,4 +82,101 @@ describe(`utils`, () => { expect(loggerErrorSpy).toHaveBeenCalledWith('error log'); }); }); + + describe('#createTsConfigWithoutPathAliases', () => { + let projectRoot: string; + + beforeEach(() => { + projectRoot = mkdtempSync(join(__dirname, '__tmp-ts6-no-path-aliases-')); + writeFileSync(join(projectRoot, 'tsconfig.lib.json'), JSON.stringify({ include: ['src'] }), 'utf-8'); + }); + + afterEach(() => { + rmSync(projectRoot, { recursive: true, force: true }); + }); + + function listGenerated() { + return readdirSync(projectRoot).filter(fileName => fileName.startsWith('tsconfig.__generated')); + } + + it('should create a transient config which extends the original one and nulls path aliases', () => { + const tsConfigPath = join(projectRoot, 'tsconfig.lib.json'); + + const actual = createTsConfigWithoutPathAliases(tsConfigPath, 'type-check'); + + expect(dirname(actual.path)).toEqual(projectRoot); + expect(basename(actual.path)).toMatch( + /^tsconfig\.__generated-no-path-aliases-type-check-\d+-\d+-[a-f0-9]+-tsconfig\.lib\.json$/, + ); + expect(JSON.parse(readFileSync(actual.path, 'utf-8'))).toEqual({ + extends: './tsconfig.lib.json', + compilerOptions: { paths: null }, + }); + + actual.cleanup(); + }); + + it('should remove the transient config on cleanup', () => { + const actual = createTsConfigWithoutPathAliases(join(projectRoot, 'tsconfig.lib.json'), 'generate-api'); + + expect(existsSync(actual.path)).toBe(true); + + actual.cleanup(); + + expect(existsSync(actual.path)).toBe(false); + expect(() => actual.cleanup()).not.toThrow(); + }); + + it("should create a unique file per invocation, so concurrent tsc runs don't race", () => { + const first = createTsConfigWithoutPathAliases(join(projectRoot, 'tsconfig.lib.json'), 'type-check'); + const second = createTsConfigWithoutPathAliases(join(projectRoot, 'tsconfig.lib.json'), 'type-check'); + + expect(first.path).not.toEqual(second.path); + expect(listGenerated()).toHaveLength(2); + + first.cleanup(); + + expect(existsSync(second.path)).toBe(true); + + second.cleanup(); + + expect(listGenerated()).toEqual([]); + }); + + it("should throw if the config to extend doesn't exist", () => { + expect(() => createTsConfigWithoutPathAliases(join(projectRoot, 'tsconfig.nope.json'), 'build')).toThrow( + /Cannot disable TS path aliases .* doesn't exist/, + ); + }); + + it('should register one process listener at most, no matter how many configs are created', () => { + const created: Array<{ path: string; cleanup: () => void }> = []; + const countOwnListeners = () => ({ + exit: process.listeners('exit').filter(listener => listener.name === 'cleanupTransientTsConfigs').length, + sigint: process.listeners('SIGINT').filter(listener => listener.name === 'cleanupTransientTsConfigsOnSignal') + .length, + }); + const before = countOwnListeners(); + + // fresh module instance, so the (module scoped) listener registration happens within this test + jest.isolateModules(() => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const utils: typeof import('./utils') = require('./utils'); + + for (let i = 0; i < 20; i++) { + created.push(utils.createTsConfigWithoutPathAliases(join(projectRoot, 'tsconfig.lib.json'), 'stress')); + } + }); + + const after = countOwnListeners(); + + expect(listGenerated()).toHaveLength(20); + expect(after.exit - before.exit).toEqual(1); + expect(after.sigint - before.sigint).toEqual(1); + + created.forEach(config => config.cleanup()); + + expect(listGenerated()).toEqual([]); + }); + }); }); diff --git a/tools/workspace-plugin/src/utils.ts b/tools/workspace-plugin/src/utils.ts index bc222e35c3a6a..138e3325fb305 100644 --- a/tools/workspace-plugin/src/utils.ts +++ b/tools/workspace-plugin/src/utils.ts @@ -1,3 +1,6 @@ +import * as crypto from 'node:crypto'; +import * as fs from 'node:fs'; +import * as path from 'node:path'; import { performance } from 'node:perf_hooks'; import yargsParser from 'yargs-parser'; import type * as Enquirer from 'enquirer'; @@ -161,7 +164,7 @@ export function getProjectPaths(projectConfig: ProjectConfiguration) { rootPackageJson: joinPathFragments(projectConfig.root, 'src', 'unstable', 'package.json__tmpl__'), }, conformanceSetup: joinPathFragments(projectConfig.root, 'src', 'testing', 'isConformant.ts'), - cypressConfig: joinPathFragments(projectConfig.root, 'cypress.config.ts'), + cypressConfig: joinPathFragments(projectConfig.root, 'cypress.config.js'), babelConfig: joinPathFragments(projectConfig.root, '.babelrc.json'), jestConfig: joinPathFragments(projectConfig.root, 'jest.config.js'), jestSetupFile: joinPathFragments(projectConfig.root, 'config', 'tests.js'), @@ -290,3 +293,93 @@ export function measureEnd(key: string) { logger.verbose(`Execution Timings: ${key} (${(measure.duration / 1000).toFixed(2)} s)`); } + +// ===================================== +// TS path aliases opt-out for tsc runs +// ===================================== + +/** + * All transient configs created by this module which have not been cleaned up yet. + * + * Registering them in one place keeps the number of process listeners constant - one listener per + * module - no matter how many `tsc` invocations an executor performs. + * + * NOTE: behaviourally aligned with `scripts/tasks/src/utils.ts#createTsConfigWithoutPathAliases`. + * The duplication is intentional - `tools/workspace-plugin` must not depend on the `just` based + * v8 build tooling. + */ +const pendingTransientTsConfigs = new Set(); +let transientTsConfigsCounter = 0; +let processListenersRegistered = false; + +function removeTransientTsConfig(generatedPath: string) { + pendingTransientTsConfigs.delete(generatedPath); + fs.rmSync(generatedPath, { force: true }); +} + +function cleanupTransientTsConfigs() { + for (const generatedPath of [...pendingTransientTsConfigs]) { + removeTransientTsConfig(generatedPath); + } +} + +function registerProcessListeners() { + if (processListenersRegistered) { + return; + } + + processListenersRegistered = true; + + process.on('exit', cleanupTransientTsConfigs); + + // node does not run `exit` listeners when a process is terminated by a signal, + // so clean up explicitly and re-raise to keep the default termination semantics + for (const signal of ['SIGINT', 'SIGTERM'] as const) { + process.once(signal, cleanupTransientTsConfigsOnSignal); + } +} + +function cleanupTransientTsConfigsOnSignal(signal: NodeJS.Signals) { + cleanupTransientTsConfigs(); + process.kill(process.pid, signal); +} + +/** + * Creates a transient tsconfig, next to `tsConfigPath`, which turns TS path aliases off + * (`"paths": null`) for a single `tsc` invocation and returns its path. + * + * TypeScript 6 deprecates `baseUrl`, which used to be (ab)used as `tsc --baseUrl ` + * to make the workspace root relative `paths` entries unresolvable. TypeScript 6 resolves + * `paths` relative to the config file that declares them, so nulling `paths` is now the only + * supported way to opt a compilation out of path aliases - and it cannot be expressed via CLI + * flags, only via a config file. + * + * NOTES: + * - the generated config lives next to the original one, so every relative path + * (`extends`/`include`/`outDir`/`rootDir`/`references`) keeps resolving identically + * - the file name is unique per process and invocation, so the concurrent `tsc` runs this + * executor spawns can never delete each other's config + */ +export function createTsConfigWithoutPathAliases(tsConfigPath: string, purpose: string) { + if (!fs.existsSync(tsConfigPath)) { + throw new Error(`Cannot disable TS path aliases for "${tsConfigPath}", because the file doesn't exist.`); + } + + const configFileName = path.basename(tsConfigPath); + const uniqueId = `${process.pid}-${transientTsConfigsCounter++}-${crypto.randomBytes(4).toString('hex')}`; + const generatedPath = path.join( + path.dirname(tsConfigPath), + `tsconfig.__generated-no-path-aliases-${purpose}-${uniqueId}-${configFileName}`, + ); + + fs.writeFileSync( + generatedPath, + JSON.stringify({ extends: `./${configFileName}`, compilerOptions: { paths: null } }, null, 2), + 'utf-8', + ); + + pendingTransientTsConfigs.add(generatedPath); + registerProcessListeners(); + + return { path: generatedPath, cleanup: () => removeTransientTsConfig(generatedPath) }; +} diff --git a/tsconfig.base.all.json b/tsconfig.base.all.json index 8d5d5cf60d08e..fe12e0b7996f4 100644 --- a/tsconfig.base.all.json +++ b/tsconfig.base.all.json @@ -1,6 +1,7 @@ { "compilerOptions": { - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "skipLibCheck": true, "typeRoots": ["node_modules/@types", "./typings"], "isolatedModules": true, @@ -8,259 +9,258 @@ "sourceMap": true, "pretty": true, "rootDir": ".", - "baseUrl": ".", "paths": { - "@fluentui/react-portal-compat-context": ["packages/react-components/react-portal-compat-context/src/index.ts"], - "@fluentui/api-docs": ["packages/api-docs/src/index.ts"], - "@fluentui/azure-themes": ["packages/azure-themes/src/index.ts"], - "@fluentui/codemods": ["packages/codemods/src/index.ts"], - "@fluentui/date-time-utilities": ["packages/date-time-utilities/src/index.ts"], - "@fluentui/example-data": ["packages/example-data/src/index.ts"], - "@fluentui/fluent2-theme": ["packages/fluent2-theme/src/index.ts"], - "@fluentui/font-icons-mdl2": ["packages/font-icons-mdl2/src/index.ts"], - "@fluentui/foundation-legacy": ["packages/foundation-legacy/src/index.ts"], - "@fluentui/jest-serializer-merge-styles": ["packages/jest-serializer-merge-styles/src/index.ts"], - "@fluentui/merge-styles": ["packages/merge-styles/src/index.ts"], - "@fluentui/react": ["packages/react/src/index.ts"], - "@fluentui/react/lib/*": ["packages/react/src/*"], - "@fluentui/react-date-time": ["packages/react-date-time/src/index.ts"], - "@fluentui/react-experiments": ["packages/react-experiments/src/index.ts"], - "@fluentui/react-experiments/lib/*": ["packages/react-experiments/src/*"], - "@fluentui/react-file-type-icons": ["packages/react-file-type-icons/src/index.ts"], - "@fluentui/react-focus": ["packages/react-focus/src/index.ts"], - "@fluentui/react-hooks": ["packages/react-hooks/src/index.ts"], - "@fluentui/scheme-utilities": ["packages/scheme-utilities/src/index.ts"], - "@fluentui/set-version": ["packages/set-version/src/index.ts"], - "@fluentui/style-utilities": ["packages/style-utilities/src/index.ts"], - "@fluentui/test-utilities": ["packages/test-utilities/src/index.ts"], - "@fluentui/theme-samples": ["packages/theme-samples/src/index.ts"], - "@fluentui/utilities": ["packages/utilities/src/index.ts"], - "@fluentui/webpack-utilities": ["packages/utilities/src/index.ts"], - "@fluentui/dom-utilities": ["packages/dom-utilities/src/index.ts"], - "@fluentui/theme": ["packages/theme/src/index.ts"], - "@fluentui/react-cards": ["packages/react-cards/src/index.ts"], - "@fluentui/react-charting": ["packages/charts/react-charting/src/index.ts"], - "@fluentui/chart-utilities": ["packages/charts/chart-utilities/src/index.ts"], - "@fluentui/react-window-provider": ["packages/react-window-provider/src/index.ts"], - "@fluentui/react-icons-mdl2": ["packages/react-icons-mdl2/src/index.ts"], - "@fluentui/react-icons-mdl2-branded": ["packages/react-icons-mdl2-branded/src/index.ts"], - "@fluentui/react-icon-provider": ["packages/react-icon-provider/src/index.ts"], - "@fluentui/react-examples": ["packages/react-examples/src/index.ts"], - "@fluentui/react-examples/lib/*": ["packages/react-examples/src/*"], - "@fluentui/public-docsite-setup": ["packages/public-docsite-setup/src/index.ts"], - "@fluentui/react-docsite-components": ["packages/react-docsite-components/src/index.ts"], - "@fluentui/react-docsite-components/lib/index2": ["packages/react-docsite-components/src/index2.ts"], + "@fluentui/react-portal-compat-context": ["./packages/react-components/react-portal-compat-context/src/index.ts"], + "@fluentui/api-docs": ["./packages/api-docs/src/index.ts"], + "@fluentui/azure-themes": ["./packages/azure-themes/src/index.ts"], + "@fluentui/codemods": ["./packages/codemods/src/index.ts"], + "@fluentui/date-time-utilities": ["./packages/date-time-utilities/src/index.ts"], + "@fluentui/example-data": ["./packages/example-data/src/index.ts"], + "@fluentui/fluent2-theme": ["./packages/fluent2-theme/src/index.ts"], + "@fluentui/font-icons-mdl2": ["./packages/font-icons-mdl2/src/index.ts"], + "@fluentui/foundation-legacy": ["./packages/foundation-legacy/src/index.ts"], + "@fluentui/jest-serializer-merge-styles": ["./packages/jest-serializer-merge-styles/src/index.ts"], + "@fluentui/merge-styles": ["./packages/merge-styles/src/index.ts"], + "@fluentui/react": ["./packages/react/src/index.ts"], + "@fluentui/react/lib/*": ["./packages/react/src/*"], + "@fluentui/react-date-time": ["./packages/react-date-time/src/index.ts"], + "@fluentui/react-experiments": ["./packages/react-experiments/src/index.ts"], + "@fluentui/react-experiments/lib/*": ["./packages/react-experiments/src/*"], + "@fluentui/react-file-type-icons": ["./packages/react-file-type-icons/src/index.ts"], + "@fluentui/react-focus": ["./packages/react-focus/src/index.ts"], + "@fluentui/react-hooks": ["./packages/react-hooks/src/index.ts"], + "@fluentui/scheme-utilities": ["./packages/scheme-utilities/src/index.ts"], + "@fluentui/set-version": ["./packages/set-version/src/index.ts"], + "@fluentui/style-utilities": ["./packages/style-utilities/src/index.ts"], + "@fluentui/test-utilities": ["./packages/test-utilities/src/index.ts"], + "@fluentui/theme-samples": ["./packages/theme-samples/src/index.ts"], + "@fluentui/utilities": ["./packages/utilities/src/index.ts"], + "@fluentui/webpack-utilities": ["./packages/utilities/src/index.ts"], + "@fluentui/dom-utilities": ["./packages/dom-utilities/src/index.ts"], + "@fluentui/theme": ["./packages/theme/src/index.ts"], + "@fluentui/react-cards": ["./packages/react-cards/src/index.ts"], + "@fluentui/react-charting": ["./packages/charts/react-charting/src/index.ts"], + "@fluentui/chart-utilities": ["./packages/charts/chart-utilities/src/index.ts"], + "@fluentui/react-window-provider": ["./packages/react-window-provider/src/index.ts"], + "@fluentui/react-icons-mdl2": ["./packages/react-icons-mdl2/src/index.ts"], + "@fluentui/react-icons-mdl2-branded": ["./packages/react-icons-mdl2-branded/src/index.ts"], + "@fluentui/react-icon-provider": ["./packages/react-icon-provider/src/index.ts"], + "@fluentui/react-examples": ["./packages/react-examples/src/index.ts"], + "@fluentui/react-examples/lib/*": ["./packages/react-examples/src/*"], + "@fluentui/public-docsite-setup": ["./packages/public-docsite-setup/src/index.ts"], + "@fluentui/react-docsite-components": ["./packages/react-docsite-components/src/index.ts"], + "@fluentui/react-docsite-components/lib/index2": ["./packages/react-docsite-components/src/index2.ts"], "@fluentui/monaco-editor": [ - "packages/monaco-editor/esm/vs/editor/editor.api.d.ts", - "packages/monaco-editor/src/monacoBundle.ts" + "./packages/monaco-editor/esm/vs/editor/editor.api.d.ts", + "./packages/monaco-editor/src/monacoBundle.ts" ], - "@fluentui/monaco-editor/esm/*": ["packages/monaco-editor/esm/"], - "@fluentui/monaco-editor/lib/*": ["packages/monaco-editor/src/*"], - "@fluentui/react-monaco-editor": ["packages/react-monaco-editor/src/index.ts"], - "@fluentui/storybook": ["packages/storybook/src/index.ts"], - "@fluentui/babel-preset-global-context": ["packages/react-components/babel-preset-global-context/src/index.ts"], + "@fluentui/monaco-editor/esm/*": ["./packages/monaco-editor/esm/"], + "@fluentui/monaco-editor/lib/*": ["./packages/monaco-editor/src/*"], + "@fluentui/react-monaco-editor": ["./packages/react-monaco-editor/src/index.ts"], + "@fluentui/storybook": ["./packages/storybook/src/index.ts"], + "@fluentui/babel-preset-global-context": ["./packages/react-components/babel-preset-global-context/src/index.ts"], "@fluentui/babel-preset-storybook-full-source": [ - "packages/react-components/babel-preset-storybook-full-source/src/index.ts" + "./packages/react-components/babel-preset-storybook-full-source/src/index.ts" ], "@fluentui/component-selector-preview": [ - "packages/react-components/component-selector-preview/library/src/index.ts" + "./packages/react-components/component-selector-preview/library/src/index.ts" ], "@fluentui/component-selector-preview-stories": [ - "packages/react-components/component-selector-preview/stories/src/index.ts" + "./packages/react-components/component-selector-preview/stories/src/index.ts" ], "@fluentui/eslint-plugin-react-components": [ - "packages/react-components/eslint-plugin-react-components/src/index.ts" + "./packages/react-components/eslint-plugin-react-components/src/index.ts" ], - "@fluentui/global-context": ["packages/react-components/global-context/src/index.ts"], - "@fluentui/keyboard-key": ["packages/keyboard-key/src/index.ts"], - "@fluentui/keyboard-keys": ["packages/react-components/keyboard-keys/src/index.ts"], - "@fluentui/priority-overflow": ["packages/react-components/priority-overflow/src/index.ts"], - "@fluentui/react-accordion": ["packages/react-components/react-accordion/library/src/index.ts"], - "@fluentui/react-accordion-stories": ["packages/react-components/react-accordion/stories/src/index.ts"], - "@fluentui/react-aria": ["packages/react-components/react-aria/library/src/index.ts"], - "@fluentui/react-aria-stories": ["packages/react-components/react-aria/stories/src/index.ts"], - "@fluentui/react-avatar": ["packages/react-components/react-avatar/library/src/index.ts"], - "@fluentui/react-avatar-stories": ["packages/react-components/react-avatar/stories/src/index.ts"], - "@fluentui/react-badge": ["packages/react-components/react-badge/library/src/index.ts"], - "@fluentui/react-badge-stories": ["packages/react-components/react-badge/stories/src/index.ts"], - "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/library/src/index.ts"], - "@fluentui/react-breadcrumb-stories": ["packages/react-components/react-breadcrumb/stories/src/index.ts"], - "@fluentui/react-button": ["packages/react-components/react-button/library/src/index.ts"], - "@fluentui/react-button-stories": ["packages/react-components/react-button/stories/src/index.ts"], - "@fluentui/react-calendar-compat": ["packages/react-components/react-calendar-compat/library/src/index.ts"], + "@fluentui/global-context": ["./packages/react-components/global-context/src/index.ts"], + "@fluentui/keyboard-key": ["./packages/keyboard-key/src/index.ts"], + "@fluentui/keyboard-keys": ["./packages/react-components/keyboard-keys/src/index.ts"], + "@fluentui/priority-overflow": ["./packages/react-components/priority-overflow/src/index.ts"], + "@fluentui/react-accordion": ["./packages/react-components/react-accordion/library/src/index.ts"], + "@fluentui/react-accordion-stories": ["./packages/react-components/react-accordion/stories/src/index.ts"], + "@fluentui/react-aria": ["./packages/react-components/react-aria/library/src/index.ts"], + "@fluentui/react-aria-stories": ["./packages/react-components/react-aria/stories/src/index.ts"], + "@fluentui/react-avatar": ["./packages/react-components/react-avatar/library/src/index.ts"], + "@fluentui/react-avatar-stories": ["./packages/react-components/react-avatar/stories/src/index.ts"], + "@fluentui/react-badge": ["./packages/react-components/react-badge/library/src/index.ts"], + "@fluentui/react-badge-stories": ["./packages/react-components/react-badge/stories/src/index.ts"], + "@fluentui/react-breadcrumb": ["./packages/react-components/react-breadcrumb/library/src/index.ts"], + "@fluentui/react-breadcrumb-stories": ["./packages/react-components/react-breadcrumb/stories/src/index.ts"], + "@fluentui/react-button": ["./packages/react-components/react-button/library/src/index.ts"], + "@fluentui/react-button-stories": ["./packages/react-components/react-button/stories/src/index.ts"], + "@fluentui/react-calendar-compat": ["./packages/react-components/react-calendar-compat/library/src/index.ts"], "@fluentui/react-calendar-compat-stories": [ - "packages/react-components/react-calendar-compat/stories/src/index.ts" + "./packages/react-components/react-calendar-compat/stories/src/index.ts" ], - "@fluentui/react-card": ["packages/react-components/react-card/library/src/index.ts"], - "@fluentui/react-card-stories": ["packages/react-components/react-card/stories/src/index.ts"], - "@fluentui/react-carousel": ["packages/react-components/react-carousel/library/src/index.ts"], - "@fluentui/react-carousel-stories": ["packages/react-components/react-carousel/stories/src/index.ts"], - "@fluentui/react-charts": ["packages/charts/react-charts/library/src/index.ts"], - "@fluentui/react-charts-stories": ["packages/charts/react-charts/stories/src/index.ts"], - "@fluentui/react-checkbox": ["packages/react-components/react-checkbox/library/src/index.ts"], - "@fluentui/react-checkbox-stories": ["packages/react-components/react-checkbox/stories/src/index.ts"], - "@fluentui/react-color-picker": ["packages/react-components/react-color-picker/library/src/index.ts"], - "@fluentui/react-color-picker-stories": ["packages/react-components/react-color-picker/stories/src/index.ts"], - "@fluentui/react-colorpicker-compat": ["packages/react-components/react-colorpicker-compat/src/index.ts"], - "@fluentui/react-combobox": ["packages/react-components/react-combobox/library/src/index.ts"], - "@fluentui/react-combobox-stories": ["packages/react-components/react-combobox/stories/src/index.ts"], - "@fluentui/react-components": ["packages/react-components/react-components/src/index.ts"], - "@fluentui/react-components/unstable": ["packages/react-components/react-components/src/unstable/index.ts"], - "@fluentui/react-conformance": ["packages/react-conformance/src/index.ts"], - "@fluentui/react-conformance-griffel": ["packages/react-components/react-conformance-griffel/src/index.ts"], - "@fluentui/react-context-selector": ["packages/react-components/react-context-selector/src/index.ts"], - "@fluentui/react-datepicker-compat": ["packages/react-components/react-datepicker-compat/library/src/index.ts"], + "@fluentui/react-card": ["./packages/react-components/react-card/library/src/index.ts"], + "@fluentui/react-card-stories": ["./packages/react-components/react-card/stories/src/index.ts"], + "@fluentui/react-carousel": ["./packages/react-components/react-carousel/library/src/index.ts"], + "@fluentui/react-carousel-stories": ["./packages/react-components/react-carousel/stories/src/index.ts"], + "@fluentui/react-charts": ["./packages/charts/react-charts/library/src/index.ts"], + "@fluentui/react-charts-stories": ["./packages/charts/react-charts/stories/src/index.ts"], + "@fluentui/react-checkbox": ["./packages/react-components/react-checkbox/library/src/index.ts"], + "@fluentui/react-checkbox-stories": ["./packages/react-components/react-checkbox/stories/src/index.ts"], + "@fluentui/react-color-picker": ["./packages/react-components/react-color-picker/library/src/index.ts"], + "@fluentui/react-color-picker-stories": ["./packages/react-components/react-color-picker/stories/src/index.ts"], + "@fluentui/react-colorpicker-compat": ["./packages/react-components/react-colorpicker-compat/src/index.ts"], + "@fluentui/react-combobox": ["./packages/react-components/react-combobox/library/src/index.ts"], + "@fluentui/react-combobox-stories": ["./packages/react-components/react-combobox/stories/src/index.ts"], + "@fluentui/react-components": ["./packages/react-components/react-components/src/index.ts"], + "@fluentui/react-components/unstable": ["./packages/react-components/react-components/src/unstable/index.ts"], + "@fluentui/react-conformance": ["./packages/react-conformance/src/index.ts"], + "@fluentui/react-conformance-griffel": ["./packages/react-components/react-conformance-griffel/src/index.ts"], + "@fluentui/react-context-selector": ["./packages/react-components/react-context-selector/src/index.ts"], + "@fluentui/react-datepicker-compat": ["./packages/react-components/react-datepicker-compat/library/src/index.ts"], "@fluentui/react-datepicker-compat-stories": [ - "packages/react-components/react-datepicker-compat/stories/src/index.ts" - ], - "@fluentui/react-dialog": ["packages/react-components/react-dialog/library/src/index.ts"], - "@fluentui/react-dialog-stories": ["packages/react-components/react-dialog/stories/src/index.ts"], - "@fluentui/react-divider": ["packages/react-components/react-divider/library/src/index.ts"], - "@fluentui/react-divider-stories": ["packages/react-components/react-divider/stories/src/index.ts"], - "@fluentui/react-drawer": ["packages/react-components/react-drawer/library/src/index.ts"], - "@fluentui/react-drawer-stories": ["packages/react-components/react-drawer/stories/src/index.ts"], - "@fluentui/react-field": ["packages/react-components/react-field/library/src/index.ts"], - "@fluentui/react-field-stories": ["packages/react-components/react-field/stories/src/index.ts"], - "@fluentui/react-focus-management": ["packages/react-focus-management/src/index.ts"], - "@fluentui/react-headless-components-preview/*": [ - "packages/react-components/react-headless-components-preview/library/src/*.ts" + "./packages/react-components/react-datepicker-compat/stories/src/index.ts" ], + "@fluentui/react-dialog": ["./packages/react-components/react-dialog/library/src/index.ts"], + "@fluentui/react-dialog-stories": ["./packages/react-components/react-dialog/stories/src/index.ts"], + "@fluentui/react-divider": ["./packages/react-components/react-divider/library/src/index.ts"], + "@fluentui/react-divider-stories": ["./packages/react-components/react-divider/stories/src/index.ts"], + "@fluentui/react-drawer": ["./packages/react-components/react-drawer/library/src/index.ts"], + "@fluentui/react-drawer-stories": ["./packages/react-components/react-drawer/stories/src/index.ts"], + "@fluentui/react-field": ["./packages/react-components/react-field/library/src/index.ts"], + "@fluentui/react-field-stories": ["./packages/react-components/react-field/stories/src/index.ts"], + "@fluentui/react-focus-management": ["./packages/react-focus-management/src/index.ts"], "@fluentui/react-headless-components-preview-stories": [ - "packages/react-components/react-headless-components-preview/stories/src/index.ts" + "./packages/react-components/react-headless-components-preview/stories/src/index.ts" + ], + "@fluentui/react-headless-components-preview/*": [ + "./packages/react-components/react-headless-components-preview/library/src/*.ts" ], - "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/library/src/index.ts"], - "@fluentui/react-icons-compat-stories": ["packages/react-components/react-icons-compat/stories/src/index.ts"], - "@fluentui/react-image": ["packages/react-components/react-image/library/src/index.ts"], - "@fluentui/react-image-stories": ["packages/react-components/react-image/stories/src/index.ts"], - "@fluentui/react-infolabel": ["packages/react-components/react-infolabel/library/src/index.ts"], - "@fluentui/react-infolabel-stories": ["packages/react-components/react-infolabel/stories/src/index.ts"], - "@fluentui/react-input": ["packages/react-components/react-input/library/src/index.ts"], - "@fluentui/react-input-stories": ["packages/react-components/react-input/stories/src/index.ts"], - "@fluentui/react-integration-tester": ["tools/react-integration-tester/src/index.ts"], - "@fluentui/react-jsx-runtime": ["packages/react-components/react-jsx-runtime/src/index.ts"], + "@fluentui/react-icons-compat": ["./packages/react-components/react-icons-compat/library/src/index.ts"], + "@fluentui/react-icons-compat-stories": ["./packages/react-components/react-icons-compat/stories/src/index.ts"], + "@fluentui/react-image": ["./packages/react-components/react-image/library/src/index.ts"], + "@fluentui/react-image-stories": ["./packages/react-components/react-image/stories/src/index.ts"], + "@fluentui/react-infolabel": ["./packages/react-components/react-infolabel/library/src/index.ts"], + "@fluentui/react-infolabel-stories": ["./packages/react-components/react-infolabel/stories/src/index.ts"], + "@fluentui/react-input": ["./packages/react-components/react-input/library/src/index.ts"], + "@fluentui/react-input-stories": ["./packages/react-components/react-input/stories/src/index.ts"], + "@fluentui/react-integration-tester": ["./tools/react-integration-tester/src/index.ts"], + "@fluentui/react-jsx-runtime": ["./packages/react-components/react-jsx-runtime/src/index.ts"], "@fluentui/react-jsx-runtime/jsx-dev-runtime": [ - "packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" + "./packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" ], - "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], - "@fluentui/react-label": ["packages/react-components/react-label/library/src/index.ts"], - "@fluentui/react-label-stories": ["packages/react-components/react-label/stories/src/index.ts"], - "@fluentui/react-link": ["packages/react-components/react-link/library/src/index.ts"], - "@fluentui/react-link-stories": ["packages/react-components/react-link/stories/src/index.ts"], - "@fluentui/react-list": ["packages/react-components/react-list/library/src/index.ts"], - "@fluentui/react-list-stories": ["packages/react-components/react-list/stories/src/index.ts"], - "@fluentui/react-menu": ["packages/react-components/react-menu/library/src/index.ts"], - "@fluentui/react-menu-grid-preview": ["packages/react-components/react-menu-grid-preview/library/src/index.ts"], + "@fluentui/react-jsx-runtime/jsx-runtime": ["./packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], + "@fluentui/react-label": ["./packages/react-components/react-label/library/src/index.ts"], + "@fluentui/react-label-stories": ["./packages/react-components/react-label/stories/src/index.ts"], + "@fluentui/react-link": ["./packages/react-components/react-link/library/src/index.ts"], + "@fluentui/react-link-stories": ["./packages/react-components/react-link/stories/src/index.ts"], + "@fluentui/react-list": ["./packages/react-components/react-list/library/src/index.ts"], + "@fluentui/react-list-stories": ["./packages/react-components/react-list/stories/src/index.ts"], + "@fluentui/react-menu": ["./packages/react-components/react-menu/library/src/index.ts"], + "@fluentui/react-menu-grid-preview": ["./packages/react-components/react-menu-grid-preview/library/src/index.ts"], "@fluentui/react-menu-grid-preview-stories": [ - "packages/react-components/react-menu-grid-preview/stories/src/index.ts" + "./packages/react-components/react-menu-grid-preview/stories/src/index.ts" ], - "@fluentui/react-menu-stories": ["packages/react-components/react-menu/stories/src/index.ts"], - "@fluentui/react-message-bar": ["packages/react-components/react-message-bar/library/src/index.ts"], - "@fluentui/react-message-bar-stories": ["packages/react-components/react-message-bar/stories/src/index.ts"], - "@fluentui/react-migration-v0-v9": ["packages/react-components/react-migration-v0-v9/library/src/index.ts"], + "@fluentui/react-menu-stories": ["./packages/react-components/react-menu/stories/src/index.ts"], + "@fluentui/react-message-bar": ["./packages/react-components/react-message-bar/library/src/index.ts"], + "@fluentui/react-message-bar-stories": ["./packages/react-components/react-message-bar/stories/src/index.ts"], + "@fluentui/react-migration-v0-v9": ["./packages/react-components/react-migration-v0-v9/library/src/index.ts"], "@fluentui/react-migration-v0-v9-stories": [ - "packages/react-components/react-migration-v0-v9/stories/src/index.ts" + "./packages/react-components/react-migration-v0-v9/stories/src/index.ts" ], - "@fluentui/react-migration-v8-v9": ["packages/react-components/react-migration-v8-v9/library/src/index.ts"], + "@fluentui/react-migration-v8-v9": ["./packages/react-components/react-migration-v8-v9/library/src/index.ts"], "@fluentui/react-migration-v8-v9-stories": [ - "packages/react-components/react-migration-v8-v9/stories/src/index.ts" + "./packages/react-components/react-migration-v8-v9/stories/src/index.ts" ], - "@fluentui/react-motion": ["packages/react-components/react-motion/library/src/index.ts"], + "@fluentui/react-motion": ["./packages/react-components/react-motion/library/src/index.ts"], "@fluentui/react-motion-components-preview": [ - "packages/react-components/react-motion-components-preview/library/src/index.ts" + "./packages/react-components/react-motion-components-preview/library/src/index.ts" ], "@fluentui/react-motion-components-preview-stories": [ - "packages/react-components/react-motion-components-preview/stories/src/index.ts" + "./packages/react-components/react-motion-components-preview/stories/src/index.ts" ], - "@fluentui/react-motion-stories": ["packages/react-components/react-motion/stories/src/index.ts"], - "@fluentui/react-nav": ["packages/react-components/react-nav/library/src/index.ts"], - "@fluentui/react-nav-stories": ["packages/react-components/react-nav/stories/src/index.ts"], - "@fluentui/react-overflow": ["packages/react-components/react-overflow/library/src/index.ts"], - "@fluentui/react-overflow-stories": ["packages/react-components/react-overflow/stories/src/index.ts"], - "@fluentui/react-persona": ["packages/react-components/react-persona/library/src/index.ts"], - "@fluentui/react-persona-stories": ["packages/react-components/react-persona/stories/src/index.ts"], - "@fluentui/react-popover": ["packages/react-components/react-popover/library/src/index.ts"], - "@fluentui/react-popover-stories": ["packages/react-components/react-popover/stories/src/index.ts"], - "@fluentui/react-portal": ["packages/react-components/react-portal/library/src/index.ts"], - "@fluentui/react-portal-compat": ["packages/react-components/react-portal-compat/src/index.ts"], - "@fluentui/react-portal-stories": ["packages/react-components/react-portal/stories/src/index.ts"], - "@fluentui/react-positioning": ["packages/react-components/react-positioning/library/src/index.ts"], - "@fluentui/react-positioning-stories": ["packages/react-components/react-positioning/stories/src/index.ts"], - "@fluentui/react-progress": ["packages/react-components/react-progress/library/src/index.ts"], - "@fluentui/react-progress-stories": ["packages/react-components/react-progress/stories/src/index.ts"], - "@fluentui/react-provider": ["packages/react-components/react-provider/library/src/index.ts"], - "@fluentui/react-provider-stories": ["packages/react-components/react-provider/stories/src/index.ts"], - "@fluentui/react-radio": ["packages/react-components/react-radio/library/src/index.ts"], - "@fluentui/react-radio-stories": ["packages/react-components/react-radio/stories/src/index.ts"], - "@fluentui/react-rating": ["packages/react-components/react-rating/library/src/index.ts"], - "@fluentui/react-rating-stories": ["packages/react-components/react-rating/stories/src/index.ts"], - "@fluentui/react-search": ["packages/react-components/react-search/library/src/index.ts"], - "@fluentui/react-search-stories": ["packages/react-components/react-search/stories/src/index.ts"], - "@fluentui/react-select": ["packages/react-components/react-select/library/src/index.ts"], - "@fluentui/react-select-stories": ["packages/react-components/react-select/stories/src/index.ts"], - "@fluentui/react-shared-contexts": ["packages/react-components/react-shared-contexts/library/src/index.ts"], + "@fluentui/react-motion-stories": ["./packages/react-components/react-motion/stories/src/index.ts"], + "@fluentui/react-nav": ["./packages/react-components/react-nav/library/src/index.ts"], + "@fluentui/react-nav-stories": ["./packages/react-components/react-nav/stories/src/index.ts"], + "@fluentui/react-overflow": ["./packages/react-components/react-overflow/library/src/index.ts"], + "@fluentui/react-overflow-stories": ["./packages/react-components/react-overflow/stories/src/index.ts"], + "@fluentui/react-persona": ["./packages/react-components/react-persona/library/src/index.ts"], + "@fluentui/react-persona-stories": ["./packages/react-components/react-persona/stories/src/index.ts"], + "@fluentui/react-popover": ["./packages/react-components/react-popover/library/src/index.ts"], + "@fluentui/react-popover-stories": ["./packages/react-components/react-popover/stories/src/index.ts"], + "@fluentui/react-portal": ["./packages/react-components/react-portal/library/src/index.ts"], + "@fluentui/react-portal-compat": ["./packages/react-components/react-portal-compat/src/index.ts"], + "@fluentui/react-portal-stories": ["./packages/react-components/react-portal/stories/src/index.ts"], + "@fluentui/react-positioning": ["./packages/react-components/react-positioning/library/src/index.ts"], + "@fluentui/react-positioning-stories": ["./packages/react-components/react-positioning/stories/src/index.ts"], + "@fluentui/react-progress": ["./packages/react-components/react-progress/library/src/index.ts"], + "@fluentui/react-progress-stories": ["./packages/react-components/react-progress/stories/src/index.ts"], + "@fluentui/react-provider": ["./packages/react-components/react-provider/library/src/index.ts"], + "@fluentui/react-provider-stories": ["./packages/react-components/react-provider/stories/src/index.ts"], + "@fluentui/react-radio": ["./packages/react-components/react-radio/library/src/index.ts"], + "@fluentui/react-radio-stories": ["./packages/react-components/react-radio/stories/src/index.ts"], + "@fluentui/react-rating": ["./packages/react-components/react-rating/library/src/index.ts"], + "@fluentui/react-rating-stories": ["./packages/react-components/react-rating/stories/src/index.ts"], + "@fluentui/react-search": ["./packages/react-components/react-search/library/src/index.ts"], + "@fluentui/react-search-stories": ["./packages/react-components/react-search/stories/src/index.ts"], + "@fluentui/react-select": ["./packages/react-components/react-select/library/src/index.ts"], + "@fluentui/react-select-stories": ["./packages/react-components/react-select/stories/src/index.ts"], + "@fluentui/react-shared-contexts": ["./packages/react-components/react-shared-contexts/library/src/index.ts"], "@fluentui/react-shared-contexts-stories": [ - "packages/react-components/react-shared-contexts/stories/src/index.ts" + "./packages/react-components/react-shared-contexts/stories/src/index.ts" ], - "@fluentui/react-skeleton": ["packages/react-components/react-skeleton/library/src/index.ts"], - "@fluentui/react-skeleton-stories": ["packages/react-components/react-skeleton/stories/src/index.ts"], - "@fluentui/react-slider": ["packages/react-components/react-slider/library/src/index.ts"], - "@fluentui/react-slider-stories": ["packages/react-components/react-slider/stories/src/index.ts"], - "@fluentui/react-spinbutton": ["packages/react-components/react-spinbutton/library/src/index.ts"], - "@fluentui/react-spinbutton-stories": ["packages/react-components/react-spinbutton/stories/src/index.ts"], - "@fluentui/react-spinner": ["packages/react-components/react-spinner/library/src/index.ts"], - "@fluentui/react-spinner-stories": ["packages/react-components/react-spinner/stories/src/index.ts"], - "@fluentui/react-storybook-addon": ["packages/react-components/react-storybook-addon/src/index.ts"], + "@fluentui/react-skeleton": ["./packages/react-components/react-skeleton/library/src/index.ts"], + "@fluentui/react-skeleton-stories": ["./packages/react-components/react-skeleton/stories/src/index.ts"], + "@fluentui/react-slider": ["./packages/react-components/react-slider/library/src/index.ts"], + "@fluentui/react-slider-stories": ["./packages/react-components/react-slider/stories/src/index.ts"], + "@fluentui/react-spinbutton": ["./packages/react-components/react-spinbutton/library/src/index.ts"], + "@fluentui/react-spinbutton-stories": ["./packages/react-components/react-spinbutton/stories/src/index.ts"], + "@fluentui/react-spinner": ["./packages/react-components/react-spinner/library/src/index.ts"], + "@fluentui/react-spinner-stories": ["./packages/react-components/react-spinner/stories/src/index.ts"], + "@fluentui/react-storybook-addon": ["./packages/react-components/react-storybook-addon/src/index.ts"], "@fluentui/react-storybook-addon-export-to-sandbox": [ - "packages/react-components/react-storybook-addon-export-to-sandbox/src/index.ts" + "./packages/react-components/react-storybook-addon-export-to-sandbox/src/index.ts" ], - "@fluentui/react-swatch-picker": ["packages/react-components/react-swatch-picker/library/src/index.ts"], - "@fluentui/react-swatch-picker-stories": ["packages/react-components/react-swatch-picker/stories/src/index.ts"], - "@fluentui/react-switch": ["packages/react-components/react-switch/library/src/index.ts"], - "@fluentui/react-switch-stories": ["packages/react-components/react-switch/stories/src/index.ts"], - "@fluentui/react-table": ["packages/react-components/react-table/library/src/index.ts"], - "@fluentui/react-table-stories": ["packages/react-components/react-table/stories/src/index.ts"], - "@fluentui/react-tabs": ["packages/react-components/react-tabs/library/src/index.ts"], - "@fluentui/react-tabs-stories": ["packages/react-components/react-tabs/stories/src/index.ts"], - "@fluentui/react-tabster": ["packages/react-components/react-tabster/src/index.ts"], - "@fluentui/react-tag-picker": ["packages/react-components/react-tag-picker/library/src/index.ts"], - "@fluentui/react-tag-picker-stories": ["packages/react-components/react-tag-picker/stories/src/index.ts"], - "@fluentui/react-tags": ["packages/react-components/react-tags/library/src/index.ts"], - "@fluentui/react-tags-stories": ["packages/react-components/react-tags/stories/src/index.ts"], - "@fluentui/react-teaching-popover": ["packages/react-components/react-teaching-popover/library/src/index.ts"], + "@fluentui/react-swatch-picker": ["./packages/react-components/react-swatch-picker/library/src/index.ts"], + "@fluentui/react-swatch-picker-stories": ["./packages/react-components/react-swatch-picker/stories/src/index.ts"], + "@fluentui/react-switch": ["./packages/react-components/react-switch/library/src/index.ts"], + "@fluentui/react-switch-stories": ["./packages/react-components/react-switch/stories/src/index.ts"], + "@fluentui/react-table": ["./packages/react-components/react-table/library/src/index.ts"], + "@fluentui/react-table-stories": ["./packages/react-components/react-table/stories/src/index.ts"], + "@fluentui/react-tabs": ["./packages/react-components/react-tabs/library/src/index.ts"], + "@fluentui/react-tabs-stories": ["./packages/react-components/react-tabs/stories/src/index.ts"], + "@fluentui/react-tabster": ["./packages/react-components/react-tabster/src/index.ts"], + "@fluentui/react-tag-picker": ["./packages/react-components/react-tag-picker/library/src/index.ts"], + "@fluentui/react-tag-picker-stories": ["./packages/react-components/react-tag-picker/stories/src/index.ts"], + "@fluentui/react-tags": ["./packages/react-components/react-tags/library/src/index.ts"], + "@fluentui/react-tags-stories": ["./packages/react-components/react-tags/stories/src/index.ts"], + "@fluentui/react-teaching-popover": ["./packages/react-components/react-teaching-popover/library/src/index.ts"], "@fluentui/react-teaching-popover-stories": [ - "packages/react-components/react-teaching-popover/stories/src/index.ts" + "./packages/react-components/react-teaching-popover/stories/src/index.ts" ], - "@fluentui/react-text": ["packages/react-components/react-text/library/src/index.ts"], - "@fluentui/react-text-stories": ["packages/react-components/react-text/stories/src/index.ts"], - "@fluentui/react-textarea": ["packages/react-components/react-textarea/library/src/index.ts"], - "@fluentui/react-textarea-stories": ["packages/react-components/react-textarea/stories/src/index.ts"], - "@fluentui/react-theme": ["packages/react-components/react-theme/library/src/index.ts"], - "@fluentui/react-theme-sass": ["packages/react-components/react-theme-sass/src/index.ts"], - "@fluentui/react-theme-stories": ["packages/react-components/react-theme/stories/src/index.ts"], - "@fluentui/react-timepicker-compat": ["packages/react-components/react-timepicker-compat/library/src/index.ts"], + "@fluentui/react-text": ["./packages/react-components/react-text/library/src/index.ts"], + "@fluentui/react-text-stories": ["./packages/react-components/react-text/stories/src/index.ts"], + "@fluentui/react-textarea": ["./packages/react-components/react-textarea/library/src/index.ts"], + "@fluentui/react-textarea-stories": ["./packages/react-components/react-textarea/stories/src/index.ts"], + "@fluentui/react-theme": ["./packages/react-components/react-theme/library/src/index.ts"], + "@fluentui/react-theme-sass": ["./packages/react-components/react-theme-sass/src/index.ts"], + "@fluentui/react-theme-stories": ["./packages/react-components/react-theme/stories/src/index.ts"], + "@fluentui/react-timepicker-compat": ["./packages/react-components/react-timepicker-compat/library/src/index.ts"], "@fluentui/react-timepicker-compat-stories": [ - "packages/react-components/react-timepicker-compat/stories/src/index.ts" + "./packages/react-components/react-timepicker-compat/stories/src/index.ts" ], - "@fluentui/react-toast": ["packages/react-components/react-toast/library/src/index.ts"], - "@fluentui/react-toast-stories": ["packages/react-components/react-toast/stories/src/index.ts"], - "@fluentui/react-toolbar": ["packages/react-components/react-toolbar/library/src/index.ts"], - "@fluentui/react-toolbar-stories": ["packages/react-components/react-toolbar/stories/src/index.ts"], - "@fluentui/react-tooltip": ["packages/react-components/react-tooltip/library/src/index.ts"], - "@fluentui/react-tooltip-stories": ["packages/react-components/react-tooltip/stories/src/index.ts"], - "@fluentui/react-tree": ["packages/react-components/react-tree/library/src/index.ts"], - "@fluentui/react-tree-stories": ["packages/react-components/react-tree/stories/src/index.ts"], - "@fluentui/react-utilities": ["packages/react-components/react-utilities/src/index.ts"], - "@fluentui/react-utilities-compat": ["packages/react-components/react-utilities-compat/library/src/index.ts"], + "@fluentui/react-toast": ["./packages/react-components/react-toast/library/src/index.ts"], + "@fluentui/react-toast-stories": ["./packages/react-components/react-toast/stories/src/index.ts"], + "@fluentui/react-toolbar": ["./packages/react-components/react-toolbar/library/src/index.ts"], + "@fluentui/react-toolbar-stories": ["./packages/react-components/react-toolbar/stories/src/index.ts"], + "@fluentui/react-tooltip": ["./packages/react-components/react-tooltip/library/src/index.ts"], + "@fluentui/react-tooltip-stories": ["./packages/react-components/react-tooltip/stories/src/index.ts"], + "@fluentui/react-tree": ["./packages/react-components/react-tree/library/src/index.ts"], + "@fluentui/react-tree-stories": ["./packages/react-components/react-tree/stories/src/index.ts"], + "@fluentui/react-utilities": ["./packages/react-components/react-utilities/src/index.ts"], + "@fluentui/react-utilities-compat": ["./packages/react-components/react-utilities-compat/library/src/index.ts"], "@fluentui/react-utilities-compat-stories": [ - "packages/react-components/react-utilities-compat/stories/src/index.ts" + "./packages/react-components/react-utilities-compat/stories/src/index.ts" ], - "@fluentui/recipes": ["packages/react-components/recipes/src/index.ts"], - "@fluentui/storybook-llms-extractor": ["tools/storybook-llms-extractor/src/index.ts"], - "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], - "@fluentui/tokens": ["packages/tokens/src/index.ts"], - "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], - "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] + "@fluentui/recipes": ["./packages/react-components/recipes/src/index.ts"], + "@fluentui/storybook-llms-extractor": ["./tools/storybook-llms-extractor/src/index.ts"], + "@fluentui/theme-designer": ["./packages/react-components/theme-designer/src/index.ts"], + "@fluentui/tokens": ["./packages/tokens/src/index.ts"], + "@fluentui/visual-regression-assert": ["./tools/visual-regression-assert/src/index.ts"], + "@fluentui/visual-regression-utilities": ["./tools/visual-regression-utilities/src/index.ts"], + "@fluentui/workspace-plugin": ["./tools/workspace-plugin/src/index.ts"] } } } diff --git a/tsconfig.base.json b/tsconfig.base.json index 58d5e4845e447..6104a82c9cd19 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -2,221 +2,221 @@ "compilerOptions": { "rootDir": ".", "target": "ES2019", - "module": "esnext", - "moduleResolution": "node", + "module": "nodenext", + "moduleResolution": "nodenext", "lib": ["ES2019", "dom"], "sourceMap": true, "strict": true, "skipLibCheck": true, "pretty": true, + "esModuleInterop": true, "typeRoots": ["node_modules/@types", "./typings"], - "baseUrl": ".", "paths": { - "@fluentui/babel-preset-global-context": ["packages/react-components/babel-preset-global-context/src/index.ts"], + "@fluentui/babel-preset-global-context": ["./packages/react-components/babel-preset-global-context/src/index.ts"], "@fluentui/babel-preset-storybook-full-source": [ - "packages/react-components/babel-preset-storybook-full-source/src/index.ts" + "./packages/react-components/babel-preset-storybook-full-source/src/index.ts" ], - "@fluentui/chart-utilities": ["packages/charts/chart-utilities/src/index.ts"], + "@fluentui/chart-utilities": ["./packages/charts/chart-utilities/src/index.ts"], "@fluentui/component-selector-preview": [ - "packages/react-components/component-selector-preview/library/src/index.ts" + "./packages/react-components/component-selector-preview/library/src/index.ts" ], "@fluentui/component-selector-preview-stories": [ - "packages/react-components/component-selector-preview/stories/src/index.ts" + "./packages/react-components/component-selector-preview/stories/src/index.ts" ], "@fluentui/eslint-plugin-react-components": [ - "packages/react-components/eslint-plugin-react-components/src/index.ts" - ], - "@fluentui/global-context": ["packages/react-components/global-context/src/index.ts"], - "@fluentui/keyboard-key": ["packages/keyboard-key/src/index.ts"], - "@fluentui/keyboard-keys": ["packages/react-components/keyboard-keys/src/index.ts"], - "@fluentui/priority-overflow": ["packages/react-components/priority-overflow/src/index.ts"], - "@fluentui/react-accordion": ["packages/react-components/react-accordion/library/src/index.ts"], - "@fluentui/react-accordion-stories": ["packages/react-components/react-accordion/stories/src/index.ts"], - "@fluentui/react-aria": ["packages/react-components/react-aria/library/src/index.ts"], - "@fluentui/react-aria-stories": ["packages/react-components/react-aria/stories/src/index.ts"], - "@fluentui/react-avatar": ["packages/react-components/react-avatar/library/src/index.ts"], - "@fluentui/react-avatar-stories": ["packages/react-components/react-avatar/stories/src/index.ts"], - "@fluentui/react-badge": ["packages/react-components/react-badge/library/src/index.ts"], - "@fluentui/react-badge-stories": ["packages/react-components/react-badge/stories/src/index.ts"], - "@fluentui/react-breadcrumb": ["packages/react-components/react-breadcrumb/library/src/index.ts"], - "@fluentui/react-breadcrumb-stories": ["packages/react-components/react-breadcrumb/stories/src/index.ts"], - "@fluentui/react-button": ["packages/react-components/react-button/library/src/index.ts"], - "@fluentui/react-button-stories": ["packages/react-components/react-button/stories/src/index.ts"], - "@fluentui/react-calendar-compat": ["packages/react-components/react-calendar-compat/library/src/index.ts"], + "./packages/react-components/eslint-plugin-react-components/src/index.ts" + ], + "@fluentui/global-context": ["./packages/react-components/global-context/src/index.ts"], + "@fluentui/keyboard-key": ["./packages/keyboard-key/src/index.ts"], + "@fluentui/keyboard-keys": ["./packages/react-components/keyboard-keys/src/index.ts"], + "@fluentui/priority-overflow": ["./packages/react-components/priority-overflow/src/index.ts"], + "@fluentui/react-accordion": ["./packages/react-components/react-accordion/library/src/index.ts"], + "@fluentui/react-accordion-stories": ["./packages/react-components/react-accordion/stories/src/index.ts"], + "@fluentui/react-aria": ["./packages/react-components/react-aria/library/src/index.ts"], + "@fluentui/react-aria-stories": ["./packages/react-components/react-aria/stories/src/index.ts"], + "@fluentui/react-avatar": ["./packages/react-components/react-avatar/library/src/index.ts"], + "@fluentui/react-avatar-stories": ["./packages/react-components/react-avatar/stories/src/index.ts"], + "@fluentui/react-badge": ["./packages/react-components/react-badge/library/src/index.ts"], + "@fluentui/react-badge-stories": ["./packages/react-components/react-badge/stories/src/index.ts"], + "@fluentui/react-breadcrumb": ["./packages/react-components/react-breadcrumb/library/src/index.ts"], + "@fluentui/react-breadcrumb-stories": ["./packages/react-components/react-breadcrumb/stories/src/index.ts"], + "@fluentui/react-button": ["./packages/react-components/react-button/library/src/index.ts"], + "@fluentui/react-button-stories": ["./packages/react-components/react-button/stories/src/index.ts"], + "@fluentui/react-calendar-compat": ["./packages/react-components/react-calendar-compat/library/src/index.ts"], "@fluentui/react-calendar-compat-stories": [ - "packages/react-components/react-calendar-compat/stories/src/index.ts" - ], - "@fluentui/react-card": ["packages/react-components/react-card/library/src/index.ts"], - "@fluentui/react-card-stories": ["packages/react-components/react-card/stories/src/index.ts"], - "@fluentui/react-carousel": ["packages/react-components/react-carousel/library/src/index.ts"], - "@fluentui/react-carousel-stories": ["packages/react-components/react-carousel/stories/src/index.ts"], - "@fluentui/react-charts": ["packages/charts/react-charts/library/src/index.ts"], - "@fluentui/react-charts-stories": ["packages/charts/react-charts/stories/src/index.ts"], - "@fluentui/react-checkbox": ["packages/react-components/react-checkbox/library/src/index.ts"], - "@fluentui/react-checkbox-stories": ["packages/react-components/react-checkbox/stories/src/index.ts"], - "@fluentui/react-color-picker": ["packages/react-components/react-color-picker/library/src/index.ts"], - "@fluentui/react-color-picker-stories": ["packages/react-components/react-color-picker/stories/src/index.ts"], - "@fluentui/react-colorpicker-compat": ["packages/react-components/react-colorpicker-compat/src/index.ts"], - "@fluentui/react-combobox": ["packages/react-components/react-combobox/library/src/index.ts"], - "@fluentui/react-combobox-stories": ["packages/react-components/react-combobox/stories/src/index.ts"], - "@fluentui/react-components": ["packages/react-components/react-components/src/index.ts"], - "@fluentui/react-components/unstable": ["packages/react-components/react-components/src/unstable/index.ts"], - "@fluentui/react-conformance": ["packages/react-conformance/src/index.ts"], - "@fluentui/react-conformance-griffel": ["packages/react-components/react-conformance-griffel/src/index.ts"], - "@fluentui/react-context-selector": ["packages/react-components/react-context-selector/src/index.ts"], - "@fluentui/react-datepicker-compat": ["packages/react-components/react-datepicker-compat/library/src/index.ts"], + "./packages/react-components/react-calendar-compat/stories/src/index.ts" + ], + "@fluentui/react-card": ["./packages/react-components/react-card/library/src/index.ts"], + "@fluentui/react-card-stories": ["./packages/react-components/react-card/stories/src/index.ts"], + "@fluentui/react-carousel": ["./packages/react-components/react-carousel/library/src/index.ts"], + "@fluentui/react-carousel-stories": ["./packages/react-components/react-carousel/stories/src/index.ts"], + "@fluentui/react-charts": ["./packages/charts/react-charts/library/src/index.ts"], + "@fluentui/react-charts-stories": ["./packages/charts/react-charts/stories/src/index.ts"], + "@fluentui/react-checkbox": ["./packages/react-components/react-checkbox/library/src/index.ts"], + "@fluentui/react-checkbox-stories": ["./packages/react-components/react-checkbox/stories/src/index.ts"], + "@fluentui/react-color-picker": ["./packages/react-components/react-color-picker/library/src/index.ts"], + "@fluentui/react-color-picker-stories": ["./packages/react-components/react-color-picker/stories/src/index.ts"], + "@fluentui/react-colorpicker-compat": ["./packages/react-components/react-colorpicker-compat/src/index.ts"], + "@fluentui/react-combobox": ["./packages/react-components/react-combobox/library/src/index.ts"], + "@fluentui/react-combobox-stories": ["./packages/react-components/react-combobox/stories/src/index.ts"], + "@fluentui/react-components": ["./packages/react-components/react-components/src/index.ts"], + "@fluentui/react-components/unstable": ["./packages/react-components/react-components/src/unstable/index.ts"], + "@fluentui/react-conformance": ["./packages/react-conformance/src/index.ts"], + "@fluentui/react-conformance-griffel": ["./packages/react-components/react-conformance-griffel/src/index.ts"], + "@fluentui/react-context-selector": ["./packages/react-components/react-context-selector/src/index.ts"], + "@fluentui/react-datepicker-compat": ["./packages/react-components/react-datepicker-compat/library/src/index.ts"], "@fluentui/react-datepicker-compat-stories": [ - "packages/react-components/react-datepicker-compat/stories/src/index.ts" - ], - "@fluentui/react-dialog": ["packages/react-components/react-dialog/library/src/index.ts"], - "@fluentui/react-dialog-stories": ["packages/react-components/react-dialog/stories/src/index.ts"], - "@fluentui/react-divider": ["packages/react-components/react-divider/library/src/index.ts"], - "@fluentui/react-divider-stories": ["packages/react-components/react-divider/stories/src/index.ts"], - "@fluentui/react-drawer": ["packages/react-components/react-drawer/library/src/index.ts"], - "@fluentui/react-drawer-stories": ["packages/react-components/react-drawer/stories/src/index.ts"], - "@fluentui/react-field": ["packages/react-components/react-field/library/src/index.ts"], - "@fluentui/react-field-stories": ["packages/react-components/react-field/stories/src/index.ts"], - "@fluentui/react-focus-management": ["packages/react-focus-management/src/index.ts"], + "./packages/react-components/react-datepicker-compat/stories/src/index.ts" + ], + "@fluentui/react-dialog": ["./packages/react-components/react-dialog/library/src/index.ts"], + "@fluentui/react-dialog-stories": ["./packages/react-components/react-dialog/stories/src/index.ts"], + "@fluentui/react-divider": ["./packages/react-components/react-divider/library/src/index.ts"], + "@fluentui/react-divider-stories": ["./packages/react-components/react-divider/stories/src/index.ts"], + "@fluentui/react-drawer": ["./packages/react-components/react-drawer/library/src/index.ts"], + "@fluentui/react-drawer-stories": ["./packages/react-components/react-drawer/stories/src/index.ts"], + "@fluentui/react-field": ["./packages/react-components/react-field/library/src/index.ts"], + "@fluentui/react-field-stories": ["./packages/react-components/react-field/stories/src/index.ts"], + "@fluentui/react-focus-management": ["./packages/react-focus-management/src/index.ts"], "@fluentui/react-headless-components-preview-stories": [ - "packages/react-components/react-headless-components-preview/stories/src/index.ts" + "./packages/react-components/react-headless-components-preview/stories/src/index.ts" ], "@fluentui/react-headless-components-preview/*": [ - "packages/react-components/react-headless-components-preview/library/src/*.ts" - ], - "@fluentui/react-icons-compat": ["packages/react-components/react-icons-compat/library/src/index.ts"], - "@fluentui/react-icons-compat-stories": ["packages/react-components/react-icons-compat/stories/src/index.ts"], - "@fluentui/react-image": ["packages/react-components/react-image/library/src/index.ts"], - "@fluentui/react-image-stories": ["packages/react-components/react-image/stories/src/index.ts"], - "@fluentui/react-infolabel": ["packages/react-components/react-infolabel/library/src/index.ts"], - "@fluentui/react-infolabel-stories": ["packages/react-components/react-infolabel/stories/src/index.ts"], - "@fluentui/react-input": ["packages/react-components/react-input/library/src/index.ts"], - "@fluentui/react-input-stories": ["packages/react-components/react-input/stories/src/index.ts"], - "@fluentui/react-integration-tester": ["tools/react-integration-tester/src/index.ts"], - "@fluentui/react-jsx-runtime": ["packages/react-components/react-jsx-runtime/src/index.ts"], + "./packages/react-components/react-headless-components-preview/library/src/*.ts" + ], + "@fluentui/react-icons-compat": ["./packages/react-components/react-icons-compat/library/src/index.ts"], + "@fluentui/react-icons-compat-stories": ["./packages/react-components/react-icons-compat/stories/src/index.ts"], + "@fluentui/react-image": ["./packages/react-components/react-image/library/src/index.ts"], + "@fluentui/react-image-stories": ["./packages/react-components/react-image/stories/src/index.ts"], + "@fluentui/react-infolabel": ["./packages/react-components/react-infolabel/library/src/index.ts"], + "@fluentui/react-infolabel-stories": ["./packages/react-components/react-infolabel/stories/src/index.ts"], + "@fluentui/react-input": ["./packages/react-components/react-input/library/src/index.ts"], + "@fluentui/react-input-stories": ["./packages/react-components/react-input/stories/src/index.ts"], + "@fluentui/react-integration-tester": ["./tools/react-integration-tester/src/index.ts"], + "@fluentui/react-jsx-runtime": ["./packages/react-components/react-jsx-runtime/src/index.ts"], "@fluentui/react-jsx-runtime/jsx-dev-runtime": [ - "packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" - ], - "@fluentui/react-jsx-runtime/jsx-runtime": ["packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], - "@fluentui/react-label": ["packages/react-components/react-label/library/src/index.ts"], - "@fluentui/react-label-stories": ["packages/react-components/react-label/stories/src/index.ts"], - "@fluentui/react-link": ["packages/react-components/react-link/library/src/index.ts"], - "@fluentui/react-link-stories": ["packages/react-components/react-link/stories/src/index.ts"], - "@fluentui/react-list": ["packages/react-components/react-list/library/src/index.ts"], - "@fluentui/react-list-stories": ["packages/react-components/react-list/stories/src/index.ts"], - "@fluentui/react-menu": ["packages/react-components/react-menu/library/src/index.ts"], - "@fluentui/react-menu-grid-preview": ["packages/react-components/react-menu-grid-preview/library/src/index.ts"], + "./packages/react-components/react-jsx-runtime/src/jsx-dev-runtime.ts" + ], + "@fluentui/react-jsx-runtime/jsx-runtime": ["./packages/react-components/react-jsx-runtime/src/jsx-runtime.ts"], + "@fluentui/react-label": ["./packages/react-components/react-label/library/src/index.ts"], + "@fluentui/react-label-stories": ["./packages/react-components/react-label/stories/src/index.ts"], + "@fluentui/react-link": ["./packages/react-components/react-link/library/src/index.ts"], + "@fluentui/react-link-stories": ["./packages/react-components/react-link/stories/src/index.ts"], + "@fluentui/react-list": ["./packages/react-components/react-list/library/src/index.ts"], + "@fluentui/react-list-stories": ["./packages/react-components/react-list/stories/src/index.ts"], + "@fluentui/react-menu": ["./packages/react-components/react-menu/library/src/index.ts"], + "@fluentui/react-menu-grid-preview": ["./packages/react-components/react-menu-grid-preview/library/src/index.ts"], "@fluentui/react-menu-grid-preview-stories": [ - "packages/react-components/react-menu-grid-preview/stories/src/index.ts" + "./packages/react-components/react-menu-grid-preview/stories/src/index.ts" ], - "@fluentui/react-menu-stories": ["packages/react-components/react-menu/stories/src/index.ts"], - "@fluentui/react-message-bar": ["packages/react-components/react-message-bar/library/src/index.ts"], - "@fluentui/react-message-bar-stories": ["packages/react-components/react-message-bar/stories/src/index.ts"], - "@fluentui/react-migration-v0-v9": ["packages/react-components/react-migration-v0-v9/library/src/index.ts"], + "@fluentui/react-menu-stories": ["./packages/react-components/react-menu/stories/src/index.ts"], + "@fluentui/react-message-bar": ["./packages/react-components/react-message-bar/library/src/index.ts"], + "@fluentui/react-message-bar-stories": ["./packages/react-components/react-message-bar/stories/src/index.ts"], + "@fluentui/react-migration-v0-v9": ["./packages/react-components/react-migration-v0-v9/library/src/index.ts"], "@fluentui/react-migration-v0-v9-stories": [ - "packages/react-components/react-migration-v0-v9/stories/src/index.ts" + "./packages/react-components/react-migration-v0-v9/stories/src/index.ts" ], - "@fluentui/react-migration-v8-v9": ["packages/react-components/react-migration-v8-v9/library/src/index.ts"], + "@fluentui/react-migration-v8-v9": ["./packages/react-components/react-migration-v8-v9/library/src/index.ts"], "@fluentui/react-migration-v8-v9-stories": [ - "packages/react-components/react-migration-v8-v9/stories/src/index.ts" + "./packages/react-components/react-migration-v8-v9/stories/src/index.ts" ], - "@fluentui/react-motion": ["packages/react-components/react-motion/library/src/index.ts"], + "@fluentui/react-motion": ["./packages/react-components/react-motion/library/src/index.ts"], "@fluentui/react-motion-components-preview": [ - "packages/react-components/react-motion-components-preview/library/src/index.ts" + "./packages/react-components/react-motion-components-preview/library/src/index.ts" ], "@fluentui/react-motion-components-preview-stories": [ - "packages/react-components/react-motion-components-preview/stories/src/index.ts" - ], - "@fluentui/react-motion-stories": ["packages/react-components/react-motion/stories/src/index.ts"], - "@fluentui/react-nav": ["packages/react-components/react-nav/library/src/index.ts"], - "@fluentui/react-nav-stories": ["packages/react-components/react-nav/stories/src/index.ts"], - "@fluentui/react-overflow": ["packages/react-components/react-overflow/library/src/index.ts"], - "@fluentui/react-overflow-stories": ["packages/react-components/react-overflow/stories/src/index.ts"], - "@fluentui/react-persona": ["packages/react-components/react-persona/library/src/index.ts"], - "@fluentui/react-persona-stories": ["packages/react-components/react-persona/stories/src/index.ts"], - "@fluentui/react-popover": ["packages/react-components/react-popover/library/src/index.ts"], - "@fluentui/react-popover-stories": ["packages/react-components/react-popover/stories/src/index.ts"], - "@fluentui/react-portal": ["packages/react-components/react-portal/library/src/index.ts"], - "@fluentui/react-portal-compat": ["packages/react-components/react-portal-compat/src/index.ts"], - "@fluentui/react-portal-compat-context": ["packages/react-components/react-portal-compat-context/src/index.ts"], - "@fluentui/react-portal-stories": ["packages/react-components/react-portal/stories/src/index.ts"], - "@fluentui/react-positioning": ["packages/react-components/react-positioning/library/src/index.ts"], - "@fluentui/react-positioning-stories": ["packages/react-components/react-positioning/stories/src/index.ts"], - "@fluentui/react-progress": ["packages/react-components/react-progress/library/src/index.ts"], - "@fluentui/react-progress-stories": ["packages/react-components/react-progress/stories/src/index.ts"], - "@fluentui/react-provider": ["packages/react-components/react-provider/library/src/index.ts"], - "@fluentui/react-provider-stories": ["packages/react-components/react-provider/stories/src/index.ts"], - "@fluentui/react-radio": ["packages/react-components/react-radio/library/src/index.ts"], - "@fluentui/react-radio-stories": ["packages/react-components/react-radio/stories/src/index.ts"], - "@fluentui/react-rating": ["packages/react-components/react-rating/library/src/index.ts"], - "@fluentui/react-rating-stories": ["packages/react-components/react-rating/stories/src/index.ts"], - "@fluentui/react-search": ["packages/react-components/react-search/library/src/index.ts"], - "@fluentui/react-search-stories": ["packages/react-components/react-search/stories/src/index.ts"], - "@fluentui/react-select": ["packages/react-components/react-select/library/src/index.ts"], - "@fluentui/react-select-stories": ["packages/react-components/react-select/stories/src/index.ts"], - "@fluentui/react-shared-contexts": ["packages/react-components/react-shared-contexts/library/src/index.ts"], + "./packages/react-components/react-motion-components-preview/stories/src/index.ts" + ], + "@fluentui/react-motion-stories": ["./packages/react-components/react-motion/stories/src/index.ts"], + "@fluentui/react-nav": ["./packages/react-components/react-nav/library/src/index.ts"], + "@fluentui/react-nav-stories": ["./packages/react-components/react-nav/stories/src/index.ts"], + "@fluentui/react-overflow": ["./packages/react-components/react-overflow/library/src/index.ts"], + "@fluentui/react-overflow-stories": ["./packages/react-components/react-overflow/stories/src/index.ts"], + "@fluentui/react-persona": ["./packages/react-components/react-persona/library/src/index.ts"], + "@fluentui/react-persona-stories": ["./packages/react-components/react-persona/stories/src/index.ts"], + "@fluentui/react-popover": ["./packages/react-components/react-popover/library/src/index.ts"], + "@fluentui/react-popover-stories": ["./packages/react-components/react-popover/stories/src/index.ts"], + "@fluentui/react-portal": ["./packages/react-components/react-portal/library/src/index.ts"], + "@fluentui/react-portal-compat": ["./packages/react-components/react-portal-compat/src/index.ts"], + "@fluentui/react-portal-compat-context": ["./packages/react-components/react-portal-compat-context/src/index.ts"], + "@fluentui/react-portal-stories": ["./packages/react-components/react-portal/stories/src/index.ts"], + "@fluentui/react-positioning": ["./packages/react-components/react-positioning/library/src/index.ts"], + "@fluentui/react-positioning-stories": ["./packages/react-components/react-positioning/stories/src/index.ts"], + "@fluentui/react-progress": ["./packages/react-components/react-progress/library/src/index.ts"], + "@fluentui/react-progress-stories": ["./packages/react-components/react-progress/stories/src/index.ts"], + "@fluentui/react-provider": ["./packages/react-components/react-provider/library/src/index.ts"], + "@fluentui/react-provider-stories": ["./packages/react-components/react-provider/stories/src/index.ts"], + "@fluentui/react-radio": ["./packages/react-components/react-radio/library/src/index.ts"], + "@fluentui/react-radio-stories": ["./packages/react-components/react-radio/stories/src/index.ts"], + "@fluentui/react-rating": ["./packages/react-components/react-rating/library/src/index.ts"], + "@fluentui/react-rating-stories": ["./packages/react-components/react-rating/stories/src/index.ts"], + "@fluentui/react-search": ["./packages/react-components/react-search/library/src/index.ts"], + "@fluentui/react-search-stories": ["./packages/react-components/react-search/stories/src/index.ts"], + "@fluentui/react-select": ["./packages/react-components/react-select/library/src/index.ts"], + "@fluentui/react-select-stories": ["./packages/react-components/react-select/stories/src/index.ts"], + "@fluentui/react-shared-contexts": ["./packages/react-components/react-shared-contexts/library/src/index.ts"], "@fluentui/react-shared-contexts-stories": [ - "packages/react-components/react-shared-contexts/stories/src/index.ts" - ], - "@fluentui/react-skeleton": ["packages/react-components/react-skeleton/library/src/index.ts"], - "@fluentui/react-skeleton-stories": ["packages/react-components/react-skeleton/stories/src/index.ts"], - "@fluentui/react-slider": ["packages/react-components/react-slider/library/src/index.ts"], - "@fluentui/react-slider-stories": ["packages/react-components/react-slider/stories/src/index.ts"], - "@fluentui/react-spinbutton": ["packages/react-components/react-spinbutton/library/src/index.ts"], - "@fluentui/react-spinbutton-stories": ["packages/react-components/react-spinbutton/stories/src/index.ts"], - "@fluentui/react-spinner": ["packages/react-components/react-spinner/library/src/index.ts"], - "@fluentui/react-spinner-stories": ["packages/react-components/react-spinner/stories/src/index.ts"], - "@fluentui/react-storybook-addon": ["packages/react-components/react-storybook-addon/src/index.ts"], + "./packages/react-components/react-shared-contexts/stories/src/index.ts" + ], + "@fluentui/react-skeleton": ["./packages/react-components/react-skeleton/library/src/index.ts"], + "@fluentui/react-skeleton-stories": ["./packages/react-components/react-skeleton/stories/src/index.ts"], + "@fluentui/react-slider": ["./packages/react-components/react-slider/library/src/index.ts"], + "@fluentui/react-slider-stories": ["./packages/react-components/react-slider/stories/src/index.ts"], + "@fluentui/react-spinbutton": ["./packages/react-components/react-spinbutton/library/src/index.ts"], + "@fluentui/react-spinbutton-stories": ["./packages/react-components/react-spinbutton/stories/src/index.ts"], + "@fluentui/react-spinner": ["./packages/react-components/react-spinner/library/src/index.ts"], + "@fluentui/react-spinner-stories": ["./packages/react-components/react-spinner/stories/src/index.ts"], + "@fluentui/react-storybook-addon": ["./packages/react-components/react-storybook-addon/src/index.ts"], "@fluentui/react-storybook-addon-export-to-sandbox": [ - "packages/react-components/react-storybook-addon-export-to-sandbox/src/index.ts" - ], - "@fluentui/react-swatch-picker": ["packages/react-components/react-swatch-picker/library/src/index.ts"], - "@fluentui/react-swatch-picker-stories": ["packages/react-components/react-swatch-picker/stories/src/index.ts"], - "@fluentui/react-switch": ["packages/react-components/react-switch/library/src/index.ts"], - "@fluentui/react-switch-stories": ["packages/react-components/react-switch/stories/src/index.ts"], - "@fluentui/react-table": ["packages/react-components/react-table/library/src/index.ts"], - "@fluentui/react-table-stories": ["packages/react-components/react-table/stories/src/index.ts"], - "@fluentui/react-tabs": ["packages/react-components/react-tabs/library/src/index.ts"], - "@fluentui/react-tabs-stories": ["packages/react-components/react-tabs/stories/src/index.ts"], - "@fluentui/react-tabster": ["packages/react-components/react-tabster/src/index.ts"], - "@fluentui/react-tag-picker": ["packages/react-components/react-tag-picker/library/src/index.ts"], - "@fluentui/react-tag-picker-stories": ["packages/react-components/react-tag-picker/stories/src/index.ts"], - "@fluentui/react-tags": ["packages/react-components/react-tags/library/src/index.ts"], - "@fluentui/react-tags-stories": ["packages/react-components/react-tags/stories/src/index.ts"], - "@fluentui/react-teaching-popover": ["packages/react-components/react-teaching-popover/library/src/index.ts"], + "./packages/react-components/react-storybook-addon-export-to-sandbox/src/index.ts" + ], + "@fluentui/react-swatch-picker": ["./packages/react-components/react-swatch-picker/library/src/index.ts"], + "@fluentui/react-swatch-picker-stories": ["./packages/react-components/react-swatch-picker/stories/src/index.ts"], + "@fluentui/react-switch": ["./packages/react-components/react-switch/library/src/index.ts"], + "@fluentui/react-switch-stories": ["./packages/react-components/react-switch/stories/src/index.ts"], + "@fluentui/react-table": ["./packages/react-components/react-table/library/src/index.ts"], + "@fluentui/react-table-stories": ["./packages/react-components/react-table/stories/src/index.ts"], + "@fluentui/react-tabs": ["./packages/react-components/react-tabs/library/src/index.ts"], + "@fluentui/react-tabs-stories": ["./packages/react-components/react-tabs/stories/src/index.ts"], + "@fluentui/react-tabster": ["./packages/react-components/react-tabster/src/index.ts"], + "@fluentui/react-tag-picker": ["./packages/react-components/react-tag-picker/library/src/index.ts"], + "@fluentui/react-tag-picker-stories": ["./packages/react-components/react-tag-picker/stories/src/index.ts"], + "@fluentui/react-tags": ["./packages/react-components/react-tags/library/src/index.ts"], + "@fluentui/react-tags-stories": ["./packages/react-components/react-tags/stories/src/index.ts"], + "@fluentui/react-teaching-popover": ["./packages/react-components/react-teaching-popover/library/src/index.ts"], "@fluentui/react-teaching-popover-stories": [ - "packages/react-components/react-teaching-popover/stories/src/index.ts" - ], - "@fluentui/react-text": ["packages/react-components/react-text/library/src/index.ts"], - "@fluentui/react-text-stories": ["packages/react-components/react-text/stories/src/index.ts"], - "@fluentui/react-textarea": ["packages/react-components/react-textarea/library/src/index.ts"], - "@fluentui/react-textarea-stories": ["packages/react-components/react-textarea/stories/src/index.ts"], - "@fluentui/react-theme": ["packages/react-components/react-theme/library/src/index.ts"], - "@fluentui/react-theme-sass": ["packages/react-components/react-theme-sass/src/index.ts"], - "@fluentui/react-theme-stories": ["packages/react-components/react-theme/stories/src/index.ts"], - "@fluentui/react-timepicker-compat": ["packages/react-components/react-timepicker-compat/library/src/index.ts"], + "./packages/react-components/react-teaching-popover/stories/src/index.ts" + ], + "@fluentui/react-text": ["./packages/react-components/react-text/library/src/index.ts"], + "@fluentui/react-text-stories": ["./packages/react-components/react-text/stories/src/index.ts"], + "@fluentui/react-textarea": ["./packages/react-components/react-textarea/library/src/index.ts"], + "@fluentui/react-textarea-stories": ["./packages/react-components/react-textarea/stories/src/index.ts"], + "@fluentui/react-theme": ["./packages/react-components/react-theme/library/src/index.ts"], + "@fluentui/react-theme-sass": ["./packages/react-components/react-theme-sass/src/index.ts"], + "@fluentui/react-theme-stories": ["./packages/react-components/react-theme/stories/src/index.ts"], + "@fluentui/react-timepicker-compat": ["./packages/react-components/react-timepicker-compat/library/src/index.ts"], "@fluentui/react-timepicker-compat-stories": [ - "packages/react-components/react-timepicker-compat/stories/src/index.ts" - ], - "@fluentui/react-toast": ["packages/react-components/react-toast/library/src/index.ts"], - "@fluentui/react-toast-stories": ["packages/react-components/react-toast/stories/src/index.ts"], - "@fluentui/react-toolbar": ["packages/react-components/react-toolbar/library/src/index.ts"], - "@fluentui/react-toolbar-stories": ["packages/react-components/react-toolbar/stories/src/index.ts"], - "@fluentui/react-tooltip": ["packages/react-components/react-tooltip/library/src/index.ts"], - "@fluentui/react-tooltip-stories": ["packages/react-components/react-tooltip/stories/src/index.ts"], - "@fluentui/react-tree": ["packages/react-components/react-tree/library/src/index.ts"], - "@fluentui/react-tree-stories": ["packages/react-components/react-tree/stories/src/index.ts"], - "@fluentui/react-utilities": ["packages/react-components/react-utilities/src/index.ts"], - "@fluentui/react-utilities-compat": ["packages/react-components/react-utilities-compat/library/src/index.ts"], + "./packages/react-components/react-timepicker-compat/stories/src/index.ts" + ], + "@fluentui/react-toast": ["./packages/react-components/react-toast/library/src/index.ts"], + "@fluentui/react-toast-stories": ["./packages/react-components/react-toast/stories/src/index.ts"], + "@fluentui/react-toolbar": ["./packages/react-components/react-toolbar/library/src/index.ts"], + "@fluentui/react-toolbar-stories": ["./packages/react-components/react-toolbar/stories/src/index.ts"], + "@fluentui/react-tooltip": ["./packages/react-components/react-tooltip/library/src/index.ts"], + "@fluentui/react-tooltip-stories": ["./packages/react-components/react-tooltip/stories/src/index.ts"], + "@fluentui/react-tree": ["./packages/react-components/react-tree/library/src/index.ts"], + "@fluentui/react-tree-stories": ["./packages/react-components/react-tree/stories/src/index.ts"], + "@fluentui/react-utilities": ["./packages/react-components/react-utilities/src/index.ts"], + "@fluentui/react-utilities-compat": ["./packages/react-components/react-utilities-compat/library/src/index.ts"], "@fluentui/react-utilities-compat-stories": [ - "packages/react-components/react-utilities-compat/stories/src/index.ts" - ], - "@fluentui/recipes": ["packages/react-components/recipes/src/index.ts"], - "@fluentui/storybook-llms-extractor": ["tools/storybook-llms-extractor/src/index.ts"], - "@fluentui/theme-designer": ["packages/react-components/theme-designer/src/index.ts"], - "@fluentui/tokens": ["packages/tokens/src/index.ts"], - "@fluentui/visual-regression-assert": ["tools/visual-regression-assert/src/index.ts"], - "@fluentui/visual-regression-utilities": ["tools/visual-regression-utilities/src/index.ts"], - "@fluentui/workspace-plugin": ["tools/workspace-plugin/src/index.ts"] + "./packages/react-components/react-utilities-compat/stories/src/index.ts" + ], + "@fluentui/recipes": ["./packages/react-components/recipes/src/index.ts"], + "@fluentui/storybook-llms-extractor": ["./tools/storybook-llms-extractor/src/index.ts"], + "@fluentui/theme-designer": ["./packages/react-components/theme-designer/src/index.ts"], + "@fluentui/tokens": ["./packages/tokens/src/index.ts"], + "@fluentui/visual-regression-assert": ["./tools/visual-regression-assert/src/index.ts"], + "@fluentui/visual-regression-utilities": ["./tools/visual-regression-utilities/src/index.ts"], + "@fluentui/workspace-plugin": ["./tools/workspace-plugin/src/index.ts"] } }, "exclude": ["node_modules"] diff --git a/tsconfig.base.v8.json b/tsconfig.base.v8.json index 16cf17379d598..fb246cf8cc8d5 100644 --- a/tsconfig.base.v8.json +++ b/tsconfig.base.v8.json @@ -1,64 +1,65 @@ { "compilerOptions": { - "moduleResolution": "Node", + "module": "commonjs", + "moduleResolution": "node10", + "ignoreDeprecations": "6.0", "pretty": true, "sourceMap": true, "skipLibCheck": true, + "strict": false, "strictNullChecks": true, "noImplicitAny": true, "typeRoots": ["node_modules/@types", "./typings"], "rootDir": ".", - "baseUrl": ".", "paths": { - "@fluentui/react-portal-compat-context": ["packages/react-components/react-portal-compat-context/src/index.ts"], - - "@fluentui/api-docs": ["packages/api-docs/src/index.ts"], - "@fluentui/azure-themes": ["packages/azure-themes/src/index.ts"], - "@fluentui/codemods": ["packages/codemods/src/index.ts"], - "@fluentui/date-time-utilities": ["packages/date-time-utilities/src/index.ts"], - "@fluentui/example-data": ["packages/example-data/src/index.ts"], - "@fluentui/fluent2-theme": ["packages/fluent2-theme/src/index.ts"], - "@fluentui/font-icons-mdl2": ["packages/font-icons-mdl2/src/index.ts"], - "@fluentui/foundation-legacy": ["packages/foundation-legacy/src/index.ts"], - "@fluentui/jest-serializer-merge-styles": ["packages/jest-serializer-merge-styles/src/index.ts"], - "@fluentui/merge-styles": ["packages/merge-styles/src/index.ts"], - "@fluentui/react": ["packages/react/src/index.ts"], - "@fluentui/react/lib/*": ["packages/react/src/*"], - "@fluentui/react-date-time": ["packages/react-date-time/src/index.ts"], - "@fluentui/react-experiments": ["packages/react-experiments/src/index.ts"], - "@fluentui/react-experiments/lib/*": ["packages/react-experiments/src/*"], - "@fluentui/react-file-type-icons": ["packages/react-file-type-icons/src/index.ts"], - "@fluentui/react-focus": ["packages/react-focus/src/index.ts"], - "@fluentui/react-hooks": ["packages/react-hooks/src/index.ts"], - "@fluentui/scheme-utilities": ["packages/scheme-utilities/src/index.ts"], - "@fluentui/set-version": ["packages/set-version/src/index.ts"], - "@fluentui/style-utilities": ["packages/style-utilities/src/index.ts"], - "@fluentui/test-utilities": ["packages/test-utilities/src/index.ts"], - "@fluentui/theme-samples": ["packages/theme-samples/src/index.ts"], - "@fluentui/utilities": ["packages/utilities/src/index.ts"], - "@fluentui/webpack-utilities": ["packages/utilities/src/index.ts"], - "@fluentui/dom-utilities": ["packages/dom-utilities/src/index.ts"], - "@fluentui/theme": ["packages/theme/src/index.ts"], - "@fluentui/react-cards": ["packages/react-cards/src/index.ts"], - "@fluentui/react-charting": ["packages/charts/react-charting/src/index.ts"], - "@fluentui/chart-utilities": ["packages/charts/chart-utilities/src/index.ts"], - "@fluentui/react-window-provider": ["packages/react-window-provider/src/index.ts"], - "@fluentui/react-icons-mdl2": ["packages/react-icons-mdl2/src/index.ts"], - "@fluentui/react-icons-mdl2-branded": ["packages/react-icons-mdl2-branded/src/index.ts"], - "@fluentui/react-icon-provider": ["packages/react-icon-provider/src/index.ts"], - "@fluentui/react-examples": ["packages/react-examples/src/index.ts"], - "@fluentui/react-examples/lib/*": ["packages/react-examples/src/*"], - "@fluentui/public-docsite-setup": ["packages/public-docsite-setup/src/index.ts"], - "@fluentui/react-docsite-components": ["packages/react-docsite-components/src/index.ts"], - "@fluentui/react-docsite-components/lib/index2": ["packages/react-docsite-components/src/index2.ts"], + "@fluentui/react-portal-compat-context": ["./packages/react-components/react-portal-compat-context/src/index.ts"], + "@fluentui/api-docs": ["./packages/api-docs/src/index.ts"], + "@fluentui/azure-themes": ["./packages/azure-themes/src/index.ts"], + "@fluentui/codemods": ["./packages/codemods/src/index.ts"], + "@fluentui/date-time-utilities": ["./packages/date-time-utilities/src/index.ts"], + "@fluentui/example-data": ["./packages/example-data/src/index.ts"], + "@fluentui/fluent2-theme": ["./packages/fluent2-theme/src/index.ts"], + "@fluentui/font-icons-mdl2": ["./packages/font-icons-mdl2/src/index.ts"], + "@fluentui/foundation-legacy": ["./packages/foundation-legacy/src/index.ts"], + "@fluentui/jest-serializer-merge-styles": ["./packages/jest-serializer-merge-styles/src/index.ts"], + "@fluentui/merge-styles": ["./packages/merge-styles/src/index.ts"], + "@fluentui/react": ["./packages/react/src/index.ts"], + "@fluentui/react/lib/*": ["./packages/react/src/*"], + "@fluentui/react-date-time": ["./packages/react-date-time/src/index.ts"], + "@fluentui/react-experiments": ["./packages/react-experiments/src/index.ts"], + "@fluentui/react-experiments/lib/*": ["./packages/react-experiments/src/*"], + "@fluentui/react-file-type-icons": ["./packages/react-file-type-icons/src/index.ts"], + "@fluentui/react-focus": ["./packages/react-focus/src/index.ts"], + "@fluentui/react-hooks": ["./packages/react-hooks/src/index.ts"], + "@fluentui/scheme-utilities": ["./packages/scheme-utilities/src/index.ts"], + "@fluentui/set-version": ["./packages/set-version/src/index.ts"], + "@fluentui/style-utilities": ["./packages/style-utilities/src/index.ts"], + "@fluentui/test-utilities": ["./packages/test-utilities/src/index.ts"], + "@fluentui/theme-samples": ["./packages/theme-samples/src/index.ts"], + "@fluentui/utilities": ["./packages/utilities/src/index.ts"], + "@fluentui/webpack-utilities": ["./packages/utilities/src/index.ts"], + "@fluentui/dom-utilities": ["./packages/dom-utilities/src/index.ts"], + "@fluentui/theme": ["./packages/theme/src/index.ts"], + "@fluentui/react-cards": ["./packages/react-cards/src/index.ts"], + "@fluentui/react-charting": ["./packages/charts/react-charting/src/index.ts"], + "@fluentui/chart-utilities": ["./packages/charts/chart-utilities/src/index.ts"], + "@fluentui/react-window-provider": ["./packages/react-window-provider/src/index.ts"], + "@fluentui/react-icons-mdl2": ["./packages/react-icons-mdl2/src/index.ts"], + "@fluentui/react-icons-mdl2-branded": ["./packages/react-icons-mdl2-branded/src/index.ts"], + "@fluentui/react-icon-provider": ["./packages/react-icon-provider/src/index.ts"], + "@fluentui/react-examples": ["./packages/react-examples/src/index.ts"], + "@fluentui/react-examples/lib/*": ["./packages/react-examples/src/*"], + "@fluentui/public-docsite-setup": ["./packages/public-docsite-setup/src/index.ts"], + "@fluentui/react-docsite-components": ["./packages/react-docsite-components/src/index.ts"], + "@fluentui/react-docsite-components/lib/index2": ["./packages/react-docsite-components/src/index2.ts"], "@fluentui/monaco-editor": [ - "packages/monaco-editor/esm/vs/editor/editor.api.d.ts", - "packages/monaco-editor/src/monacoBundle.ts" + "./packages/monaco-editor/esm/vs/editor/editor.api.d.ts", + "./packages/monaco-editor/src/monacoBundle.ts" ], - "@fluentui/monaco-editor/esm/*": ["packages/monaco-editor/esm/"], - "@fluentui/monaco-editor/lib/*": ["packages/monaco-editor/src/*"], - "@fluentui/react-monaco-editor": ["packages/react-monaco-editor/src/index.ts"], - "@fluentui/storybook": ["packages/storybook/src/index.ts"] + "@fluentui/monaco-editor/esm/*": ["./packages/monaco-editor/esm/"], + "@fluentui/monaco-editor/lib/*": ["./packages/monaco-editor/src/*"], + "@fluentui/react-monaco-editor": ["./packages/react-monaco-editor/src/index.ts"], + "@fluentui/storybook": ["./packages/storybook/src/index.ts"] } } } diff --git a/tsconfig.base.wc.json b/tsconfig.base.wc.json index 8884bb799a86f..413b990553e6d 100644 --- a/tsconfig.base.wc.json +++ b/tsconfig.base.wc.json @@ -3,6 +3,7 @@ "target": "ES2022", "useDefineForClassFields": false, "module": "ESNext", + "moduleResolution": "bundler", "esModuleInterop": true, "sourceMap": true, "strict": true, @@ -10,11 +11,10 @@ "pretty": true, "typeRoots": ["node_modules/@types", "./typings"], "rootDir": ".", - "baseUrl": ".", "paths": { - "@fluentui/chart-web-components": ["packages/charts/chart-web-components/src/index.ts"], - "@fluentui/web-components": ["packages/web-components/src/index.ts"], - "@fluentui/tokens": ["packages/tokens/src/index.ts"] + "@fluentui/chart-web-components": ["./packages/charts/chart-web-components/src/index.ts"], + "@fluentui/web-components": ["./packages/web-components/src/index.ts"], + "@fluentui/tokens": ["./packages/tokens/src/index.ts"] } } } diff --git a/typings/custom-global/index.d.ts b/typings/custom-global/index.d.ts index 6d73ebc6ac1e1..141a7d25c30f3 100644 --- a/typings/custom-global/index.d.ts +++ b/typings/custom-global/index.d.ts @@ -4,10 +4,16 @@ /** * Generic typings for sass files. + * + * Both a `default` export and named string members are declared so that either + * `import styles from './x.scss'` or `import * as styles from './x.scss'` resolves the + * class-name map. Under `moduleResolution: nodenext` (with the implied `esModuleInterop`), + * a namespace import of a default-only module exposes just `{ default }`, so the named + * index signature is required for the legacy `import * as styles` access pattern. */ declare module '*.scss' { const styles: { [className: string]: string }; - export default styles; + export = styles; } // These declarations are meant to represent the parts of Map/WeakMap/Set that exist in IE 11. diff --git a/typings/static-assets/index.d.ts b/typings/static-assets/index.d.ts index d73d3587940f8..b9b4974ac86f6 100644 --- a/typings/static-assets/index.d.ts +++ b/typings/static-assets/index.d.ts @@ -36,3 +36,14 @@ declare module '*.module.css' { const classes: { readonly [key: string]: string }; export default classes; } + +/** + * Plain stylesheets are only ever imported for their side effects; the bundler injects them. + * Declared so `noUncheckedSideEffectImports` (default since TypeScript 6) can resolve them. + * + * The empty module body is intentional: a shorthand ambient module (`declare module '*.css';` with no + * body) makes every export `any`, so a value/default/named import from a plain stylesheet would silently + * type-check. An explicit empty body still allows `import './foo.css';` for its side effect, but a + * value/default/named import fails to compile because the module declares no exports. + */ +declare module '*.css' {} diff --git a/yarn.lock b/yarn.lock index 5c15d3be64028..c26256ee32530 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1564,17 +1564,17 @@ resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz#c8e119a30a7c8d60b9d2e22d2073722dde3b710b" integrity sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ== -"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.7.0": - version "4.7.0" - resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz#607084630c6c033992a082de6e6fbc1a8b52175a" - integrity sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw== +"@eslint-community/eslint-utils@^4.1.2", "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0", "@eslint-community/eslint-utils@^4.9.1": + version "4.9.1" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz#4e90af67bc51ddee6cdef5284edf572ec376b595" + integrity sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== dependencies: eslint-visitor-keys "^3.4.3" -"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.11.0", "@eslint-community/regexpp@^4.12.1": - version "4.12.1" - resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz#cfc6cffe39df390a3841cde2abccf92eaa7ae0e0" - integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ== +"@eslint-community/regexpp@^4.11.0", "@eslint-community/regexpp@^4.12.1", "@eslint-community/regexpp@^4.12.2": + version "4.12.2" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz#bccdf615bcf7b6e8db830ec0b8d21c9a25de597b" + integrity sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== "@eslint/compat@1.3.0": version "1.3.0" @@ -3858,6 +3858,31 @@ "@storybook/global" "^5.0.0" "@storybook/react-dom-shim" "9.1.17" +"@swc-node/core@1.13.3", "@swc-node/core@^1.13.1": + version "1.13.3" + resolved "https://registry.yarnpkg.com/@swc-node/core/-/core-1.13.3.tgz#0821d01263f48314392d38d80ef1a03fef5f11b3" + integrity sha512-OGsvXIid2Go21kiNqeTIn79jcaX4l0G93X2rAnas4LFoDyA9wAwVK7xZdm+QsKoMn5Mus2yFLCc4OtX2dD/PWA== + +"@swc-node/register@1.9.2": + version "1.9.2" + resolved "https://registry.yarnpkg.com/@swc-node/register/-/register-1.9.2.tgz#314b86e32ed1f742d2e025d66f84c2f528082b70" + integrity sha512-BBjg0QNuEEmJSoU/++JOXhrjWdu3PTyYeJWsvchsI0Aqtj8ICkz/DqlwtXbmZVZ5vuDPpTfFlwDBZe81zgShMA== + dependencies: + "@swc-node/core" "^1.13.1" + "@swc-node/sourcemap-support" "^0.5.0" + colorette "^2.0.20" + debug "^4.3.4" + pirates "^4.0.6" + tslib "^2.6.2" + +"@swc-node/sourcemap-support@^0.5.0": + version "0.5.1" + resolved "https://registry.yarnpkg.com/@swc-node/sourcemap-support/-/sourcemap-support-0.5.1.tgz#0355540d62874891770ce1ba06838de186f098ff" + integrity sha512-JxIvIo/Hrpv0JCHSyRpetAdQ6lB27oFYhv0PKCNf1g2gUXOjpeR1exrXccRxLMuAV5WAmGFBwRnNOJqN38+qtg== + dependencies: + source-map-support "^0.5.21" + tslib "^2.6.3" + "@swc/cli@0.7.7": version "0.7.7" resolved "https://registry.yarnpkg.com/@swc/cli/-/cli-0.7.7.tgz#b367daba7db5a25fdcdbefe6a80f4c49c300d5fc" @@ -3947,12 +3972,12 @@ resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9" integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ== -"@swc/helpers@0.5.1", "@swc/helpers@^0.5.1", "@swc/helpers@~0.5.1": - version "0.5.1" - resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.1.tgz#e9031491aa3f26bfcc974a67f48bd456c8a5357a" - integrity sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg== +"@swc/helpers@0.5.23", "@swc/helpers@^0.5.23", "@swc/helpers@~0.5.1": + version "0.5.23" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.23.tgz#19287d0d86d962b111376039a50c792902c9a86a" + integrity sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== dependencies: - tslib "^2.4.0" + tslib "^2.8.0" "@swc/jest@0.2.38": version "0.2.38" @@ -5069,53 +5094,52 @@ dependencies: "@types/node" "*" -"@typescript-eslint/eslint-plugin@8.46.2", "@typescript-eslint/eslint-plugin@^8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz#dc4ab93ee3d7e6c8e38820a0d6c7c93c7183e2dc" - integrity sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w== - dependencies: - "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "8.46.2" - "@typescript-eslint/type-utils" "8.46.2" - "@typescript-eslint/utils" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" - graphemer "^1.4.0" - ignore "^7.0.0" +"@typescript-eslint/eslint-plugin@8.64.0", "@typescript-eslint/eslint-plugin@^8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.64.0.tgz#71a0c3d5f8a5e6c5dfdb4f0f04bd1bfb572d5e24" + integrity sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q== + dependencies: + "@eslint-community/regexpp" "^4.12.2" + "@typescript-eslint/scope-manager" "8.64.0" + "@typescript-eslint/type-utils" "8.64.0" + "@typescript-eslint/utils" "8.64.0" + "@typescript-eslint/visitor-keys" "8.64.0" + ignore "^7.0.5" natural-compare "^1.4.0" - ts-api-utils "^2.1.0" + ts-api-utils "^2.5.0" -"@typescript-eslint/parser@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.46.2.tgz#dd938d45d581ac8ffa9d8a418a50282b306f7ebf" - integrity sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g== +"@typescript-eslint/parser@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.64.0.tgz#c9864a1cc28a13ff29a7314fbdef0528bb122f72" + integrity sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw== dependencies: - "@typescript-eslint/scope-manager" "8.46.2" - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" - debug "^4.3.4" + "@typescript-eslint/scope-manager" "8.64.0" + "@typescript-eslint/types" "8.64.0" + "@typescript-eslint/typescript-estree" "8.64.0" + "@typescript-eslint/visitor-keys" "8.64.0" + debug "^4.4.3" -"@typescript-eslint/project-service@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.46.2.tgz#ab2f02a0de4da6a7eeb885af5e059be57819d608" - integrity sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg== +"@typescript-eslint/project-service@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/project-service/-/project-service-8.64.0.tgz#14c4e29390d7325a7f8a1218c2788fd649b85da6" + integrity sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg== dependencies: - "@typescript-eslint/tsconfig-utils" "^8.46.2" - "@typescript-eslint/types" "^8.46.2" - debug "^4.3.4" + "@typescript-eslint/tsconfig-utils" "^8.64.0" + "@typescript-eslint/types" "^8.64.0" + debug "^4.4.3" -"@typescript-eslint/rule-tester@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/rule-tester/-/rule-tester-8.46.2.tgz#270ef39f812475bfde31c16a92f5ecfabaedfc25" - integrity sha512-95F3U8JcJmQEvMyD/VH88c96EWTg3d5F7iIb7puZPowweIArCiVFHbnBJVXw7nhJGsCFMG6LavdMWkkJaOxBdw== +"@typescript-eslint/rule-tester@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/rule-tester/-/rule-tester-8.64.0.tgz#97e84611761b93feaa67b8fd876b6412504d5bdb" + integrity sha512-QmEBcU+JKvSx2DagEb/Nip7331IAQkH77QlEp5KNWs9/OvbhiUXvdf09k43dZtIRtAA0ahNDesQ+bfpjN/UsMA== dependencies: - "@typescript-eslint/parser" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" - "@typescript-eslint/utils" "8.46.2" + "@typescript-eslint/parser" "8.64.0" + "@typescript-eslint/typescript-estree" "8.64.0" + "@typescript-eslint/utils" "8.64.0" ajv "^6.12.6" json-stable-stringify-without-jsonify "^1.0.1" lodash.merge "4.6.2" - semver "^7.6.0" + semver "^7.7.3" "@typescript-eslint/scope-manager@7.18.0": version "7.18.0" @@ -5125,39 +5149,39 @@ "@typescript-eslint/types" "7.18.0" "@typescript-eslint/visitor-keys" "7.18.0" -"@typescript-eslint/scope-manager@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz#7d37df2493c404450589acb3b5d0c69cc0670a88" - integrity sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA== +"@typescript-eslint/scope-manager@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.64.0.tgz#d45f15304a94c85c39db317b717b158fb6259958" + integrity sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w== dependencies: - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" + "@typescript-eslint/types" "8.64.0" + "@typescript-eslint/visitor-keys" "8.64.0" -"@typescript-eslint/tsconfig-utils@8.46.2", "@typescript-eslint/tsconfig-utils@^8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz#d110451cb93bbd189865206ea37ef677c196828c" - integrity sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag== +"@typescript-eslint/tsconfig-utils@8.64.0", "@typescript-eslint/tsconfig-utils@^8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.64.0.tgz#c62ac8ea9173c3cac8b38b8e66e30a046b548851" + integrity sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw== -"@typescript-eslint/type-utils@8.46.2", "@typescript-eslint/type-utils@^8.0.0", "@typescript-eslint/type-utils@^8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz#802d027864e6fb752e65425ed09f3e089fb4d384" - integrity sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA== +"@typescript-eslint/type-utils@8.64.0", "@typescript-eslint/type-utils@^8.0.0", "@typescript-eslint/type-utils@^8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.64.0.tgz#106fa7d58cf9cf7758f3dd8e426ac8237eceacf3" + integrity sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg== dependencies: - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" - "@typescript-eslint/utils" "8.46.2" - debug "^4.3.4" - ts-api-utils "^2.1.0" + "@typescript-eslint/types" "8.64.0" + "@typescript-eslint/typescript-estree" "8.64.0" + "@typescript-eslint/utils" "8.64.0" + debug "^4.4.3" + ts-api-utils "^2.5.0" "@typescript-eslint/types@7.18.0": version "7.18.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== -"@typescript-eslint/types@8.46.2", "@typescript-eslint/types@^8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.46.2.tgz#2bad7348511b31e6e42579820e62b73145635763" - integrity sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ== +"@typescript-eslint/types@8.64.0", "@typescript-eslint/types@^8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.64.0.tgz#b41f8ef5dd40616908658b991197a9d486cda60b" + integrity sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA== "@typescript-eslint/typescript-estree@7.18.0": version "7.18.0" @@ -5173,31 +5197,30 @@ semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/typescript-estree@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz#ab547a27e4222bb6a3281cb7e98705272e2c7d08" - integrity sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ== +"@typescript-eslint/typescript-estree@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.64.0.tgz#b8d51255e2d726eb4bd80d397a4fb4170c02eecc" + integrity sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA== dependencies: - "@typescript-eslint/project-service" "8.46.2" - "@typescript-eslint/tsconfig-utils" "8.46.2" - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/visitor-keys" "8.46.2" - debug "^4.3.4" - fast-glob "^3.3.2" - is-glob "^4.0.3" - minimatch "^9.0.4" - semver "^7.6.0" - ts-api-utils "^2.1.0" + "@typescript-eslint/project-service" "8.64.0" + "@typescript-eslint/tsconfig-utils" "8.64.0" + "@typescript-eslint/types" "8.64.0" + "@typescript-eslint/visitor-keys" "8.64.0" + debug "^4.4.3" + minimatch "^10.2.2" + semver "^7.7.3" + tinyglobby "^0.2.15" + ts-api-utils "^2.5.0" -"@typescript-eslint/utils@8.46.2", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.46.2.tgz#b313d33d67f9918583af205bd7bcebf20f231732" - integrity sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg== +"@typescript-eslint/utils@8.64.0", "@typescript-eslint/utils@^6.0.0 || ^7.0.0 || ^8.0.0", "@typescript-eslint/utils@^8.0.0", "@typescript-eslint/utils@^8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.64.0.tgz#98bb2010cfb754b41985b9c93e6e8b3dcd7bd600" + integrity sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ== dependencies: - "@eslint-community/eslint-utils" "^4.7.0" - "@typescript-eslint/scope-manager" "8.46.2" - "@typescript-eslint/types" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" + "@eslint-community/eslint-utils" "^4.9.1" + "@typescript-eslint/scope-manager" "8.64.0" + "@typescript-eslint/types" "8.64.0" + "@typescript-eslint/typescript-estree" "8.64.0" "@typescript-eslint/utils@^7.18.0": version "7.18.0" @@ -5217,13 +5240,13 @@ "@typescript-eslint/types" "7.18.0" eslint-visitor-keys "^3.4.3" -"@typescript-eslint/visitor-keys@8.46.2": - version "8.46.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz#803fa298948c39acf810af21bdce6f8babfa9738" - integrity sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w== +"@typescript-eslint/visitor-keys@8.64.0": + version "8.64.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.64.0.tgz#7a08421d10e54960733352cd7c95fab1784e8473" + integrity sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw== dependencies: - "@typescript-eslint/types" "8.46.2" - eslint-visitor-keys "^4.2.1" + "@typescript-eslint/types" "8.64.0" + eslint-visitor-keys "^5.0.0" "@uifabric/set-version@^7.0.23": version "7.0.23" @@ -7687,7 +7710,7 @@ colorette@^1.2.1: resolved "https://registry.yarnpkg.com/colorette/-/colorette-1.2.2.tgz#cbcc79d5e99caea2dbf10eb3a26fd8b3e6acfa94" integrity sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w== -colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16: +colorette@^2.0.10, colorette@^2.0.14, colorette@^2.0.16, colorette@^2.0.20: version "2.0.20" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== @@ -9778,6 +9801,11 @@ eslint-visitor-keys@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz#4cfea60fe7dd0ad8e816e1ed026c1d5251b512c1" integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== +eslint-visitor-keys@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz#9e3c9489697824d2d4ce3a8ad12628f91e9f59be" + integrity sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== + eslint@9.31.0: version "9.31.0" resolved "https://registry.yarnpkg.com/eslint/-/eslint-9.31.0.tgz#9a488e6da75bbe05785cd62e43c5ea99356d21ba" @@ -10310,10 +10338,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -fdir@^6.4.4: - version "6.4.4" - resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" - integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== +fdir@^6.4.4, fdir@^6.5.0: + version "6.5.0" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" + integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== fela-dom@^11.7.0: version "11.7.0" @@ -11323,11 +11351,6 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== -graphemer@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" - integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== - gzip-size@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-6.0.0.tgz#065367fd50c239c0671cbcbad5be3e2eeb10e462" @@ -11345,7 +11368,7 @@ handle-thing@^2.0.0: resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@*, handlebars@^4.4.3, handlebars@^4.7.8, handlebars@^4.7.9: +handlebars@*, handlebars@^4.4.3, handlebars@^4.7.9: version "4.7.9" resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.7.9.tgz#6f139082ab58dc4e5a0e51efe7db5ae890d56a0f" integrity sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ== @@ -11848,10 +11871,10 @@ ignore@^5.0.4, ignore@^5.1.1, ignore@^5.1.4, ignore@^5.2.0, ignore@^5.2.4: resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== -ignore@^7.0.0: - version "7.0.5" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.5.tgz#4cb5f6cd7d4c7ab0365738c7aea888baa6d7efd9" - integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg== +ignore@^7.0.5: + version "7.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-7.0.6.tgz#6a57aaef4c90df27ac3590875d29e8f11988c88e" + integrity sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw== immediate@~3.0.5: version "3.0.6" @@ -15211,7 +15234,7 @@ minimatch@9.0.1, minimatch@9.0.3, minimatch@^9.0.3, minimatch@^9.0.4, minimatch@ dependencies: brace-expansion "^2.0.2" -minimatch@^10.1.1: +minimatch@^10.1.1, minimatch@^10.2.2: version "10.2.5" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.5.tgz#bd48687a0be38ed2961399105600f832095861d1" integrity sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg== @@ -16511,10 +16534,10 @@ picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -picomatch@^4.0.2, picomatch@^4.0.3: - version "4.0.4" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.4.tgz#fd6f5e00a143086e074dffe4c924b8fb293b0589" - integrity sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A== +picomatch@^4.0.2, picomatch@^4.0.3, picomatch@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.5.tgz#51ea57a17d86f605f81039595fbc40ed06a55fab" + integrity sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== pify@^2.0.0, pify@^2.2.0: version "2.3.0" @@ -18108,10 +18131,10 @@ semver@^6.0.0, semver@^6.2.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4: - version "7.7.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a" - integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA== +semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4, semver@^7.6.0, semver@^7.6.2, semver@^7.6.3, semver@^7.7.1, semver@^7.7.2, semver@^7.7.3, semver@^7.7.4, semver@^7.8.0: + version "7.8.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.8.5.tgz#39b646037dd50c14fb451e7e4cac58ed8b863f69" + integrity sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== semver@~7.3.0: version "7.3.8" @@ -18507,7 +18530,7 @@ source-map-support@0.5.19: buffer-from "^1.0.0" source-map "^0.6.0" -source-map-support@^0.5.16, source-map-support@~0.5.12, source-map-support@~0.5.20: +source-map-support@^0.5.16, source-map-support@^0.5.21, source-map-support@~0.5.12, source-map-support@~0.5.20: version "0.5.21" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== @@ -19372,13 +19395,13 @@ tiny-invariant@^1.3.3: resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== -tinyglobby@^0.2.12, tinyglobby@^0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" - integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== +tinyglobby@^0.2.12, tinyglobby@^0.2.13, tinyglobby@^0.2.15: + version "0.2.17" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.17.tgz#562a9a6c9eb2b3b123d39719f9af5bb44fcd7631" + integrity sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== dependencies: - fdir "^6.4.4" - picomatch "^4.0.2" + fdir "^6.5.0" + picomatch "^4.0.4" tinyrainbow@^2.0.0: version "2.0.0" @@ -19576,10 +19599,10 @@ ts-api-utils@^1.3.0: resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== -ts-api-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.1.0.tgz#595f7094e46eed364c13fd23e75f9513d29baf91" - integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ== +ts-api-utils@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz#4acd4a155e22734990a5ed1fe9e97f113bcb37c1" + integrity sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== ts-dedent@^2.0.0: version "2.2.0" @@ -19593,18 +19616,18 @@ ts-invariant@^0.10.3: dependencies: tslib "^2.1.0" -ts-jest@29.4.5: - version "29.4.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.5.tgz#a6b0dc401e521515d5342234be87f1ca96390a6f" - integrity sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q== +ts-jest@29.4.11: + version "29.4.11" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.4.11.tgz#42f5de21c37ccc01a580253afae6955abbf4d0b3" + integrity sha512-IrFl7l9AuB/qrNw5quqvAv/hmKMb8dhWOH4jQOGo0Oq8tCeo1O86/iTFG1FaRimgUkF13l4PcepO8ATFT6Ns4g== dependencies: bs-logger "^0.2.6" fast-json-stable-stringify "^2.1.0" - handlebars "^4.7.8" + handlebars "^4.7.9" json5 "^2.2.3" lodash.memoize "^4.1.2" make-error "^1.3.6" - semver "^7.7.3" + semver "^7.8.0" type-fest "^4.41.0" yargs-parser "^21.1.1" @@ -19659,7 +19682,7 @@ tsconfig-paths-webpack-plugin@4.1.0: enhanced-resolve "^5.7.0" tsconfig-paths "^4.1.2" -tsconfig-paths@4.2.0, tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: +tsconfig-paths@^4.1.2, tsconfig-paths@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz#ef78e19039133446d244beac0fd6a1632e2d107c" integrity sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg== @@ -19678,7 +19701,7 @@ tsconfig-paths@^3.15.0: minimist "^1.2.6" strip-bom "^3.0.0" -tslib@2.8.1, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.8.1: +tslib@2.8.1, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.2, tslib@^2.6.3, tslib@^2.8.0, tslib@^2.8.1: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -19832,21 +19855,26 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-eslint@8.46.2, typescript-eslint@^8.0.0, typescript-eslint@^8.46.2: - version "8.46.2" - resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.46.2.tgz#da1adec683ba93a1b6c3850a4efb0922ffbc627d" - integrity sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg== +typescript-eslint@8.64.0, typescript-eslint@^8.0.0, typescript-eslint@^8.64.0: + version "8.64.0" + resolved "https://registry.yarnpkg.com/typescript-eslint/-/typescript-eslint-8.64.0.tgz#4984dae4de9dc8bf892acf5c394d0a2a5f08c3e1" + integrity sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ== dependencies: - "@typescript-eslint/eslint-plugin" "8.46.2" - "@typescript-eslint/parser" "8.46.2" - "@typescript-eslint/typescript-estree" "8.46.2" - "@typescript-eslint/utils" "8.46.2" + "@typescript-eslint/eslint-plugin" "8.64.0" + "@typescript-eslint/parser" "8.64.0" + "@typescript-eslint/typescript-estree" "8.64.0" + "@typescript-eslint/utils" "8.64.0" typescript@5.7.3: version "5.7.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e" integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== +typescript@6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-6.0.3.tgz#90251dc007916e972786cb94d74d15b185577d21" + integrity sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw== + typescript@~5.4.2: version "5.4.5" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.5.tgz#42ccef2c571fdbd0f6718b1d1f5e6e5ef006f611"