From b44da0c5a18bdb25c9bbf00a52f1af0db50f1ea2 Mon Sep 17 00:00:00 2001 From: Paul Gschwendtner Date: Fri, 21 Feb 2025 19:25:01 +0100 Subject: [PATCH] build: update dev-infra and rework windows native testing As part of go/ng:windows-dev-future, we are changing how our infrastructure supports Windows build & testing. Clearly: - we will still support contributors on Windows, and we believe we will be improving and streamlining the experience here - we will continue testing the Angular CLI for our Windows users. We are aware of the many Windows users using the `ng` CLI. What is changing? We are no longer actively working towards a Bazel infrastructure that supports native Windows building and testing. There are currently two ways to contribute to Angular on Windows. That is via WSL, or via e.g. native Windows cmd.exe, with Git Bash on top. We acknowledge that the latter worked sometimes, but we also realize it very often breaks as nobody on our team uses, verifies it, and it introduces extra complexity because Bazel on Windows is quite disconnected from Linux/Mac (e.g. no sandboxing). Going forward, to improve our team's effectiveness, and improve our stability guarantees for Windows (and Windows contributors), we are actively discouraging the use of Git Bash for contributing to Angular; but instead ask for WSL to be used. I can speak as one of the few long-term team members that have worked on Windows (without WSL) most of my time, that WSL is great and the contributing experience is much smoother and also easier to "guide". It's a positive change because we won't be suggesting "two ways to contribute on Windows", where in reality one is very brittle and can break at any time! --- For testing of the Angular CLI: We will continue to maintain the capability to cross-compile via Bazel with Windows as the target platform. This allows us to build the e2e tests for Windows, and run them natively outside WSL to ensure native Windows `ng` CLI testing! This is what this change mostly does. Notably, two things are missing here and will be followed up: - caching of the e2e tests on Windows is not properly functioning yet. - caching of the WSL node modules + nvm is not working properly yet. Other than that, we are seeing very similar timing and results of the Windows tests, so this change unblocks our `rules_js` migration. --- .../npm_translate_lock_MzA5NzUwNzMx | 8 +- .bazelrc | 14 +- .../windows-bazel-test/action.yml | 79 ++++++++ .github/workflows/ci.yml | 36 ++-- .github/workflows/pr.yml | 24 +-- WORKSPACE | 36 ++-- goldens/BUILD.bazel | 4 +- package.json | 6 +- packages/angular/build/BUILD.bazel | 6 +- packages/angular/ssr/BUILD.bazel | 6 +- packages/angular/ssr/package.json | 1 - .../angular/ssr/test/npm_package/BUILD.bazel | 1 - .../ssr/test/npm_package/package_spec.ts | 7 +- packages/angular_devkit/architect/BUILD.bazel | 6 +- .../angular_devkit/build_angular/BUILD.bazel | 6 +- .../angular_devkit/build_webpack/BUILD.bazel | 6 +- packages/angular_devkit/core/BUILD.bazel | 10 +- .../angular_devkit/schematics/BUILD.bazel | 10 +- packages/ngtools/webpack/BUILD.bazel | 6 +- pnpm-lock.yaml | 38 ++-- scripts/windows-testing/convert-symlinks.mjs | 158 ++++++++++++++++ scripts/windows-testing/parallel-executor.mjs | 178 ++++++++++++++++++ tests/legacy-cli/BUILD.bazel | 59 ++++-- tests/legacy-cli/e2e.bzl | 25 +-- .../legacy-cli/e2e/setup/010-local-publish.ts | 3 +- tests/legacy-cli/e2e/tests/BUILD.bazel | 3 - tests/legacy-cli/e2e/tests/build/auto-csp.ts | 1 + .../express-engine-csp-nonce.ts | 1 + .../express-engine-ngmodule.ts | 1 + .../express-engine-standalone.ts | 1 + ...outes-output-mode-server-i18n-base-href.ts | 1 + ...routes-output-mode-server-i18n-sub-path.ts | 1 + .../server-routes-output-mode-server-i18n.ts | 1 + .../server-routes-output-mode-server.ts | 1 + .../server-routes-preload-links.ts | 1 + .../e2e/tests/build/styles/tailwind-v3.ts | 6 +- .../commands/analytics/analytics-info.ts | 3 + .../misc/invalid-schematic-dependencies.ts | 9 +- .../vite/reuse-dep-optimization-cache.ts | 15 +- .../tests/vite/ssr-new-dep-optimization.ts | 2 +- .../vite/ssr-no-server-entry-sub-path.ts | 1 + tests/legacy-cli/e2e/utils/BUILD.bazel | 2 + tests/legacy-cli/e2e/utils/assets.ts | 2 +- tests/legacy-cli/e2e/utils/process.ts | 29 ++- tests/legacy-cli/e2e/utils/registry.ts | 2 +- tests/legacy-cli/e2e/utils/test_process.ts | 4 +- tests/legacy-cli/e2e_runner.ts | 17 +- tests/legacy-cli/rollup.config.mjs | 45 +++++ tools/BUILD.bazel | 21 +-- tools/interop.bzl | 13 +- tools/rules_ts_windows.patch | 30 --- tools/tar_system.bat | 7 - yarn.lock | 26 +-- 53 files changed, 742 insertions(+), 237 deletions(-) create mode 100644 .github/shared-actions/windows-bazel-test/action.yml create mode 100644 scripts/windows-testing/convert-symlinks.mjs create mode 100644 scripts/windows-testing/parallel-executor.mjs create mode 100644 tests/legacy-cli/rollup.config.mjs delete mode 100644 tools/rules_ts_windows.patch delete mode 100755 tools/tar_system.bat diff --git a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx index d72d212ddfb0..eda2684114ae 100755 --- a/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx +++ b/.aspect/rules/external_repository_action_cache/npm_translate_lock_MzA5NzUwNzMx @@ -3,11 +3,11 @@ # This file should be checked into version control along with the pnpm-lock.yaml file. .npmrc=-1406867100 modules/testing/builder/package.json=973445093 -package.json=1411918173 +package.json=-462103860 packages/angular/build/package.json=1920607808 packages/angular/cli/package.json=-1917515334 packages/angular/pwa/package.json=1108903917 -packages/angular/ssr/package.json=-2027233365 +packages/angular/ssr/package.json=1556449772 packages/angular_devkit/architect/package.json=-363443363 packages/angular_devkit/architect_cli/package.json=1551210941 packages/angular_devkit/build_angular/package.json=595549079 @@ -17,7 +17,7 @@ packages/angular_devkit/schematics/package.json=-1133510866 packages/angular_devkit/schematics_cli/package.json=-2026655035 packages/ngtools/webpack/package.json=884391309 packages/schematics/angular/package.json=251715148 -pnpm-lock.yaml=2112966384 +pnpm-lock.yaml=-758853739 pnpm-workspace.yaml=-1264044456 tests/package.json=700948366 -yarn.lock=1484073494 +yarn.lock=1188157 diff --git a/.bazelrc b/.bazelrc index 6944b4807a3d..da0450325967 100644 --- a/.bazelrc +++ b/.bazelrc @@ -33,10 +33,6 @@ test:no-sharding --flaky_test_attempts=1 --test_sharding_strategy=disabled # See https://github.com/bazelbuild/bazel/issues/4603 build --symlink_prefix=dist/ -# Disable watchfs as it causes tests to be flaky on Windows -# https://github.com/angular/angular/issues/29541 -build --nowatchfs - # Turn off legacy external runfiles build --nolegacy_external_runfiles @@ -133,9 +129,9 @@ build:remote --jobs=150 # Setup the toolchain and platform for the remote build execution. The platform # is provided by the shared dev-infra package and targets k8 remote containers. -build:remote --extra_execution_platforms=@npm//@angular/build-tooling/bazel/remote-execution:platform_with_network -build:remote --host_platform=@npm//@angular/build-tooling/bazel/remote-execution:platform_with_network -build:remote --platforms=@npm//@angular/build-tooling/bazel/remote-execution:platform_with_network +build:remote --extra_execution_platforms=@devinfra//bazel/remote-execution:platform_with_network +build:remote --host_platform=@devinfra//bazel/remote-execution:platform_with_network +build:remote --platforms=@devinfra//bazel/remote-execution:platform_with_network # Set remote caching settings build:remote --remote_accept_cached=true @@ -162,10 +158,6 @@ build:remote-cache --google_default_credentials # Fixes use of npm paths with spaces such as some within the puppeteer module build --experimental_inprocess_symlink_creation -# Enable runfiles even on Windows. -# Architect resolves output files from data files, and this isn't possible without runfile support. -build --enable_runfiles - #################################################### # rules_js specific flags #################################################### diff --git a/.github/shared-actions/windows-bazel-test/action.yml b/.github/shared-actions/windows-bazel-test/action.yml new file mode 100644 index 000000000000..a0bc927297c4 --- /dev/null +++ b/.github/shared-actions/windows-bazel-test/action.yml @@ -0,0 +1,79 @@ +name: 'Native Windows Bazel e2e test' +description: 'Runs an Angular CLI e2e Bazel test on native Windows (dispatched from inside WSL)' +author: 'Angular' + +inputs: + test_target_name: + description: E2E test target name + required: true + test_args: + description: | + Text representing the command line arguments that + should be passed to the e2e test runner. + required: false + default: '' + +runs: + using: composite + steps: + - name: Setup Bazel RBE + uses: angular/dev-infra/github-actions/bazel/configure-remote@2667d139a421977a40c3ea7ec768609fb19a8b9d + with: + allow_windows_rbe: true + + - name: Initialize WSL + id: init_wsl + uses: angular/dev-infra/github-actions/setup-wsl@9a3e28a515bf51cd2ecfd5f4d5b17613845e6f44 + with: + wsl_firewall_interface: 'vEthernet (WSL (Hyper-V firewall))' + + - name: Install node modules in WSL (re-using from previous install/cache restore) + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + yarn install --immutable + shell: wsl-bash {0} + + - name: Build test binary for Windows (inside WSL) + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + yarn bazel \ + build --config=e2e //tests/legacy-cli:${{inputs.test_target_name}} --platforms=tools:windows_x64 + env: + # See: https://devblogs.microsoft.com/commandline/share-environment-vars-between-wsl-and-windows + WSLENV: 'GOOGLE_APPLICATION_CREDENTIALS/p' + + - name: Copying binary artifact to host + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + tar -cf /tmp/test.tar.gz dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_ + mkdir /mnt/c/test + mv /tmp/test.tar.gz /mnt/c/test + (cd /mnt/c/test && tar -xf /mnt/c/test/test.tar.gz) + + - name: Convert symlinks for Windows host + shell: wsl-bash {0} + run: | + cd ${{steps.init_wsl.outputs.repo_path}} + + runfiles_dir="/mnt/c/test/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles" + + # Make WSL symlinks compatible on Windows native file system. + node scripts/windows-testing/convert-symlinks.mjs $runfiles_dir "${{steps.init_wsl.outputs.cmd_path}}" + + # Needed for resolution because Aspect/Bazel looks for repositories at `/external`. + # TODO(devversion): consult with Aspect on why this is needed. + (cd $runfiles_dir/angular_cli && ${{steps.init_wsl.outputs.cmd_path}} /C "mklink /D external ..") + + - name: Run tests + # Note: This is Git Bash. + shell: bash + env: + BAZEL_BINDIR: '.' + working-directory: "C:\\test" + run: | + node "${{github.workspace}}\\scripts\\windows-testing\\parallel-executor.mjs" \ + $PWD/dist/bin/tests/legacy-cli/${{inputs.test_target_name}}_/${{inputs.test_target_name}}.bat.runfiles \ + ${{inputs.test_target_name}} \ + "${{inputs.test_args}}" \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b2b9c345bb9e..91fad7bcdd51 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,20 +74,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] node: [20, 22] subset: [npm, esbuild] shard: [0, 1, 2, 3, 4, 5] - exclude: - # Skip Node.js v20 tests on Windows - - os: windows-latest - node: 20 runs-on: ${{ matrix.os }} steps: - # Workaround for: https://github.com/bazel-contrib/bazel-lib/issues/968. - # TODO(devversion): Remove when Aspect lib issue is fixed. - - run: choco install gzip - if: ${{matrix.os == 'windows-latest'}} - name: Initialize environment uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Install node modules @@ -97,7 +89,27 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=6 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + + e2e_windows: + strategy: + fail-fast: false + matrix: + os: [windows-2025] + node: [22] + subset: [npm, esbuild] + shard: [0, 1, 2, 3, 4, 5] + runs-on: ${{ matrix.os }} + steps: + - name: Initialize environment + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0000d926624b2fd918e93f1c6b5e2defba9af91f + - name: Run CLI E2E tests + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e.${{ matrix.subset }}_node${{ matrix.node }} + env: + E2E_SHARD_TOTAL: 6 + E2E_SHARD_INDEX: ${{ matrix.shard }} e2e-package-managers: needs: test @@ -119,7 +131,7 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=3 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} e2e-snapshots: needs: test @@ -141,7 +153,7 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=6 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} browsers: needs: build diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index b967cff7b7c1..436c32b47b37 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -123,25 +123,19 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=6 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} e2e-windows-subset: needs: build - runs-on: windows-latest + runs-on: windows-2025 steps: - # Workaround for: https://github.com/bazel-contrib/bazel-lib/issues/968. - # TODO(devversion): Remove when Aspect lib issue is fixed. - - run: choco install gzip - name: Initialize environment - uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@836bdd0543d15904c469f5a0ce869d30a8029971 - - name: Install node modules - run: yarn install --immutable - - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@836bdd0543d15904c469f5a0ce869d30a8029971 - - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 + uses: angular/dev-infra/github-actions/npm/checkout-and-setup-node@0000d926624b2fd918e93f1c6b5e2defba9af91f - name: Run CLI E2E tests - run: yarn bazel test --config=e2e //tests/legacy-cli:e2e_node22 --test_filter="tests/basic/{build,rebuild}.ts" --test_arg="--esbuild" + uses: ./.github/shared-actions/windows-bazel-test + with: + test_target_name: e2e_node22 + test_args: --esbuild --glob "tests/basic/{build,rebuild}.ts" e2e-package-managers: needs: build @@ -163,7 +157,7 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=3 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=3 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.${{ matrix.subset }}_node${{ matrix.node }} e2e-snapshots: needs: [analyze, build] @@ -186,4 +180,4 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@836bdd0543d15904c469f5a0ce869d30a8029971 - name: Run CLI E2E tests - run: yarn bazel test --define=E2E_SHARD_TOTAL=6 --define=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} + run: yarn bazel test --test_env=E2E_SHARD_TOTAL=6 --test_env=E2E_SHARD_INDEX=${{ matrix.shard }} --config=e2e //tests/legacy-cli:e2e.snapshots.${{ matrix.subset }}_node${{ matrix.node }} diff --git a/WORKSPACE b/WORKSPACE index 1f58027380e1..5637bd00cb06 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -2,12 +2,6 @@ workspace(name = "angular_cli") DEFAULT_NODE_VERSION = "20.11.1" -# Workaround for: https://github.com/bazel-contrib/bazel-lib/issues/968. -# Override toolchain for tar on windows. -register_toolchains( - "//tools:windows_tar_system_toolchain", -) - load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_file") http_archive( @@ -145,17 +139,6 @@ aspect_bazel_lib_dependencies() aspect_bazel_lib_register_toolchains() -register_toolchains( - "@npm//@angular/build-tooling/bazel/git-toolchain:git_linux_toolchain", - "@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_x86_toolchain", - "@npm//@angular/build-tooling/bazel/git-toolchain:git_macos_arm64_toolchain", - "@npm//@angular/build-tooling/bazel/git-toolchain:git_windows_toolchain", -) - -load("@npm//@angular/build-tooling/bazel/browsers:browser_repositories.bzl", "browser_repositories") - -browser_repositories() - load("@build_bazel_rules_nodejs//toolchains/esbuild:esbuild_repositories.bzl", "esbuild_repositories") esbuild_repositories( @@ -205,6 +188,10 @@ npm_translate_lock( # for `rules_nodejs` dependencies :) }, pnpm_lock = "//:pnpm-lock.yaml", + public_hoist_packages = { + # TODO: Remove when https://github.com/verdaccio/verdaccio/commit/bf0e09a509e8e0a74167b0307d129202bc3f40d2 is available. + "@verdaccio/config": [""], + }, update_pnpm_lock = True, verify_node_modules_ignored = "//:.bazelignore", yarn_lock = "//:yarn.lock", @@ -216,8 +203,6 @@ npm_repositories() http_archive( name = "aspect_rules_ts", - patch_args = ["-p1"], - patches = ["//tools:rules_ts_windows.patch"], sha256 = "4263532b2fb4d16f309d80e3597191a1cb2fb69c19e95d91711bd6b97874705e", strip_prefix = "rules_ts-3.5.0", url = "https://github.com/aspect-build/rules_ts/releases/download/v3.5.0/rules_ts-v3.5.0.tar.gz", @@ -253,7 +238,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "devinfra", - commit = "0ad6a370f70638e785d6ef1f90dc6ede34684a47", + commit = "bf0dd632ed129ee8770b09a6e11c6497162b3edb", remote = "https://github.com/angular/dev-infra.git", ) @@ -264,3 +249,14 @@ setup_dependencies_1() load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2") setup_dependencies_2() + +load("@devinfra//bazel/browsers:browser_repositories.bzl", "browser_repositories") + +browser_repositories() + +register_toolchains( + "@devinfra//bazel/git-toolchain:git_linux_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_x86_toolchain", + "@devinfra//bazel/git-toolchain:git_macos_arm64_toolchain", + "@devinfra//bazel/git-toolchain:git_windows_toolchain", +) diff --git a/goldens/BUILD.bazel b/goldens/BUILD.bazel index 3b3283026537..6dbbdd28f25b 100644 --- a/goldens/BUILD.bazel +++ b/goldens/BUILD.bazel @@ -1,6 +1,8 @@ +load("@aspect_bazel_lib//lib:copy_to_bin.bzl", "copy_to_bin") + package(default_visibility = ["//visibility:public"]) -filegroup( +copy_to_bin( name = "public-api", srcs = glob([ "public-api/**/*.md", diff --git a/package.json b/package.json index 5c2aaf6582cc..20a0210b8057 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "devDependencies": { "@ampproject/remapping": "2.3.0", "@angular/animations": "19.2.0", - "@angular/bazel": "https://github.com/angular/bazel-builds.git#8faa06d66416ce78073ab59539ff03f5253b8d52", - "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592", + "@angular/bazel": "https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65", + "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1", "@angular/cdk": "19.2.1", "@angular/common": "19.2.0", "@angular/compiler": "19.2.0", @@ -72,7 +72,6 @@ "@babel/runtime": "7.26.9", "@bazel/bazelisk": "1.25.0", "@bazel/buildifier": "8.0.3", - "@bazel/runfiles": "^6.0.0", "@discoveryjs/json-ext": "0.6.3", "@eslint/compat": "1.2.7", "@eslint/eslintrc": "3.3.0", @@ -82,6 +81,7 @@ "@listr2/prompt-adapter-inquirer": "2.0.18", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.0", + "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^13.0.5", "@stylistic/eslint-plugin": "^4.0.0", "@types/babel__core": "7.20.5", diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index 77677355c546..2be247eb62e7 100644 --- a/packages/angular/build/BUILD.bazel +++ b/packages/angular/build/BUILD.bazel @@ -1,4 +1,4 @@ -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") @@ -309,6 +309,6 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular/build", - npm_package = "angular_cli/packages/angular/build/npm_package", + golden_dir = "goldens/public-api/angular/build", + npm_package = "packages/angular/build/npm_package", ) diff --git a/packages/angular/ssr/BUILD.bazel b/packages/angular/ssr/BUILD.bazel index f16b21afea70..fdd727079ae7 100644 --- a/packages/angular/ssr/BUILD.bazel +++ b/packages/angular/ssr/BUILD.bazel @@ -1,5 +1,5 @@ load("@aspect_rules_js//npm:defs.bzl", "npm_package") -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("@rules_pkg//:pkg.bzl", "pkg_tar") load("//tools:defaults2.bzl", "ng_package", "ts_project") @@ -90,6 +90,6 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular/ssr", - npm_package = "angular_cli/packages/angular/ssr/npm_package", + golden_dir = "goldens/public-api/angular/ssr", + npm_package = "packages/angular/ssr/npm_package", ) diff --git a/packages/angular/ssr/package.json b/packages/angular/ssr/package.json index a961859ad100..baf26214afd1 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -35,7 +35,6 @@ "@angular/platform-browser": "19.2.0", "@angular/platform-server": "19.2.0", "@angular/router": "19.2.0", - "@bazel/runfiles": "^6.0.0", "@schematics/angular": "workspace:*" }, "sideEffects": false, diff --git a/packages/angular/ssr/test/npm_package/BUILD.bazel b/packages/angular/ssr/test/npm_package/BUILD.bazel index 6f5964521dcf..0fcfd9a87ba6 100644 --- a/packages/angular/ssr/test/npm_package/BUILD.bazel +++ b/packages/angular/ssr/test/npm_package/BUILD.bazel @@ -7,7 +7,6 @@ ts_project( testonly = True, srcs = glob(["**/*.ts"]), deps = [ - "//:node_modules/@bazel/runfiles", "//:node_modules/@types/node", ], ) diff --git a/packages/angular/ssr/test/npm_package/package_spec.ts b/packages/angular/ssr/test/npm_package/package_spec.ts index 39580cce2013..2bd37aeabf5b 100644 --- a/packages/angular/ssr/test/npm_package/package_spec.ts +++ b/packages/angular/ssr/test/npm_package/package_spec.ts @@ -6,17 +6,14 @@ * found in the LICENSE file at https://angular.dev/license */ -import { runfiles } from '@bazel/runfiles'; import { existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; -import { dirname, join } from 'node:path'; +import { join, resolve } from 'node:path'; /** * Resolve paths for the Beasties license file and the golden reference file. */ -const ANGULAR_SSR_PACKAGE_PATH = dirname( - runfiles.resolve('angular_cli/packages/angular/ssr/npm_package/package.json'), -); +const ANGULAR_SSR_PACKAGE_PATH = resolve('../../npm_package'); /** * Path to the actual license file for the Beasties library. diff --git a/packages/angular_devkit/architect/BUILD.bazel b/packages/angular_devkit/architect/BUILD.bazel index 276e649b337f..98a15714bff7 100644 --- a/packages/angular_devkit/architect/BUILD.bazel +++ b/packages/angular_devkit/architect/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") @@ -122,7 +122,7 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular_devkit/architect", - npm_package = "angular_cli/packages/angular_devkit/architect/npm_package", + golden_dir = "goldens/public-api/angular_devkit/architect", + npm_package = "packages/angular_devkit/architect/npm_package", ) # @external_end diff --git a/packages/angular_devkit/build_angular/BUILD.bazel b/packages/angular_devkit/build_angular/BUILD.bazel index c86f2a1fe929..80245e6f842e 100644 --- a/packages/angular_devkit/build_angular/BUILD.bazel +++ b/packages/angular_devkit/build_angular/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "copy_to_bin", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") @@ -275,8 +275,8 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular_devkit/build_angular", - npm_package = "angular_cli/packages/angular_devkit/build_angular/npm_package", + golden_dir = "goldens/public-api/angular_devkit/build_angular", + npm_package = "packages/angular_devkit/build_angular/npm_package", ) # Large build_angular specs diff --git a/packages/angular_devkit/build_webpack/BUILD.bazel b/packages/angular_devkit/build_webpack/BUILD.bazel index 5e198c7ba883..a02d2feba31b 100644 --- a/packages/angular_devkit/build_webpack/BUILD.bazel +++ b/packages/angular_devkit/build_webpack/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "jasmine_test", "npm_package", "ts_project") load("//tools:ts_json_schema.bzl", "ts_json_schema") @@ -120,6 +120,6 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular_devkit/build_webpack", - npm_package = "angular_cli/packages/angular_devkit/build_webpack/npm_package", + golden_dir = "goldens/public-api/angular_devkit/build_webpack", + npm_package = "packages/angular_devkit/build_webpack/npm_package", ) diff --git a/packages/angular_devkit/core/BUILD.bazel b/packages/angular_devkit/core/BUILD.bazel index c71a9c84e0b4..528dabcffe82 100644 --- a/packages/angular_devkit/core/BUILD.bazel +++ b/packages/angular_devkit/core/BUILD.bazel @@ -1,4 +1,4 @@ -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("//tools:defaults2.bzl", "jasmine_test", "npm_package", "ts_project") # Copyright Google Inc. All Rights Reserved. @@ -90,8 +90,10 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular_devkit/core", - npm_package = "angular_cli/packages/angular_devkit/core/npm_package", - types = ["@npm//@types/node"], + golden_dir = "goldens/public-api/angular_devkit/core", + npm_package = "packages/angular_devkit/core/npm_package", + types = { + "//:node_modules/@types/node": "node", + }, ) # @external_end diff --git a/packages/angular_devkit/schematics/BUILD.bazel b/packages/angular_devkit/schematics/BUILD.bazel index e2c1e4bbbff8..491b1cbfd9ed 100644 --- a/packages/angular_devkit/schematics/BUILD.bazel +++ b/packages/angular_devkit/schematics/BUILD.bazel @@ -1,4 +1,4 @@ -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "jasmine_test", "npm_package", "ts_project") @@ -85,7 +85,9 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/angular_devkit/schematics", - npm_package = "angular_cli/packages/angular_devkit/schematics/npm_package", - types = ["@npm//@types/node"], + golden_dir = "goldens/public-api/angular_devkit/schematics", + npm_package = "packages/angular_devkit/schematics/npm_package", + types = { + "//:node_modules/@types/node": "node", + }, ) diff --git a/packages/ngtools/webpack/BUILD.bazel b/packages/ngtools/webpack/BUILD.bazel index 36d3096f6f03..7e4d5ce7a53e 100644 --- a/packages/ngtools/webpack/BUILD.bazel +++ b/packages/ngtools/webpack/BUILD.bazel @@ -3,7 +3,7 @@ # Use of this source code is governed by an MIT-style license that can be # found in the LICENSE file at https://angular.dev/license -load("@npm//@angular/build-tooling/bazel/api-golden:index.bzl", "api_golden_test_npm_package") +load("@devinfra//bazel/api-golden:index_rjs.bzl", "api_golden_test_npm_package") load("@npm2//:defs.bzl", "npm_link_all_packages") load("//tools:defaults2.bzl", "jasmine_test", "npm_package", "ts_project") @@ -88,6 +88,6 @@ api_golden_test_npm_package( ":npm_package", "//goldens:public-api", ], - golden_dir = "angular_cli/goldens/public-api/ngtools/webpack", - npm_package = "angular_cli/packages/ngtools/webpack/npm_package", + golden_dir = "goldens/public-api/ngtools/webpack", + npm_package = "packages/ngtools/webpack/npm_package", ) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de87bd9577a5..b7e065fe9910 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,11 +23,11 @@ importers: specifier: 19.2.0 version: 19.2.0(@angular/core@19.2.0) '@angular/bazel': - specifier: https://github.com/angular/bazel-builds.git#8faa06d66416ce78073ab59539ff03f5253b8d52 - version: github.com/angular/bazel-builds/8faa06d66416ce78073ab59539ff03f5253b8d52(@angular/compiler-cli@19.2.0)(@rollup/plugin-commonjs@28.0.2)(@rollup/plugin-node-resolve@13.3.0)(@types/node@20.17.19)(rollup-plugin-sourcemaps@0.6.3)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2) + specifier: https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65 + version: github.com/angular/bazel-builds/58e1a344eed2dfea489cd290a4b4a963f7e3ac65(@angular/compiler-cli@19.2.0)(@rollup/plugin-commonjs@28.0.2)(@rollup/plugin-node-resolve@13.3.0)(@types/node@20.17.19)(rollup-plugin-sourcemaps@0.6.3)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2) '@angular/build-tooling': - specifier: https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592 - version: github.com/angular/dev-infra-private-build-tooling-builds/a6a996a69cfc03b3fbe538f11dd24b7bc4b30592(debug@4.4.0)(karma-chrome-launcher@3.2.0)(karma-jasmine@5.1.0)(karma@6.4.4)(rxjs@7.8.2)(terser@5.39.0)(zone.js@0.15.0) + specifier: https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1 + version: github.com/angular/dev-infra-private-build-tooling-builds/d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1(debug@4.4.0)(karma-chrome-launcher@3.2.0)(karma-jasmine@5.1.0)(karma@6.4.4)(rxjs@7.8.2)(terser@5.39.0)(zone.js@0.15.0) '@angular/cdk': specifier: 19.2.1 version: 19.2.1(@angular/common@19.2.0)(@angular/core@19.2.0)(rxjs@7.8.2) @@ -106,9 +106,6 @@ importers: '@bazel/buildifier': specifier: 8.0.3 version: 8.0.3 - '@bazel/runfiles': - specifier: ^6.0.0 - version: 6.3.1 '@discoveryjs/json-ext': specifier: 0.6.3 version: 0.6.3 @@ -136,6 +133,9 @@ importers: '@rollup/plugin-commonjs': specifier: ^28.0.0 version: 28.0.2(rollup@4.34.9) + '@rollup/plugin-json': + specifier: ^6.1.0 + version: 6.1.0(rollup@4.34.9) '@rollup/plugin-node-resolve': specifier: ^13.0.5 version: 13.3.0(rollup@4.34.9) @@ -735,9 +735,6 @@ importers: '@angular/router': specifier: 19.2.0 version: 19.2.0(@angular/common@19.2.0)(@angular/core@19.2.0)(@angular/platform-browser@19.2.0)(rxjs@7.8.2) - '@bazel/runfiles': - specifier: ^6.0.0 - version: 6.3.1 '@schematics/angular': specifier: workspace:* version: link:../../schematics/angular @@ -2387,7 +2384,6 @@ packages: /@bazel/typescript@5.8.1(typescript@5.8.2): resolution: {integrity: sha512-NAJ8WQHZL1WE1YmRoCrq/1hhG15Mvy/viWh6TkvFnBeEhNUiQUsA5GYyhU1ztnBIYW03nATO3vwhAEfO7Q0U5g==} - deprecated: No longer maintained, https://github.com/aspect-build/rules_ts is the recommended replacement hasBin: true peerDependencies: typescript: 5.8.2 @@ -11578,7 +11574,7 @@ packages: /puppeteer@18.2.1: resolution: {integrity: sha512-7+UhmYa7wxPh2oMRwA++k8UGVDxh3YdWFB52r9C3tM81T6BU7cuusUSxImz0GEYSOYUKk/YzIhkQ6+vc0gHbxQ==} engines: {node: '>=14.1.0'} - deprecated: < 22.8.2 is no longer supported + deprecated: < 19.4.0 is no longer supported requiresBuild: true dependencies: https-proxy-agent: 5.0.1(supports-color@10.0.0) @@ -14233,15 +14229,15 @@ packages: resolution: {integrity: sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==} dev: true - github.com/angular/bazel-builds/8faa06d66416ce78073ab59539ff03f5253b8d52(@angular/compiler-cli@19.2.0)(@rollup/plugin-commonjs@28.0.2)(@rollup/plugin-node-resolve@13.3.0)(@types/node@20.17.19)(rollup-plugin-sourcemaps@0.6.3)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2): - resolution: {tarball: https://codeload.github.com/angular/bazel-builds/tar.gz/8faa06d66416ce78073ab59539ff03f5253b8d52} - id: github.com/angular/bazel-builds/8faa06d66416ce78073ab59539ff03f5253b8d52 + github.com/angular/bazel-builds/58e1a344eed2dfea489cd290a4b4a963f7e3ac65(@angular/compiler-cli@19.2.0)(@rollup/plugin-commonjs@28.0.2)(@rollup/plugin-node-resolve@13.3.0)(@types/node@20.17.19)(rollup-plugin-sourcemaps@0.6.3)(rollup@4.34.9)(terser@5.39.0)(typescript@5.8.2): + resolution: {tarball: https://codeload.github.com/angular/bazel-builds/tar.gz/58e1a344eed2dfea489cd290a4b4a963f7e3ac65} + id: github.com/angular/bazel-builds/58e1a344eed2dfea489cd290a4b4a963f7e3ac65 name: '@angular/bazel' - version: 20.0.0-next.0 + version: 19.3.0-next.0 engines: {node: ^18.19.1 || ^20.11.1 || >=22.0.0} hasBin: true peerDependencies: - '@angular/compiler-cli': 20.0.0-next.0+sha-51b8ff2 + '@angular/compiler-cli': 19.3.0-next.0+sha-8657a0e '@bazel/concatjs': ^5.3.0 '@bazel/worker': ^5.3.0 '@rollup/plugin-commonjs': ^28.0.0 @@ -14268,11 +14264,11 @@ packages: - '@types/node' dev: true - github.com/angular/dev-infra-private-build-tooling-builds/a6a996a69cfc03b3fbe538f11dd24b7bc4b30592(debug@4.4.0)(karma-chrome-launcher@3.2.0)(karma-jasmine@5.1.0)(karma@6.4.4)(rxjs@7.8.2)(terser@5.39.0)(zone.js@0.15.0): - resolution: {tarball: https://codeload.github.com/angular/dev-infra-private-build-tooling-builds/tar.gz/a6a996a69cfc03b3fbe538f11dd24b7bc4b30592} - id: github.com/angular/dev-infra-private-build-tooling-builds/a6a996a69cfc03b3fbe538f11dd24b7bc4b30592 + github.com/angular/dev-infra-private-build-tooling-builds/d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1(debug@4.4.0)(karma-chrome-launcher@3.2.0)(karma-jasmine@5.1.0)(karma@6.4.4)(rxjs@7.8.2)(terser@5.39.0)(zone.js@0.15.0): + resolution: {tarball: https://codeload.github.com/angular/dev-infra-private-build-tooling-builds/tar.gz/d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1} + id: github.com/angular/dev-infra-private-build-tooling-builds/d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1 name: '@angular/build-tooling' - version: 0.0.0-b015169b635123c1ab9084f604e36b6342eac171 + version: 0.0.0-74aabba6d202918280dafe92f87f9c154476fa86 dependencies: '@angular/benchpress': 0.3.0(rxjs@7.8.2)(zone.js@0.15.0) '@angular/build': link:packages/angular/build diff --git a/scripts/windows-testing/convert-symlinks.mjs b/scripts/windows-testing/convert-symlinks.mjs new file mode 100644 index 000000000000..56d8b1ad85bc --- /dev/null +++ b/scripts/windows-testing/convert-symlinks.mjs @@ -0,0 +1,158 @@ +/** + * @fileoverview Script that takes a directory and converts all its Unix symlinks + * to relative Windows-compatible symlinks. This is necessary because when building + * tests via Bazel inside WSL; the output cannot simply be used outside WSL to perform + * native Windows testing. This is a known limitation/bug of the WSL <> Windows interop. + * + * Symlinks are commonly used by Bazel inside the `.runfiles` directory, which is relevant + * for executing tests outside Bazel on the host machine. In addition, `rules_js` heavily + * relies on symlinks for node modules. + * + * Some more details in: + * - https://blog.trailofbits.com/2024/02/12/why-windows-cant-follow-wsl-symlinks/. + * - https://pnpm.io/symlinked-node-modules-structure. + */ + +import path from 'node:path'; +import fs from 'node:fs/promises'; +import childProcess from 'node:child_process'; + +const [rootDir, cmdPath] = process.argv.slice(2); + +// GitHub actions can set this environment variable when pressing the "re-run" button. +const debug = process.env.ACTIONS_STEP_DEBUG === 'true'; +const skipDirectories = [ + // Modules that we don't need and would unnecessarily slow-down this. + '_windows_amd64/bin/nodejs/node_modules', +]; + +const workspaceRootPaths = [/.*\.runfiles\/angular_cli\//, /^.*-fastbuild\/bin\//]; + +// Copying can be parallelized and doesn't cause any WSL flakiness (no exe is invoked). +const parallelCopyTasks = []; + +async function transformDir(p) { + // We perform all command executions in parallel here to speed up. + // Note that we can't parallelize for the full recursive directory, + // as WSL and its interop would otherwise end up with some flaky errors. + // See: https://github.com/microsoft/WSL/issues/8677. + const tasks = []; + // We explore directories after all files were checked at this level. + const directoriesToVisit = []; + + for (const file of await fs.readdir(p, { withFileTypes: true })) { + const subPath = path.join(p, file.name); + + if (skipDirectories.some((d) => subPath.endsWith(d))) { + continue; + } + + if (file.isSymbolicLink()) { + // Allow for parallel processing of directory entries. + tasks.push( + (async () => { + let target = ''; + try { + target = await fs.realpath(subPath); + } catch (e) { + if (debug) { + console.error('Skipping', subPath); + } + return; + } + + await fs.rm(subPath); + + const subPathId = relativizeForSimilarWorkspacePaths(subPath); + const targetPathId = relativizeForSimilarWorkspacePaths(target); + const isSelfLink = subPathId === targetPathId; + + // This is an actual file that needs to be copied. Copy contents. + // - the target path is equivalent to the link. This is a self-link from `.runfiles` to `bin/`. + // - the target path is outside any of our workspace roots. + if (isSelfLink || targetPathId.startsWith('..')) { + parallelCopyTasks.push(exec(`cp -Rf ${target} ${subPath}`)); + return; + } + + const relativeSubPath = relativizeToRoot(subPath); + const targetAtDestination = path.relative(path.dirname(subPathId), targetPathId); + const targetAtDestinationWindowsPath = targetAtDestination.replace(/\//g, '\\'); + + const wslSubPath = relativeSubPath.replace(/\//g, '\\'); + + if (debug) { + console.log({ + targetAtDestination, + subPath, + relativeSubPath, + target, + targetPathId, + subPathId, + }); + } + + if ((await fs.stat(target)).isDirectory()) { + // This is a symlink to a directory, create a dir junction. + // Re-create this symlink on the Windows FS using the Windows mklink command. + await exec( + `${cmdPath} /c mklink /d "${wslSubPath}" "${targetAtDestinationWindowsPath}"`, + ); + } else { + // This is a symlink to a file, create a file junction. + // Re-create this symlink on the Windows FS using the Windows mklink command. + await exec(`${cmdPath} /c mklink "${wslSubPath}" "${targetAtDestinationWindowsPath}"`); + } + })(), + ); + } else if (file.isDirectory()) { + directoriesToVisit.push(subPath); + } + } + + // Wait for all commands/tasks to complete, executed in parallel. + await Promise.all(tasks); + + // Descend into other directories, sequentially to avoid WSL interop errors. + for (const d of directoriesToVisit) { + await transformDir(d); + } +} + +function exec(cmd) { + return new Promise((resolve, reject) => { + childProcess.exec(cmd, { cwd: rootDir }, (error) => { + if (error !== null) { + reject(error); + } else { + resolve(); + } + }); + }); +} + +function relativizeForSimilarWorkspacePaths(p) { + const workspaceRootMatch = workspaceRootPaths.find((r) => r.test(p)); + if (workspaceRootMatch !== undefined) { + return p.replace(workspaceRootMatch, ''); + } + + return path.relative(rootDir, p); +} + +function relativizeToRoot(p) { + const res = path.relative(rootDir, p); + if (!res.startsWith('..')) { + return res; + } + + throw new Error('Could not relativize to root: ' + p); +} + +try { + await transformDir(rootDir); + await Promise.all(parallelCopyTasks); +} catch (err) { + console.error('Could not convert symlinks:', err); + process.exitCode = 1; +} diff --git a/scripts/windows-testing/parallel-executor.mjs b/scripts/windows-testing/parallel-executor.mjs new file mode 100644 index 000000000000..0020354692a7 --- /dev/null +++ b/scripts/windows-testing/parallel-executor.mjs @@ -0,0 +1,178 @@ +import * as child_process from 'node:child_process'; +import path from 'node:path'; +import { stripVTControlCharacters } from 'node:util'; + +const initialStatusRegex = /Running (\d+) tests/; + +async function main() { + const [runfilesDir, targetName, testArgs] = process.argv.slice(2); + const maxShards = 4; + + const testEntrypoint = path.resolve(runfilesDir, '../', targetName); + const testWorkingDir = path.resolve(runfilesDir, 'angular_cli'); + const tasks = []; + const progress = {}; + + for (let i = 0; i < maxShards; i++) { + tasks.push( + spawnTest( + 'bash', + [testEntrypoint, ...testArgs.split(' ').filter((arg) => arg !== '')], + { + cwd: testWorkingDir, + env: { + // Try to construct a pretty hermetic environment, as within Bazel. + PATH: process.env.PATH, + TEST_TOTAL_SHARDS: maxShards, + TEST_SHARD_INDEX: i, + E2E_SHARD_TOTAL: process.env.E2E_SHARD_TOTAL, + E2E_SHARD_INDEX: process.env.E2E_SHARD_INDEX, + FORCE_COLOR: '3', + // Needed by `rules_js` + BAZEL_BINDIR: '.', + }, + }, + (s) => (progress[i] = s), + ), + ); + } + + const printUpdate = () => { + console.error(`----`); + for (const [taskId, status] of Object.entries(progress)) { + const durationInMin = (Date.now() - status.startTime) / 1000 / 60; + console.error( + `Shard #${taskId}: stage ${status.state} | ` + + `${status.current}/${status.max} tests completed (${durationInMin.toFixed(2)}min)`, + ); + } + }; + + const progressInterval = setInterval(printUpdate, 4000); + + try { + const outputs = await Promise.all(tasks); + printUpdate(); + + for (const [idx, text] of outputs.entries()) { + console.log(`---------- ${idx} -----------`); + console.log(text); + } + + console.error(''); + console.error('Done! Passing'); + } catch (e) { + if (e instanceof TestSpawnError) { + console.error(e.output); + console.error(e.message); + } else if (e instanceof Error) { + console.error(e.message, e.stack); + } else { + console.error(e); + } + + console.error('Tests failed!'); + process.exitCode = 1; + } finally { + clearInterval(progressInterval); + } +} + +function spawnTest(cmd, args, options, reportStatus, startTime = Date.now(), testAttempts = 2) { + testAttempts -= 1; + + const testProgressRegex = /Running test[^\(]+\((\d+) of/g; + + return new Promise((resolve, reject) => { + let output = ''; + let state = 'setup'; + let current = 0; + let max = 0; + + const proc = child_process.spawn(cmd, args, { ...options, stdio: 'pipe' }); + const syncStatus = () => reportStatus({ current, max, state, startTime }); + const restartTest = () => { + console.error(output); + console.error(`Test restarted due to failure.`); + resolve(spawnTest(cmd, args, options, reportStatus, startTime, testAttempts)); + }; + const onOutputChange = () => { + // Extract initial status (i.e. how many tests there are in this shard) + if (initialStatusRegex.test(output) && state === 'setup') { + max = Number(output.match(initialStatusRegex)[1]); + } + if (/Running initializer/.test(output) && state === 'setup') { + state = 'initializing'; + } + if (/Running test/.test(output) && state === 'initializing') { + state = 'testing'; + } + if (state === 'testing') { + const oldLastIndex = testProgressRegex.lastIndex; + const newMatch = testProgressRegex.exec(stripVTControlCharacters(output))?.[1]; + // Do not advance the Regex, or more precisely, reset to index `0`. + if (newMatch === undefined) { + testProgressRegex.lastIndex = oldLastIndex; + } else { + current = Number(newMatch); + } + } + syncStatus(); + }; + proc.stdout.on('data', (data) => { + output += data; + onOutputChange(); + }); + proc.stderr.on('data', (data) => { + output += data; + onOutputChange(); + }); + proc.on('error', (err) => { + syncStatus(); + + // If this test failed and there are test attempts remaining, re-run. + if (testAttempts > 0) { + restartTest(); + return; + } + + reject(new TestSpawnError(err.message, output)); + }); + proc.on('close', (code, signal) => { + syncStatus(); + + if (code === 0 && signal === null) { + resolve(output); + } else { + if (testAttempts > 0) { + restartTest(); + return; + } + + reject( + new TestSpawnError(`Command failed with code: ${code} and signal ${signal}`, output), + ); + } + }); + + // Report initial status, without knowing anything. + syncStatus(); + }); +} + +class TestSpawnError extends Error { + /** @type {string} */ + output; + + constructor(message, output) { + super(message); + this.output = output; + } +} + +try { + main(); +} catch (e) { + console.error(e); + process.exitCode = 1; +} diff --git a/tests/legacy-cli/BUILD.bazel b/tests/legacy-cli/BUILD.bazel index 0dd104c5b43b..f382333beb5b 100644 --- a/tests/legacy-cli/BUILD.bazel +++ b/tests/legacy-cli/BUILD.bazel @@ -1,16 +1,16 @@ +load("@aspect_bazel_lib//lib:directory_path.bzl", "directory_path") +load("@npm2//:rollup/package_json.bzl", rollup = "bin") load("//tools:interop.bzl", "ts_project") load(":e2e.bzl", "e2e_suites") +package(default_visibility = ["//visibility:public"]) + ts_project( name = "runner", testonly = True, srcs = [ "e2e_runner.ts", ], - data = [ - "verdaccio.yaml", - "verdaccio_auth.yaml", - ], deps = [ "//:node_modules/@types/node", "//:node_modules/ansi-colors", @@ -21,17 +21,54 @@ ts_project( ], ) +rollup.rollup( + name = "runner_bundled", + testonly = True, + srcs = [ + "rollup.config.mjs", + ":runner_rjs", + "//:node_modules/@rollup/plugin-alias", + "//:node_modules/@rollup/plugin-commonjs", + "//:node_modules/@rollup/plugin-json", + "//:node_modules/@rollup/plugin-node-resolve", + "//:node_modules/fast-glob", + "//tests/legacy-cli/e2e/initialize:initialize_rjs", + "//tests/legacy-cli/e2e/ng-snapshot", + "//tests/legacy-cli/e2e/setup:setup_rjs", + "//tests/legacy-cli/e2e/tests:tests_rjs", + ], + args = [ + "--format=cjs", + "--config=./rollup.config.mjs", + ], + chdir = package_name(), + out_dirs = ["runner_bundled_out"], + progress_message = "Bundling e2e test runner", +) + +directory_path( + name = "runner_entrypoint", + testonly = True, + directory = ":runner_bundled", + path = "./e2e_runner.js", +) + e2e_suites( name = "e2e", data = [ - ":runner", + ":runner_bundled", + "verdaccio.yaml", + "verdaccio_auth.yaml", - # Tests + setup - # Loaded dynamically at runtime, not compiletime deps + # Dynamically loaded. "//tests/legacy-cli/e2e/assets", - "//tests/legacy-cli/e2e/setup", - "//tests/legacy-cli/e2e/initialize", - "//tests/legacy-cli/e2e/tests", + "//:node_modules/verdaccio", + "//:node_modules/verdaccio-auth-memory", + + # Extra runtime deps due to bundling issues. + # TODO: Clean this up. + "//:node_modules/@verdaccio/config", + "//:node_modules/express", ], - runner = ":e2e_runner.ts", + runner = ":runner_entrypoint", ) diff --git a/tests/legacy-cli/e2e.bzl b/tests/legacy-cli/e2e.bzl index 74402064f961..1bed11a77596 100644 --- a/tests/legacy-cli/e2e.bzl +++ b/tests/legacy-cli/e2e.bzl @@ -1,4 +1,4 @@ -load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_test") +load("@aspect_rules_js//js:defs.bzl", "js_test") load("//tools:toolchain_info.bzl", "TOOLCHAINS_NAMES", "TOOLCHAINS_VERSIONS") # bazel query --output=label "kind('pkg_tar', //packages/...)" @@ -87,7 +87,7 @@ def e2e_suites(name, runner, data): # Saucelabs tests are only run on the default toolchain _e2e_suite(name, runner, "saucelabs", data) -def _e2e_tests(name, runner, **kwargs): +def _e2e_tests(name, runner, toolchain, **kwargs): # Always specify all the npm packages args = kwargs.pop("templated_args", []) + [ "--package $(rootpath %s)" % p @@ -100,15 +100,12 @@ def _e2e_tests(name, runner, **kwargs): # Tags that must always be applied tags = kwargs.pop("tags", []) + TEST_TAGS - # Passthru E2E variables in case it is customized by CI etc - configuration_env_vars = kwargs.pop("configuration_env_vars", []) + ["E2E_TEMP", "E2E_SHARD_INDEX", "E2E_SHARD_TOTAL"] - env = kwargs.pop("env", {}) toolchains = kwargs.pop("toolchains", []) # The git toolchain + env env.update({"GIT_BIN": "$(GIT_BIN_PATH)"}) - toolchains = toolchains + ["@npm//@angular/build-tooling/bazel/git-toolchain:current_git_toolchain"] + toolchains = toolchains + ["@devinfra//bazel/git-toolchain:current_git_toolchain"] # Chromium browser toolchain env.update({ @@ -116,18 +113,24 @@ def _e2e_tests(name, runner, **kwargs): "CHROME_PATH": "$(CHROMIUM)", "CHROMEDRIVER_BIN": "$(CHROMEDRIVER)", }) - toolchains = toolchains + ["@npm//@angular/build-tooling/bazel/browsers/chromium:toolchain_alias"] - data = data + ["@npm//@angular/build-tooling/bazel/browsers/chromium"] + toolchains = toolchains + ["@devinfra//bazel/browsers/chromium:toolchain_alias"] + data = data + ["@devinfra//bazel/browsers/chromium"] - nodejs_test( + js_test( name = name, - templated_args = args, + fixed_args = args, data = data, entry_point = runner, env = env, - configuration_env_vars = configuration_env_vars, tags = tags, toolchains = toolchains, + node_toolchain = toolchain, + include_npm = select({ + # For Windows testing mode, we use the real global NPM as otherwise this + # will be a lot of files that need to be brought from WSL to the host FS. + "@platforms//os:windows": False, + "//conditions:default": True, + }), **kwargs ) diff --git a/tests/legacy-cli/e2e/setup/010-local-publish.ts b/tests/legacy-cli/e2e/setup/010-local-publish.ts index 4809b5a8c12d..b58a5b871daf 100644 --- a/tests/legacy-cli/e2e/setup/010-local-publish.ts +++ b/tests/legacy-cli/e2e/setup/010-local-publish.ts @@ -2,7 +2,7 @@ import { writeFile } from 'node:fs/promises'; import { join } from 'node:path/posix'; import { getGlobalVariable } from '../utils/env'; import { PkgInfo } from '../utils/packages'; -import { globalNpm, extractNpmEnv } from '../utils/process'; +import { globalNpm, extractNpmEnv, extractCIAndInfraEnv } from '../utils/process'; import { isPrereleaseCli } from '../utils/project'; export default async function () { @@ -21,6 +21,7 @@ export default async function () { packageTars.map(({ path: p }) => globalNpm(['publish', '--tag', isPrereleaseCli() ? 'next' : 'latest', p], { ...extractNpmEnv(), + ...extractCIAndInfraEnv(), 'NPM_CONFIG_USERCONFIG': npmrc, }), ), diff --git a/tests/legacy-cli/e2e/tests/BUILD.bazel b/tests/legacy-cli/e2e/tests/BUILD.bazel index 61fc5a6c2120..23c3de288a28 100644 --- a/tests/legacy-cli/e2e/tests/BUILD.bazel +++ b/tests/legacy-cli/e2e/tests/BUILD.bazel @@ -6,9 +6,6 @@ ts_project( name = "tests", testonly = True, srcs = glob(["**/*.ts"]), - data = [ - "//tests/legacy-cli/e2e/ng-snapshot", - ], deps = [ "//:node_modules/@types/express", "//:node_modules/@types/node", diff --git a/tests/legacy-cli/e2e/tests/build/auto-csp.ts b/tests/legacy-cli/e2e/tests/build/auto-csp.ts index 6b975dd5a3e8..1839b160d549 100644 --- a/tests/legacy-cli/e2e/tests/build/auto-csp.ts +++ b/tests/legacy-cli/e2e/tests/build/auto-csp.ts @@ -122,6 +122,7 @@ export default async function () { const port = await findFreePort(); await execAndWaitForOutputToMatch('node', ['serve.js'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts index e7bd9d9b1ecb..96be34e524da 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-csp-nonce.ts @@ -149,6 +149,7 @@ export default async function () { ['run', runCommand], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts index 6cb4d9e15ed2..dda29bdced62 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-ngmodule.ts @@ -155,6 +155,7 @@ export default async function () { ['run', runCommand], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts index 632c90522a3e..b697ac513ab4 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/express-engine-standalone.ts @@ -118,6 +118,7 @@ export default async function () { ['run', runCommand], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts index c9bcc6ee5a09..c4c2065f8b64 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-base-href.ts @@ -109,6 +109,7 @@ async function spawnServer(): Promise { ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts index 6e473880b32c..9b7f75f04a87 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n-sub-path.ts @@ -144,6 +144,7 @@ async function spawnServer(): Promise { ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts index 1c327922d5d2..0f10a959a9de 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server-i18n.ts @@ -120,6 +120,7 @@ async function spawnServer(): Promise { ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts index 822b9ea9bb7e..891b646bfc38 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-output-mode-server.ts @@ -204,6 +204,7 @@ async function spawnServer(): Promise { ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts index 77670e5eb64d..92c154db3891 100644 --- a/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts +++ b/tests/legacy-cli/e2e/tests/build/server-rendering/server-routes-preload-links.ts @@ -215,6 +215,7 @@ async function spawnServer(): Promise { ['run', 'serve:ssr:test-project'], /Node Express server listening on/, { + ...process.env, 'PORT': String(port), }, ); diff --git a/tests/legacy-cli/e2e/tests/build/styles/tailwind-v3.ts b/tests/legacy-cli/e2e/tests/build/styles/tailwind-v3.ts index aa47808d8203..efda7dbcef66 100644 --- a/tests/legacy-cli/e2e/tests/build/styles/tailwind-v3.ts +++ b/tests/legacy-cli/e2e/tests/build/styles/tailwind-v3.ts @@ -1,4 +1,4 @@ -import { deleteFile, expectFileToMatch, writeFile } from '../../../utils/fs'; +import { deleteFile, expectFileToMatch, rimraf, writeFile } from '../../../utils/fs'; import { installPackage, uninstallPackage } from '../../../utils/packages'; import { ng, silentExec } from '../../../utils/process'; import { expectToFail } from '../../../utils/utils'; @@ -8,6 +8,10 @@ export default async function () { // and its configuration file. Otherwise cached builds without tailwind will cause test failures. await ng('cache', 'off'); + // In case a previous test installed tailwindcss, clear it. + // (we don't clear node module directories between tests) + await rimraf('node_modules/tailwindcss'); + // Create configuration file await silentExec('npx', 'tailwindcss@3', 'init'); diff --git a/tests/legacy-cli/e2e/tests/commands/analytics/analytics-info.ts b/tests/legacy-cli/e2e/tests/commands/analytics/analytics-info.ts index 68d15db2358e..d92dfd2ffde6 100644 --- a/tests/legacy-cli/e2e/tests/commands/analytics/analytics-info.ts +++ b/tests/legacy-cli/e2e/tests/commands/analytics/analytics-info.ts @@ -5,16 +5,19 @@ export default async function () { // Should be disabled by default. await configureTest(undefined /** analytics */); await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: disabled/, { + ...process.env, NG_FORCE_TTY: '0', // Disable prompts }); await configureTest('1dba0835-38a3-4957-bf34-9974e2df0df3' /** analytics */); await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: enabled/, { + ...process.env, NG_FORCE_TTY: '0', // Disable prompts }); await configureTest(false /** analytics */); await execAndWaitForOutputToMatch('ng', ['analytics', 'info'], /Effective status: disabled/, { + ...process.env, NG_FORCE_TTY: '0', // Disable prompts }); } diff --git a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts b/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts index 1579125f0d0e..88300951965e 100644 --- a/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts +++ b/tests/legacy-cli/e2e/tests/misc/invalid-schematic-dependencies.ts @@ -1,6 +1,12 @@ import { join } from 'node:path'; import { expectFileToMatch } from '../../utils/fs'; -import { execWithEnv, extractNpmEnv, ng, silentNpm } from '../../utils/process'; +import { + execWithEnv, + extractCIAndInfraEnv, + extractNpmEnv, + ng, + silentNpm, +} from '../../utils/process'; import { getActivePackageManager, installPackage, uninstallPackage } from '../../utils/packages'; import { isPrereleaseCli } from '../../utils/project'; import { appendFile, writeFile } from 'node:fs/promises'; @@ -48,6 +54,7 @@ async function publishOutdated(npmSpecifier: string): Promise { await execWithEnv('npm', ['publish', stdoutPack.trim(), '--tag=outdated'], { ...extractNpmEnv(), + ...extractCIAndInfraEnv(), 'NPM_CONFIG_USERCONFIG': npmrc, }); } diff --git a/tests/legacy-cli/e2e/tests/vite/reuse-dep-optimization-cache.ts b/tests/legacy-cli/e2e/tests/vite/reuse-dep-optimization-cache.ts index ddcbe5e60d20..56ecdfee8cd0 100644 --- a/tests/legacy-cli/e2e/tests/vite/reuse-dep-optimization-cache.ts +++ b/tests/legacy-cli/e2e/tests/vite/reuse-dep-optimization-cache.ts @@ -12,18 +12,19 @@ export default async function () { await ng('cache', 'on'); const port = await findFreePort(); - await execAndWaitForOutputToMatch( + const serveReady = execAndWaitForOutputToMatch( 'ng', ['serve', '--port', `${port}`], /Application bundle generation complete/, // Use CI:0 to force caching - { DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, + { ...process.env, DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, ); - const [, response] = await Promise.all([ - waitForAnyProcessOutputToMatch(/dependencies optimized/, 10_000), - fetch(`http://localhost:${port}/main.js`), - ]); + // Note: Don't await `serveReady` before, as otherwise we might not see + // the dependencies optimized output. There is some debouncing for `ng serve` + // going on that could cause this. + await Promise.all([serveReady, waitForAnyProcessOutputToMatch(/dependencies optimized/, 10_000)]); + const response = await fetch(`http://localhost:${port}/main.js`); assert(response.ok, `Expected 'response.ok' to be 'true'.`); @@ -35,6 +36,6 @@ export default async function () { ['serve', '--port=0'], /Hash is consistent\. Skipping/, // Use CI:0 to force caching - { DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, + { ...process.env, DEBUG: 'vite:deps', CI: '0', NO_COLOR: 'true' }, ); } diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-new-dep-optimization.ts b/tests/legacy-cli/e2e/tests/vite/ssr-new-dep-optimization.ts index d2dfb8b554b9..be814b01bf89 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-new-dep-optimization.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-new-dep-optimization.ts @@ -31,7 +31,7 @@ export default async function () { 'ng', ['serve', '--port', port.toString()], /Application bundle generation complete/, - { CI: '0', NO_COLOR: 'true' }, + { ...process.env, CI: '0', NO_COLOR: 'true' }, ); await validateResponse('/', /Hello,/); diff --git a/tests/legacy-cli/e2e/tests/vite/ssr-no-server-entry-sub-path.ts b/tests/legacy-cli/e2e/tests/vite/ssr-no-server-entry-sub-path.ts index a55f48d0b39f..a4d4ac2cfc61 100644 --- a/tests/legacy-cli/e2e/tests/vite/ssr-no-server-entry-sub-path.ts +++ b/tests/legacy-cli/e2e/tests/vite/ssr-no-server-entry-sub-path.ts @@ -39,6 +39,7 @@ export default async function () { const port = await findFreePort(); await execAndWaitForOutputToMatch('ng', ['serve', '--port', `${port}`], /complete/, { + ...process.env, NO_COLOR: 'true', }); diff --git a/tests/legacy-cli/e2e/utils/BUILD.bazel b/tests/legacy-cli/e2e/utils/BUILD.bazel index 4d690d4bace2..1ae6a25a3144 100644 --- a/tests/legacy-cli/e2e/utils/BUILD.bazel +++ b/tests/legacy-cli/e2e/utils/BUILD.bazel @@ -9,6 +9,8 @@ ts_project( data = [ "//tests/legacy-cli/e2e/ng-snapshot", ], + # TODO(devversion): Remove + enable_runtime_rnjs_interop = False, deps = [ "//:node_modules/@types/jasmine", "//:node_modules/@types/node", diff --git a/tests/legacy-cli/e2e/utils/assets.ts b/tests/legacy-cli/e2e/utils/assets.ts index 51b09a7416a9..d421086c1b9e 100644 --- a/tests/legacy-cli/e2e/utils/assets.ts +++ b/tests/legacy-cli/e2e/utils/assets.ts @@ -8,7 +8,7 @@ import { installWorkspacePackages, setRegistry } from './packages'; import { useBuiltPackagesVersions } from './project'; export function assetDir(assetName: string) { - return join(__dirname, '../assets', assetName); + return join(__dirname, '../e2e/assets', assetName); } export function copyProjectAsset(assetName: string, to?: string) { diff --git a/tests/legacy-cli/e2e/utils/process.ts b/tests/legacy-cli/e2e/utils/process.ts index db279cf6cc8b..3cd6c77bc187 100644 --- a/tests/legacy-cli/e2e/utils/process.ts +++ b/tests/legacy-cli/e2e/utils/process.ts @@ -188,16 +188,16 @@ export function extractNpmEnv() { }, {}); } -function extractCIEnv(): NodeJS.ProcessEnv { +export function extractCIAndInfraEnv(): NodeJS.ProcessEnv { return Object.keys(process.env) .filter( (v) => v.startsWith('SAUCE_') || v === 'CI' || - v === 'CIRCLECI' || v === 'CHROME_BIN' || v === 'CHROME_PATH' || - v === 'CHROMEDRIVER_BIN', + v === 'CHROMEDRIVER_BIN' || + v.startsWith('JS_BINARY__'), ) .reduce((vars, n) => { vars[n] = process.env[n]; @@ -214,14 +214,16 @@ function extractNgEnv() { }, {}); } -export function waitForAnyProcessOutputToMatch( +export async function waitForAnyProcessOutputToMatch( match: RegExp, timeout = 30000, ): Promise { + let timeoutId: ReturnType | null = null; + // Race between _all_ processes, and the timeout. First one to resolve/reject wins. const timeoutPromise: Promise = new Promise((_resolve, reject) => { // Wait for 30 seconds and timeout. - setTimeout(() => { + timeoutId = setTimeout(() => { reject(new Error(`Waiting for ${match} timed out (timeout: ${timeout}msec)...`)); }, timeout); }); @@ -248,7 +250,11 @@ export function waitForAnyProcessOutputToMatch( }), ); - return Promise.race(matchPromises.concat([timeoutPromise])); + const matchingProcess = await Promise.race(matchPromises.concat([timeoutPromise])); + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + return matchingProcess; } export async function killAllProcesses(signal = 'SIGTERM'): Promise { @@ -393,7 +399,7 @@ export function globalNpm(args: string[], env?: NodeJS.ProcessEnv) { ); } - return _exec({ silent: true, env }, process.execPath, [require.resolve('npm'), ...args]); + return _exec({ silent: true, env }, 'npm', args); } export function node(...args: string[]) { @@ -435,7 +441,7 @@ export async function launchTestProcess(entry: string, ...args: any[]): Promise< BAZEL_TARGET: process.env.BAZEL_TARGET, ...extractNpmEnv(), - ...extractCIEnv(), + ...extractCIAndInfraEnv(), ...extractNgEnv(), ...getGlobalVariablesEnv(), }; @@ -447,7 +453,12 @@ export async function launchTestProcess(entry: string, ...args: any[]): Promise< .filter((p) => p.startsWith(tempRoot) || p.startsWith(TEMP) || !p.includes('angular-cli')) .join(delimiter); - const testProcessArgs = [resolve(__dirname, 'test_process'), entry, ...args]; + const testProcessArgs = [ + // Note: `__dirname` is the bundle directory here. + resolve(__dirname, 'e2e/utils/test_process.js'), + entry, + ...args, + ]; return new Promise((resolve, reject) => { spawn(process.execPath, testProcessArgs, { diff --git a/tests/legacy-cli/e2e/utils/registry.ts b/tests/legacy-cli/e2e/utils/registry.ts index 8aad8a57eda2..1bd3084d4f48 100644 --- a/tests/legacy-cli/e2e/utils/registry.ts +++ b/tests/legacy-cli/e2e/utils/registry.ts @@ -14,7 +14,7 @@ export async function createNpmRegistry( const registryPath = await mktempd('angular-cli-e2e-registry-'); let configContent = await readFile( - join(__dirname, '../../', withAuthentication ? 'verdaccio_auth.yaml' : 'verdaccio.yaml'), + join(__dirname, '../', withAuthentication ? 'verdaccio_auth.yaml' : 'verdaccio.yaml'), ); configContent = configContent.replace(/\$\{HTTP_PORT\}/g, String(port)); configContent = configContent.replace(/\$\{HTTPS_PORT\}/g, String(httpsPort)); diff --git a/tests/legacy-cli/e2e/utils/test_process.ts b/tests/legacy-cli/e2e/utils/test_process.ts index dace5cb35b3b..af6bd61af365 100644 --- a/tests/legacy-cli/e2e/utils/test_process.ts +++ b/tests/legacy-cli/e2e/utils/test_process.ts @@ -1,3 +1,5 @@ +import { killAllProcesses } from './process'; + const testScript: string = process.argv[2]; const testModule = require(testScript); const testFunction: () => Promise | void = @@ -16,6 +18,6 @@ const testFunction: () => Promise | void = console.error('Test Process error', e); process.exitCode = -1; } finally { - process.exit(); + killAllProcesses().finally(() => process.exit()); } })(); diff --git a/tests/legacy-cli/e2e_runner.ts b/tests/legacy-cli/e2e_runner.ts index 0904e1ed67d9..01f5a6683c27 100644 --- a/tests/legacy-cli/e2e_runner.ts +++ b/tests/legacy-cli/e2e_runner.ts @@ -12,6 +12,7 @@ import { findFreePort } from './e2e/utils/network'; import { extractFile } from './e2e/utils/tar'; import { realpathSync } from 'node:fs'; import { PkgInfo } from './e2e/utils/packages'; +import { rm } from 'node:fs/promises'; Error.stackTraceLimit = Infinity; @@ -173,12 +174,16 @@ const tests = allTests.filter((name) => { }); }); +console.log(`Running with shard configuration:`); +console.log(`Total shards: ${nbShards}, current shard: ${shardId}`); + // Remove tests that are not part of this shard. const testsToRun = tests.filter((name, i) => shardId === null || i % nbShards == shardId); if (testsToRun.length === 0) { if (shardId !== null && tests.length <= shardId) { - console.log(`No tests to run on shard ${shardId}, exiting.`); + console.log(`No tests to run on shard ${shardId}, exiting`); + console.log(`Without sharding, there were ${tests.length} tests found.`); process.exit(0); } else { console.log(`No tests would be ran, aborting.`); @@ -328,7 +333,17 @@ async function runTest(absoluteName: string): Promise { process.chdir(join(getGlobalVariable('projects-root'), 'test-project')); await launchTestProcess(absoluteName); + await cleanTestProject(); +} + +async function cleanTestProject() { await gitClean(); + + const testProject = join(getGlobalVariable('projects-root'), 'test-project'); + + // Note: Dist directory is not cleared between tests, as `git clean` + // doesn't delete it. + await rm(join(testProject, 'dist/'), { recursive: true, force: true }); } function printHeader( diff --git a/tests/legacy-cli/rollup.config.mjs b/tests/legacy-cli/rollup.config.mjs new file mode 100644 index 000000000000..0fc2768c5057 --- /dev/null +++ b/tests/legacy-cli/rollup.config.mjs @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ +import { nodeResolve } from '@rollup/plugin-node-resolve'; +import commonjs from '@rollup/plugin-commonjs'; +import json from '@rollup/plugin-json'; +import glob from 'fast-glob'; + +const testFiles = [ + 'e2e_runner.js', + 'e2e/utils/test_process.js', + ...glob.sync('e2e/(initialize|setup|tests)/**/*.js'), +]; + +// Generate chunks to keep the original folder structure. +// Needed as we dynamically load these files. +const chunks = {}; +for (const file of testFiles) { + chunks[file.slice(0, -'.js'.length)] = file; +} + +export default { + input: chunks, + external: [], + plugins: [ + nodeResolve({ + preferBuiltins: true, + browser: false, + }), + json(), + commonjs({ + // Test runner uses dynamic requires, and those are fine. + // Rollup should not try to process them. + ignoreDynamicRequires: true, + }), + ], + output: { + dir: './runner_bundled_out', + exports: 'auto', + }, +}; diff --git a/tools/BUILD.bazel b/tools/BUILD.bazel index 88cc63790940..ab42b524c5d5 100644 --- a/tools/BUILD.bazel +++ b/tools/BUILD.bazel @@ -1,9 +1,16 @@ -load("@aspect_bazel_lib//lib/private:tar_toolchain.bzl", "tar_toolchain") load("@bazel_skylib//rules:copy_file.bzl", "copy_file") load("//tools:defaults2.bzl", "js_binary") package(default_visibility = ["//visibility:public"]) +platform( + name = "windows_x64", + constraint_values = [ + "@platforms//os:windows", + "@platforms//cpu:x86_64", + ], +) + exports_files([ "package_json_release_filter.jq", ]) @@ -25,18 +32,6 @@ js_binary( entry_point = "quicktype_runner.js", ) -tar_toolchain( - name = "system_tar_exec", - binary = "tar_system.bat", -) - -toolchain( - name = "windows_tar_system_toolchain", - exec_compatible_with = ["@platforms//os:windows"], - toolchain = ":system_tar_exec", - toolchain_type = "@aspect_bazel_lib//lib:tar_toolchain_type", -) - # TODO(devversion): Improve this by potentially sharing this common block. copy_file( name = "copy_worker_js", diff --git a/tools/interop.bzl b/tools/interop.bzl index bc3a98fec08e..e4f76822e66f 100644 --- a/tools/interop.bzl +++ b/tools/interop.bzl @@ -35,6 +35,7 @@ ts_deps_interop = rule( attrs = { "deps": attr.label_list(providers = [DeclarationInfo], mandatory = True), }, + toolchains = ["@devinfra//bazel/git-toolchain:toolchain_type"], ) def _ts_project_module_impl(ctx): @@ -106,6 +107,7 @@ def ts_project( testonly = False, visibility = None, ignore_strict_deps = False, + enable_runtime_rnjs_interop = True, **kwargs): interop_deps = [] @@ -115,11 +117,12 @@ def ts_project( # dependencies so that we can forward and capture the module mappings for runtime # execution, with regards to first-party dependency linking. rjs_modules_to_rnjs = [] - for d in deps: - if d.startswith("//:node_modules/"): - rjs_modules_to_rnjs.append(d.replace("//:node_modules/", "@npm//")) - if d.endswith("_rjs"): - rjs_modules_to_rnjs.append(d.replace("_rjs", "")) + if enable_runtime_rnjs_interop: + for d in deps: + if d.startswith("//:node_modules/"): + rjs_modules_to_rnjs.append(d.replace("//:node_modules/", "@npm//")) + if d.endswith("_rjs"): + rjs_modules_to_rnjs.append(d.replace("_rjs", "")) if tsconfig == None: tsconfig = "//:test-tsconfig" if testonly else "//:build-tsconfig" diff --git a/tools/rules_ts_windows.patch b/tools/rules_ts_windows.patch deleted file mode 100644 index 7a7cd342b0a5..000000000000 --- a/tools/rules_ts_windows.patch +++ /dev/null @@ -1,30 +0,0 @@ -diff --git a/ts/private/ts_project.bzl b/ts/private/ts_project.bzl -index 367bba0..a112f8f 100644 ---- a/ts/private/ts_project.bzl -+++ b/ts/private/ts_project.bzl -@@ -93,25 +93,6 @@ def _ts_project_impl(ctx): - elif ctx.attr.supports_workers == 0: - supports_workers = False - -- host_is_windows = platform_utils.host_platform_is_windows() -- if host_is_windows and supports_workers: -- supports_workers = False -- -- # buildifier: disable=print -- print("""\ --WARNING: disabling ts_project workers which are not currently supported on Windows hosts. --See https://github.com/aspect-build/rules_ts/issues/228 for more details. --""") -- -- if ctx.attr.is_typescript_5_or_greater and supports_workers: -- supports_workers = False -- -- # buildifier: disable=print -- print("""\ --WARNING: disabling ts_project workers which are not currently supported with TS >= 5.0.0. --See https://github.com/aspect-build/rules_ts/issues/361 for more details. --""") -- - if supports_workers: - execution_requirements["supports-workers"] = "1" - execution_requirements["worker-key-mnemonic"] = "TsProject" diff --git a/tools/tar_system.bat b/tools/tar_system.bat deleted file mode 100755 index c80a06339804..000000000000 --- a/tools/tar_system.bat +++ /dev/null @@ -1,7 +0,0 @@ -@ECHO OFF - -if exist "C:\Program Files\Git\bin\bash.exe" ( - set "BASH=C:\Program Files\Git\bin\bash.exe" -) - -"%BASH%" -c "tar %*" diff --git a/yarn.lock b/yarn.lock index f27e98e79a70..876ae9d3ce70 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,15 +55,15 @@ __metadata: languageName: node linkType: hard -"@angular/bazel@https://github.com/angular/bazel-builds.git#8faa06d66416ce78073ab59539ff03f5253b8d52": - version: 20.0.0-next.0+sha-51b8ff2 - resolution: "@angular/bazel@https://github.com/angular/bazel-builds.git#commit=8faa06d66416ce78073ab59539ff03f5253b8d52" +"@angular/bazel@https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65": + version: 19.3.0-next.0+sha-8657a0e + resolution: "@angular/bazel@https://github.com/angular/bazel-builds.git#commit=58e1a344eed2dfea489cd290a4b4a963f7e3ac65" dependencies: "@microsoft/api-extractor": "npm:^7.24.2" magic-string: "npm:^0.30.0" tslib: "npm:^2.3.0" peerDependencies: - "@angular/compiler-cli": 20.0.0-next.0+sha-51b8ff2 + "@angular/compiler-cli": 19.3.0-next.0+sha-8657a0e "@bazel/concatjs": ^5.3.0 "@bazel/worker": ^5.3.0 "@rollup/plugin-commonjs": ^28.0.0 @@ -80,7 +80,7 @@ __metadata: packager: ./src/ng_package/packager.mjs types_bundler: ./src/types_bundle/index.mjs xi18n: ./src/ngc-wrapped/extract_i18n.mjs - checksum: 10c0/c582964a0172c4a1f62964750b0f70766bbcac01bf1be85ca5b4dac623ef1fdaa92c38ce49ca08e0b0e1b97d8240faa42f06c6aa2d85810b0a3657e15b4b543e + checksum: 10c0/aa554ae9966a4a2a4057214c8811fd5e9fa8b27dd20af05043b181a6daadc6ae66c33a0c2acb23d7c355d1d8f368f9e7ee76f0155fb091aaedd3f15494e5828e languageName: node linkType: hard @@ -94,9 +94,9 @@ __metadata: languageName: node linkType: hard -"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592": - version: 0.0.0-b015169b635123c1ab9084f604e36b6342eac171 - resolution: "@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#commit=a6a996a69cfc03b3fbe538f11dd24b7bc4b30592" +"@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1": + version: 0.0.0-74aabba6d202918280dafe92f87f9c154476fa86 + resolution: "@angular/build-tooling@https://github.com/angular/dev-infra-private-build-tooling-builds.git#commit=d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1" dependencies: "@angular/benchpress": "npm:0.3.0" "@angular/build": "npm:19.2.0-next.1" @@ -133,7 +133,7 @@ __metadata: dependenciesMeta: re2: built: false - checksum: 10c0/e700e9ff2f8c55ad2d4983489271c0cef1d8b27d92a8a9d51ebcdad636f9f7b9f306d3960b6b8fd650e8376e3200ec9745bc38708c6cc7a23454afeda0f50aa6 + checksum: 10c0/9c7dc4060ae176d30fdd8763cd3942a6e9c53a49e43dc75e3a46f09a556588699091f2a5e813732c14ea43e01bca9b79fa3dceb595e8a0f4fcb8962df58209ad languageName: node linkType: hard @@ -299,8 +299,8 @@ __metadata: dependencies: "@ampproject/remapping": "npm:2.3.0" "@angular/animations": "npm:19.2.0" - "@angular/bazel": "https://github.com/angular/bazel-builds.git#8faa06d66416ce78073ab59539ff03f5253b8d52" - "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592" + "@angular/bazel": "https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65" + "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1" "@angular/cdk": "npm:19.2.1" "@angular/common": "npm:19.2.0" "@angular/compiler": "npm:19.2.0" @@ -327,7 +327,6 @@ __metadata: "@babel/runtime": "npm:7.26.9" "@bazel/bazelisk": "npm:1.25.0" "@bazel/buildifier": "npm:8.0.3" - "@bazel/runfiles": "npm:^6.0.0" "@discoveryjs/json-ext": "npm:0.6.3" "@eslint/compat": "npm:1.2.7" "@eslint/eslintrc": "npm:3.3.0" @@ -337,6 +336,7 @@ __metadata: "@listr2/prompt-adapter-inquirer": "npm:2.0.18" "@rollup/plugin-alias": "npm:^5.1.1" "@rollup/plugin-commonjs": "npm:^28.0.0" + "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^13.0.5" "@stylistic/eslint-plugin": "npm:^4.0.0" "@types/babel__core": "npm:7.20.5" @@ -1859,7 +1859,7 @@ __metadata: languageName: node linkType: hard -"@bazel/runfiles@npm:^6.0.0, @bazel/runfiles@npm:^6.3.1": +"@bazel/runfiles@npm:^6.3.1": version: 6.3.1 resolution: "@bazel/runfiles@npm:6.3.1" checksum: 10c0/7b542dcff9e917cc521520db137bd4f4a478796693700e2ec2c27f4beede800c9f4987e20c6b965d81000638f63549160780aea51eca2f0d0275be76fdc5e49f