From 1325949594cf84449596dc1440666a61a0847e04 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Fri, 29 Aug 2025 17:21:15 +0000 Subject: [PATCH 1/2] feat(bazel): create a base jasmine_test with source map and relative path rewrite support --- bazel/jasmine/BUILD.bazel | 14 ++++++++++++++ bazel/jasmine/jasmine.bzl | 15 +++++++++++++++ bazel/jasmine/stack-traces.mjs | 19 +++++++++++++++++++ bazel/package.json | 2 ++ pnpm-lock.yaml | 26 ++++++++++++++++++++++++++ tools/defaults.bzl | 3 +-- 6 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 bazel/jasmine/BUILD.bazel create mode 100644 bazel/jasmine/jasmine.bzl create mode 100644 bazel/jasmine/stack-traces.mjs diff --git a/bazel/jasmine/BUILD.bazel b/bazel/jasmine/BUILD.bazel new file mode 100644 index 000000000..c2c841877 --- /dev/null +++ b/bazel/jasmine/BUILD.bazel @@ -0,0 +1,14 @@ +load("@aspect_rules_js//js:defs.bzl", "js_library") + +js_library( + name = "stack-traces", + srcs = [ + "stack-traces.mjs", + ], + visibility = [ + "//visibility:public", + ], + deps = [ + "//bazel:node_modules/source-map-support", + ], +) diff --git a/bazel/jasmine/jasmine.bzl b/bazel/jasmine/jasmine.bzl new file mode 100644 index 000000000..1b810cabf --- /dev/null +++ b/bazel/jasmine/jasmine.bzl @@ -0,0 +1,15 @@ +load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test") + +def jasmine_test(name, data = [], node_options = [], **kwargs): + _jasmine_test( + name = name, + data = data + [ + "@devinfra//bazel/jasmine:stack-traces", + ], + size = kwargs.pop("size", "medium"), + node_options = [ + "--import", + "$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/jasmine:stack-traces)", + ] + node_options, + **kwargs + ) diff --git a/bazel/jasmine/stack-traces.mjs b/bazel/jasmine/stack-traces.mjs new file mode 100644 index 000000000..95f9d1538 --- /dev/null +++ b/bazel/jasmine/stack-traces.mjs @@ -0,0 +1,19 @@ +import {install} from 'source-map-support'; + +// Set up source map support to ensure we are logging the stack trace for the source file (i.e. .ts file) and +// not the generated file (i.e. .js. file). +install(); + +/** The root path that the test files are running from within. */ +let rootPath = `${process.env.RUNFILES}/${process.env.TEST_WORKSPACE}/`; +/** The root path match for when test files are not within the sandbox, but the executation is happening within the sandbox. */ +let sandboxPath = `/.*${process.env.JS_BINARY__WORKSPACE}/${process.env.JS_BINARY__BINDIR}/`; +/** Regex to capture the content and name of the function in the stack trace. */ +const basePathRegex = new RegExp(`(at.*)(?:file.*${rootPath}|file.*${sandboxPath})`, 'g'); + +// Replace the prepareStackTrace function with one which replaces the full path execution location with +// relative paths to the base of the workspace source files. +const originalPrepareStackTrace = Error.prepareStackTrace; +Error.prepareStackTrace = function (e, s) { + return originalPrepareStackTrace(e, s).replaceAll(basePathRegex, '$1./'); +}; diff --git a/bazel/package.json b/bazel/package.json index 94cf5705b..36ca06da2 100644 --- a/bazel/package.json +++ b/bazel/package.json @@ -8,6 +8,7 @@ "@types/selenium-webdriver": "^4.1.28", "@types/send": "0.17.5", "@types/wait-on": "^5.3.4", + "@types/source-map-support": "0.5.10", "@types/yargs": "17.0.33", "browser-sync": "3.0.4", "chalk": "5.6.0", @@ -20,6 +21,7 @@ "protractor": "7.0.0", "semver": "7.7.2", "selenium-webdriver": "4.35.0", + "source-map-support": "0.5.21", "tinyglobby": "0.2.14" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0019ac850..11760ee22 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,6 +214,9 @@ importers: '@types/send': specifier: 0.17.5 version: 0.17.5 + '@types/source-map-support': + specifier: 0.5.10 + version: 0.5.10 '@types/wait-on': specifier: ^5.3.4 version: 5.3.4 @@ -241,6 +244,9 @@ importers: send: specifier: 1.2.0 version: 1.2.0(supports-color@10.2.0) + source-map-support: + specifier: 0.5.21 + version: 0.5.21 tinyglobby: specifier: 0.2.14 version: 0.2.14 @@ -2369,6 +2375,9 @@ packages: '@types/serve-static@1.15.8': resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==} + '@types/source-map-support@0.5.10': + resolution: {integrity: sha512-tgVP2H469x9zq34Z0m/fgPewGhg/MLClalNOiPIzQlXrSS2YrKu/xCdSCKnEDwkFha51VKEKB6A9wW26/ZNwzA==} + '@types/stack-trace@0.0.33': resolution: {integrity: sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==} @@ -2724,6 +2733,9 @@ packages: buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} @@ -5250,6 +5262,9 @@ packages: source-map-support@0.4.18: resolution: {integrity: sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==} + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + source-map@0.5.7: resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} engines: {node: '>=0.10.0'} @@ -8000,6 +8015,10 @@ snapshots: '@types/node': 24.3.0 '@types/send': 0.17.5 + '@types/source-map-support@0.5.10': + dependencies: + source-map: 0.6.1 + '@types/stack-trace@0.0.33': {} '@types/supports-color@10.0.0': @@ -8413,6 +8432,8 @@ snapshots: buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} + buffer@5.7.1: dependencies: base64-js: 1.5.1 @@ -11422,6 +11443,11 @@ snapshots: dependencies: source-map: 0.5.7 + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + source-map@0.5.7: {} source-map@0.6.1: {} diff --git a/tools/defaults.bzl b/tools/defaults.bzl index fb2f07d8e..e5c3171b4 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -1,7 +1,6 @@ load("@aspect_bazel_lib//lib:copy_to_bin.bzl", _copy_to_bin = "copy_to_bin") load("@aspect_bazel_lib//lib:write_source_files.bzl", _write_source_file = "write_source_file") load("@aspect_rules_esbuild//esbuild:defs.bzl", _esbuild = "esbuild") -load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test") load("@aspect_rules_js//js:defs.bzl", _js_binary = "js_binary") load("@aspect_rules_js//npm:defs.bzl", _npm_package = "npm_package") load("@aspect_rules_ts//ts:defs.bzl", _ts_config = "ts_config") @@ -10,6 +9,7 @@ load("@rules_angular//src/ng_project:index.bzl", _ng_project = "ng_project") load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project") load("@rules_sass//src:index.bzl", _npm_sass_library = "npm_sass_library", _sass_binary = "sass_binary") load("//bazel:extract_types.bzl", _extract_types = "extract_types") +load("//bazel/jasmine:jasmine.bzl", _jasmine_test = "jasmine_test") load("//bazel/ts_project:index.bzl", _strict_deps_test = "strict_deps_test") copy_to_bin = _copy_to_bin @@ -110,7 +110,6 @@ def jasmine_test(name, **kwargs): _jasmine_test( name = name, node_modules = "//:node_modules", - chdir = native.package_name(), fixed_args = [ "'**/*+(.|_)spec.js'", ], From ca1e4b1de162901edb93ea1d49f40447fa708536 Mon Sep 17 00:00:00 2001 From: Joey Perrott Date: Tue, 2 Sep 2025 19:07:42 +0000 Subject: [PATCH 2/2] feat(bazel): provide the node loader to load esm files in jasmine tests --- bazel/jasmine/jasmine.bzl | 13 +++-- bazel/package.json | 1 + bazel/private/node_loader/BUILD.bazel | 15 ++++++ bazel/private/node_loader/hooks.mjs | 75 +++++++++++++++++++++++++++ bazel/private/node_loader/index.mjs | 11 ++++ pnpm-lock.yaml | 3 ++ 6 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 bazel/private/node_loader/BUILD.bazel create mode 100644 bazel/private/node_loader/hooks.mjs create mode 100644 bazel/private/node_loader/index.mjs diff --git a/bazel/jasmine/jasmine.bzl b/bazel/jasmine/jasmine.bzl index 1b810cabf..988020d3a 100644 --- a/bazel/jasmine/jasmine.bzl +++ b/bazel/jasmine/jasmine.bzl @@ -1,15 +1,22 @@ load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test") -def jasmine_test(name, data = [], node_options = [], **kwargs): +def jasmine_test(name, data = [], tsconfig = None, node_options = [], env = {}, **kwargs): + if tsconfig: + env = dict(env, **{ + "NODE_OPTIONS_TSCONFIG_PATH": "$(rlocationpath %s)" % tsconfig, + }) + _jasmine_test( name = name, data = data + [ "@devinfra//bazel/jasmine:stack-traces", + "@devinfra//bazel/private/node_loader:node_loader", ], + env = env, size = kwargs.pop("size", "medium"), node_options = [ - "--import", - "$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/jasmine:stack-traces)", + "--import=$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/private/node_loader:node_loader)", + "--import=$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/jasmine:stack-traces)", ] + node_options, **kwargs ) diff --git a/bazel/package.json b/bazel/package.json index 36ca06da2..5cd842344 100644 --- a/bazel/package.json +++ b/bazel/package.json @@ -12,6 +12,7 @@ "@types/yargs": "17.0.33", "browser-sync": "3.0.4", "chalk": "5.6.0", + "get-tsconfig": "4.10.1", "piscina": "^5.0.0", "send": "1.2.0", "true-case-path": "2.2.1", diff --git a/bazel/private/node_loader/BUILD.bazel b/bazel/private/node_loader/BUILD.bazel new file mode 100644 index 000000000..e90b34241 --- /dev/null +++ b/bazel/private/node_loader/BUILD.bazel @@ -0,0 +1,15 @@ +load("@aspect_rules_js//js:defs.bzl", "js_library") + +js_library( + name = "node_loader_lib", + srcs = ["hooks.mjs"], + visibility = ["//visibility:public"], + deps = ["//bazel:node_modules/get-tsconfig"], +) + +js_library( + name = "node_loader", + srcs = ["index.mjs"], + visibility = ["//visibility:public"], + deps = [":node_loader_lib"], +) diff --git a/bazel/private/node_loader/hooks.mjs b/bazel/private/node_loader/hooks.mjs new file mode 100644 index 000000000..843feea58 --- /dev/null +++ b/bazel/private/node_loader/hooks.mjs @@ -0,0 +1,75 @@ +/** + * @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 + */ + +/** + * @fileoverview + * + * Module loader that augments NodeJS's execution to: + * + * - support native execution of Angular JavaScript output + * that isn't strict ESM at this point (lack of explicit extensions). + * - support path mappings at runtime. This allows us to natively execute ESM + * without having to pre-bundle for testing, or use the slow full npm linked packages + */ + +import {parseTsconfig, createPathsMatcher} from 'get-tsconfig'; + +import path from 'node:path'; + +const explicitExtensionRe = /\.[mc]?js$/; +const nonModuleImportRe = /^[.\/]/; + +const runfilesRoot = process.env.JS_BINARY__RUNFILES; +const tsconfigPath = process.env.NODE_OPTIONS_TSCONFIG_PATH; + +let pathMappingMatcher; +// When no tsconfig is provided no match can be generated so we always return an empty list. +if (tsconfigPath === undefined) { + pathMappingMatcher = () => []; +} else { + const tsconfigFullPath = path.join(runfilesRoot, tsconfigPath); + const tsconfig = parseTsconfig(tsconfigFullPath); + pathMappingMatcher = createPathsMatcher({config: tsconfig, path: tsconfigFullPath}); +} + +/** @type {import('module').ResolveHook} */ +export const resolve = async (specifier, context, nextResolve) => { + // True when it's a non-module import without explicit extensions. + const isNonModuleExtensionlessImport = + nonModuleImportRe.test(specifier) && !explicitExtensionRe.test(specifier); + const pathMappings = !nonModuleImportRe.test(specifier) ? pathMappingMatcher(specifier) : []; + + // If it's neither path mapped, nor an extension-less import that may be fixed up, exit early. + if (!isNonModuleExtensionlessImport && pathMappings.length === 0) { + return nextResolve(specifier, context); + } + + if (pathMappings.length > 0) { + for (const mapping of pathMappings) { + const res = await resolve(mapping, context, nextResolve).catch(() => null); + if (res !== null) { + return res; + } + } + } else { + const specifiers = [ + `${specifier}.js`, + `${specifier}/index.js`, + // Legacy variants for the `zone.js` variant using still `ts_library`. + // TODO(rules_js migration): Remove this. + `${specifier}.mjs`, + `${specifier}/index.mjs`, + ]; + for (const specifier of specifiers) { + try { + return await nextResolve(specifier, context); + } catch {} + } + } + return nextResolve(specifier, context); +}; diff --git a/bazel/private/node_loader/index.mjs b/bazel/private/node_loader/index.mjs new file mode 100644 index 000000000..22bc24ff5 --- /dev/null +++ b/bazel/private/node_loader/index.mjs @@ -0,0 +1,11 @@ +/** + * @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 {register} from 'node:module'; + +register('./hooks.mjs', {parentURL: import.meta.url}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11760ee22..3ddb66896 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: chalk: specifier: 5.6.0 version: 5.6.0 + get-tsconfig: + specifier: 4.10.1 + version: 4.10.1 piscina: specifier: ^5.0.0 version: 5.1.3