From b0196413d59529a75c91e61b23edd88518340867 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! --- .../npm_translate_lock_MzA5NzUwNzMx | 8 +- .bazelrc | 14 +- .../windows-bazel-test/action.yml | 79 ++++++++ .github/workflows/ci.yml | 39 ++-- .github/workflows/pr.yml | 24 +-- WORKSPACE | 36 ++-- goldens/BUILD.bazel | 4 +- package.json | 4 +- 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 | 24 +-- 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 | 14 +- 53 files changed, 728 insertions(+), 226 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 47239f886644..7899ead468a1 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=817786158 +package.json=875297300 packages/angular/build/package.json=1540368256 packages/angular/cli/package.json=-1917515334 packages/angular/pwa/package.json=1108903917 -packages/angular/ssr/package.json=708248541 +packages/angular/ssr/package.json=-149631110 packages/angular_devkit/architect/package.json=-1496633956 packages/angular_devkit/architect_cli/package.json=1551210941 packages/angular_devkit/build_angular/package.json=-221067535 @@ -17,6 +17,6 @@ packages/angular_devkit/schematics/package.json=673943597 packages/angular_devkit/schematics_cli/package.json=-2026655035 packages/ngtools/webpack/package.json=605871936 packages/schematics/angular/package.json=251715148 -pnpm-lock.yaml=-555932675 +pnpm-lock.yaml=-10828064 pnpm-workspace.yaml=-1056556036 -yarn.lock=-1678795202 +yarn.lock=-1549158718 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 ffcac1bd8d5a..2f32eba264a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -74,23 +74,12 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest] + os: [ubuntu-latest] node: [18, 20, 22] subset: [npm, esbuild] shard: [0, 1, 2, 3, 4, 5] - exclude: - # Skip Node.js v18 tests on Windows - - os: windows-latest - node: 18 - # 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@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - name: Install node modules @@ -100,7 +89,27 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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 @@ -122,7 +131,7 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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 @@ -144,7 +153,7 @@ jobs: - name: Setup Bazel RBE uses: angular/dev-infra/github-actions/bazel/configure-remote@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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 e25534f4eb85..3478072da4e1 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@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - - name: Install node modules - run: yarn install --immutable - - name: Setup Bazel - uses: angular/dev-infra/github-actions/bazel/setup@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - - name: Setup Bazel RBE - uses: angular/dev-infra/github-actions/bazel/configure-remote@0ad6a370f70638e785d6ef1f90dc6ede34684a47 + 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@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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@0ad6a370f70638e785d6ef1f90dc6ede34684a47 - 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 a8eba9a32ccb..a23dda782d09 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -2,12 +2,6 @@ workspace(name = "angular_cli") DEFAULT_NODE_VERSION = "18.19.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( @@ -160,17 +154,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( @@ -219,6 +202,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", @@ -230,8 +217,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", @@ -267,7 +252,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", ) @@ -278,3 +263,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 7c8b35ecc5b1..e056b7e76837 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@ampproject/remapping": "2.3.0", "@angular/animations": "19.2.0-rc.0", "@angular/bazel": "https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65", - "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592", + "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1", "@angular/cdk": "19.2.0-rc.0", "@angular/common": "19.2.0-rc.0", "@angular/compiler": "19.2.0-rc.0", @@ -72,13 +72,13 @@ "@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", "@inquirer/confirm": "5.1.6", "@inquirer/prompts": "7.3.2", "@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": "^3.0.0", "@types/babel__core": "7.20.5", diff --git a/packages/angular/build/BUILD.bazel b/packages/angular/build/BUILD.bazel index c15cc3a44694..218c114da4a6 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") @@ -313,6 +313,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 ca442a031acf..34c342a58167 100644 --- a/packages/angular/ssr/package.json +++ b/packages/angular/ssr/package.json @@ -35,7 +35,6 @@ "@angular/platform-browser": "19.2.0-rc.0", "@angular/platform-server": "19.2.0-rc.0", "@angular/router": "19.2.0-rc.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 2baa11919c79..eae365a75e35 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") @@ -277,8 +277,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 cf0ebd9c7e13..62470a59c050 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") @@ -87,7 +87,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 7b4fa6fc5c8f..87a0b2737285 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,8 +26,8 @@ importers: specifier: https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65 version: github.com/angular/bazel-builds/58e1a344eed2dfea489cd290a4b4a963f7e3ac65(@angular/compiler-cli@19.2.0-rc.0)(@rollup/plugin-commonjs@28.0.2)(@rollup/plugin-node-resolve@13.3.0)(@types/node@18.19.76)(rollup-plugin-sourcemaps@0.6.3)(rollup@4.34.8)(terser@5.39.0)(typescript@5.8.1-rc) '@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.1)(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.1)(terser@5.39.0)(zone.js@0.15.0) '@angular/cdk': specifier: 19.2.0-rc.0 version: 19.2.0-rc.0(@angular/common@19.2.0-rc.0)(@angular/core@19.2.0-rc.0)(rxjs@7.8.1) @@ -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 @@ -127,6 +124,9 @@ importers: '@rollup/plugin-commonjs': specifier: ^28.0.0 version: 28.0.2(rollup@4.34.8) + '@rollup/plugin-json': + specifier: ^6.1.0 + version: 6.1.0(rollup@4.34.8) '@rollup/plugin-node-resolve': specifier: ^13.0.5 version: 13.3.0(rollup@4.34.8) @@ -720,9 +720,6 @@ importers: '@angular/router': specifier: 19.2.0-rc.0 version: 19.2.0-rc.0(@angular/common@19.2.0-rc.0)(@angular/core@19.2.0-rc.0)(@angular/platform-browser@19.2.0-rc.0)(rxjs@7.8.1) - '@bazel/runfiles': - specifier: ^6.0.0 - version: 6.3.1 '@schematics/angular': specifier: workspace:* version: link:../../schematics/angular @@ -2366,7 +2363,6 @@ packages: /@bazel/typescript@5.8.1(typescript@5.8.1-rc): 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.1-rc @@ -11550,7 +11546,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) @@ -14251,11 +14247,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.1)(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.1)(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.1)(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 04292bb17711..9bb7ed296cc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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 @@ -300,7 +300,7 @@ __metadata: "@ampproject/remapping": "npm:2.3.0" "@angular/animations": "npm:19.2.0-rc.0" "@angular/bazel": "https://github.com/angular/bazel-builds.git#58e1a344eed2dfea489cd290a4b4a963f7e3ac65" - "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#a6a996a69cfc03b3fbe538f11dd24b7bc4b30592" + "@angular/build-tooling": "https://github.com/angular/dev-infra-private-build-tooling-builds.git#d4727212a9d0f7eb63ae3116d73c769d9bd0bdc1" "@angular/cdk": "npm:19.2.0-rc.0" "@angular/common": "npm:19.2.0-rc.0" "@angular/compiler": "npm:19.2.0-rc.0" @@ -327,13 +327,13 @@ __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" "@inquirer/confirm": "npm:5.1.6" "@inquirer/prompts": "npm:7.3.2" "@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:^3.0.0" "@types/babel__core": "npm:7.20.5" @@ -1854,7 +1854,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