Skip to content

Commit 0cabf62

Browse files
committed
feat(bazel): provide the node loader to load esm files in jasmine tests (#3025)
PR Close #3025
1 parent b99ca1e commit 0cabf62

File tree

6 files changed

+115
-3
lines changed

6 files changed

+115
-3
lines changed

bazel/jasmine/jasmine.bzl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
load("@aspect_rules_jasmine//jasmine:defs.bzl", _jasmine_test = "jasmine_test")
22

3-
def jasmine_test(name, data = [], node_options = [], **kwargs):
3+
def jasmine_test(name, data = [], tsconfig = None, node_options = [], env = {}, **kwargs):
4+
if tsconfig:
5+
env = dict(env, **{
6+
"NODE_OPTIONS_TSCONFIG_PATH": "$(rlocationpath %s)" % tsconfig,
7+
})
8+
49
_jasmine_test(
510
name = name,
611
data = data + [
712
"@devinfra//bazel/jasmine:stack-traces",
13+
"@devinfra//bazel/private/node_loader:node_loader",
814
],
15+
env = env,
916
size = kwargs.pop("size", "medium"),
1017
node_options = [
11-
"--import",
12-
"$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/jasmine:stack-traces)",
18+
"--import=$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/private/node_loader:node_loader)",
19+
"--import=$$JS_BINARY__RUNFILES/$(rlocationpath @devinfra//bazel/jasmine:stack-traces)",
1320
] + node_options,
1421
**kwargs
1522
)

bazel/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"@types/yargs": "17.0.33",
1313
"browser-sync": "3.0.4",
1414
"chalk": "5.6.0",
15+
"get-tsconfig": "4.10.1",
1516
"piscina": "^5.0.0",
1617
"send": "1.2.0",
1718
"true-case-path": "2.2.1",
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@aspect_rules_js//js:defs.bzl", "js_library")
2+
3+
js_library(
4+
name = "node_loader_lib",
5+
srcs = ["hooks.mjs"],
6+
visibility = ["//visibility:public"],
7+
deps = ["//bazel:node_modules/get-tsconfig"],
8+
)
9+
10+
js_library(
11+
name = "node_loader",
12+
srcs = ["index.mjs"],
13+
visibility = ["//visibility:public"],
14+
deps = [":node_loader_lib"],
15+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
/**
10+
* @fileoverview
11+
*
12+
* Module loader that augments NodeJS's execution to:
13+
*
14+
* - support native execution of Angular JavaScript output
15+
* that isn't strict ESM at this point (lack of explicit extensions).
16+
* - support path mappings at runtime. This allows us to natively execute ESM
17+
* without having to pre-bundle for testing, or use the slow full npm linked packages
18+
*/
19+
20+
import {parseTsconfig, createPathsMatcher} from 'get-tsconfig';
21+
22+
import path from 'node:path';
23+
24+
const explicitExtensionRe = /\.[mc]?js$/;
25+
const nonModuleImportRe = /^[.\/]/;
26+
27+
const runfilesRoot = process.env.JS_BINARY__RUNFILES;
28+
const tsconfigPath = process.env.NODE_OPTIONS_TSCONFIG_PATH;
29+
30+
let pathMappingMatcher;
31+
// When no tsconfig is provided no match can be generated so we always return an empty list.
32+
if (tsconfigPath === undefined) {
33+
pathMappingMatcher = () => [];
34+
} else {
35+
const tsconfigFullPath = path.join(runfilesRoot, tsconfigPath);
36+
const tsconfig = parseTsconfig(tsconfigFullPath);
37+
pathMappingMatcher = createPathsMatcher({config: tsconfig, path: tsconfigFullPath});
38+
}
39+
40+
/** @type {import('module').ResolveHook} */
41+
export const resolve = async (specifier, context, nextResolve) => {
42+
// True when it's a non-module import without explicit extensions.
43+
const isNonModuleExtensionlessImport =
44+
nonModuleImportRe.test(specifier) && !explicitExtensionRe.test(specifier);
45+
const pathMappings = !nonModuleImportRe.test(specifier) ? pathMappingMatcher(specifier) : [];
46+
47+
// If it's neither path mapped, nor an extension-less import that may be fixed up, exit early.
48+
if (!isNonModuleExtensionlessImport && pathMappings.length === 0) {
49+
return nextResolve(specifier, context);
50+
}
51+
52+
if (pathMappings.length > 0) {
53+
for (const mapping of pathMappings) {
54+
const res = await resolve(mapping, context, nextResolve).catch(() => null);
55+
if (res !== null) {
56+
return res;
57+
}
58+
}
59+
} else {
60+
const specifiers = [
61+
`${specifier}.js`,
62+
`${specifier}/index.js`,
63+
// Legacy variants for the `zone.js` variant using still `ts_library`.
64+
// TODO(rules_js migration): Remove this.
65+
`${specifier}.mjs`,
66+
`${specifier}/index.mjs`,
67+
];
68+
for (const specifier of specifiers) {
69+
try {
70+
return await nextResolve(specifier, context);
71+
} catch {}
72+
}
73+
}
74+
return nextResolve(specifier, context);
75+
};
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {register} from 'node:module';
10+
11+
register('./hooks.mjs', {parentURL: import.meta.url});

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)