@backstage/cli 0.29.5-next.1 → 0.30.0-next.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/config/jest.js +98 -62
- package/config/jestCachingModuleLoader.js +14 -0
- package/config/jestSwcTransform.js +6 -0
- package/config/nodeTransform.cjs +10 -1
- package/config/nodeTransformHooks.mjs +282 -0
- package/config/tsconfig.json +1 -0
- package/dist/commands/index.cjs.js +24 -34
- package/dist/commands/versions/bump.cjs.js +10 -9
- package/dist/lib/builder/config.cjs.js +60 -7
- package/dist/lib/builder/packager.cjs.js +1 -1
- package/dist/lib/bundler/server.cjs.js +56 -140
- package/dist/lib/lazy.cjs.js +4 -2
- package/dist/modules/config/index.cjs.js +4 -4
- package/dist/packages/backend-defaults/package.json.cjs.js +1 -1
- package/dist/packages/backend-plugin-api/package.json.cjs.js +1 -1
- package/dist/packages/backend-test-utils/package.json.cjs.js +1 -1
- package/dist/packages/catalog-client/package.json.cjs.js +1 -1
- package/dist/packages/cli/package.json.cjs.js +3 -6
- package/dist/packages/config/package.json.cjs.js +1 -1
- package/dist/packages/core-app-api/package.json.cjs.js +1 -1
- package/dist/packages/core-components/package.json.cjs.js +1 -1
- package/dist/packages/core-plugin-api/package.json.cjs.js +1 -1
- package/dist/packages/dev-utils/package.json.cjs.js +1 -1
- package/dist/packages/errors/package.json.cjs.js +1 -1
- package/dist/packages/test-utils/package.json.cjs.js +1 -1
- package/dist/plugins/auth-backend/package.json.cjs.js +1 -1
- package/dist/plugins/auth-backend-module-guest-provider/package.json.cjs.js +1 -1
- package/dist/plugins/catalog-node/package.json.cjs.js +1 -1
- package/dist/plugins/scaffolder-node/package.json.cjs.js +1 -1
- package/dist/plugins/scaffolder-node-test-utils/package.json.cjs.js +1 -1
- package/package.json +31 -49
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @backstage/cli
|
|
2
2
|
|
|
3
|
+
## 0.30.0-next.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- cb76663: **BREAKING**: Add support for native ESM in Node.js code. This changes the behavior of dynamic import expressions in Node.js code. Typically this can be fixed by replacing `import(...)` with `require(...)`, with an `as typeof import(...)` cast if needed for types. This is because dynamic imports will no longer be transformed to `require(...)` calls, but instead be left as-is. This in turn allows you to load ESM modules from CommonJS code using `import(...)`.
|
|
8
|
+
|
|
9
|
+
This change adds support for the following in Node.js packages, across type checking, package builds, runtime transforms and Jest tests:
|
|
10
|
+
|
|
11
|
+
- Dynamic imports that load ESM modules from CommonJS code.
|
|
12
|
+
- Both `.mjs` and `.mts` files as explicit ESM files, as well as `.cjs` and `.cts` as explicit CommonJS files.
|
|
13
|
+
- Support for the `"type": "module"` field in `package.json` to indicate that the package is an ESM package.
|
|
14
|
+
|
|
15
|
+
There are a few caveats to be aware of:
|
|
16
|
+
|
|
17
|
+
- To enable support for native ESM in tests, you need to run the tests with the `--experimental-vm-modules` flag enabled, typically via `NODE_OPTIONS='--experimental-vm-modules'`.
|
|
18
|
+
- Declaring a package as `"type": "module"` in `package.json` is supported, but in tests it will cause all local transitive dependencies to also be treated as ESM, regardless of whether they declare `"type": "module"` or not.
|
|
19
|
+
- Node.js has an [ESM interoperability layer with CommonJS](https://nodejs.org/docs/latest-v22.x/api/esm.html#interoperability-with-commonjs) that allows for imports from ESM to identify named exports in CommonJS packages. This interoperability layer is **only** enabled when importing packages with a `.cts` or `.cjs` extension. This is because the interoperability layer is not fully compatible with the NPM ecosystem, and would break package if it was enabled for `.js` files.
|
|
20
|
+
- Dynamic imports of CommonJS packages will vary in shape depending on the runtime, i.e. test vs local development, etc. It is therefore recommended to avoid dynamic imports of CommonJS packages and instead use `require`, or to use the explicit CommonJS extensions as mentioned above. If you do need to dynamically import CommonJS packages, avoid using `default` exports, as the shape of them vary across different environments and you would otherwise need to manually unwrap the import based on the shape of the module object.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- f21b125: Ensure that both global-agent and undici agents are enabled when proxying is enabled.
|
|
25
|
+
- Updated dependencies
|
|
26
|
+
- @backstage/cli-node@0.2.13-next.0
|
|
27
|
+
- @backstage/config-loader@1.9.6-next.0
|
|
28
|
+
- @backstage/catalog-model@1.7.3
|
|
29
|
+
- @backstage/cli-common@0.1.15
|
|
30
|
+
- @backstage/config@1.3.2
|
|
31
|
+
- @backstage/errors@1.2.7
|
|
32
|
+
- @backstage/eslint-plugin@0.1.10
|
|
33
|
+
- @backstage/integration@1.16.1
|
|
34
|
+
- @backstage/release-manifests@0.0.12
|
|
35
|
+
- @backstage/types@1.2.1
|
|
36
|
+
|
|
37
|
+
## 0.29.5
|
|
38
|
+
|
|
39
|
+
### Patch Changes
|
|
40
|
+
|
|
41
|
+
- e937ce0: Fixed incompatible `@typescript-eslint` versions with current `eslint@8.x.x`
|
|
42
|
+
- 8557e09: Removed the `EXPERIMENTAL_VITE` flag for using Vite as a dev server. If you were using this feature, we recommend switching to Rspack via the `EXPERIMENTAL_RSPACK` flag.
|
|
43
|
+
- Updated dependencies
|
|
44
|
+
- @backstage/types@1.2.1
|
|
45
|
+
- @backstage/config-loader@1.9.5
|
|
46
|
+
- @backstage/integration@1.16.1
|
|
47
|
+
- @backstage/catalog-model@1.7.3
|
|
48
|
+
- @backstage/cli-common@0.1.15
|
|
49
|
+
- @backstage/cli-node@0.2.12
|
|
50
|
+
- @backstage/config@1.3.2
|
|
51
|
+
- @backstage/errors@1.2.7
|
|
52
|
+
- @backstage/eslint-plugin@0.1.10
|
|
53
|
+
- @backstage/release-manifests@0.0.12
|
|
54
|
+
|
|
3
55
|
## 0.29.5-next.1
|
|
4
56
|
|
|
5
57
|
### Patch Changes
|
package/config/jest.js
CHANGED
|
@@ -31,6 +31,14 @@ const FRONTEND_ROLES = [
|
|
|
31
31
|
'frontend-plugin-module',
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
const NODE_ROLES = [
|
|
35
|
+
'backend',
|
|
36
|
+
'cli',
|
|
37
|
+
'node-library',
|
|
38
|
+
'backend-plugin',
|
|
39
|
+
'backend-plugin-module',
|
|
40
|
+
];
|
|
41
|
+
|
|
34
42
|
const envOptions = {
|
|
35
43
|
oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS),
|
|
36
44
|
};
|
|
@@ -130,11 +138,97 @@ const transformIgnorePattern = [
|
|
|
130
138
|
].join('|');
|
|
131
139
|
|
|
132
140
|
// Provides additional config that's based on the role of the target package
|
|
133
|
-
function getRoleConfig(role) {
|
|
141
|
+
function getRoleConfig(role, pkgJson) {
|
|
142
|
+
// Only Node.js package roles support native ESM modules, frontend and common
|
|
143
|
+
// packages are always transpiled to CommonJS.
|
|
144
|
+
const moduleOpts = NODE_ROLES.includes(role)
|
|
145
|
+
? {
|
|
146
|
+
module: {
|
|
147
|
+
ignoreDynamic: true,
|
|
148
|
+
exportInteropAnnotation: true,
|
|
149
|
+
},
|
|
150
|
+
}
|
|
151
|
+
: undefined;
|
|
152
|
+
|
|
153
|
+
const transform = {
|
|
154
|
+
'\\.(mjs|cjs|js)$': [
|
|
155
|
+
require.resolve('./jestSwcTransform'),
|
|
156
|
+
{
|
|
157
|
+
...moduleOpts,
|
|
158
|
+
jsc: {
|
|
159
|
+
parser: {
|
|
160
|
+
syntax: 'ecmascript',
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
'\\.jsx$': [
|
|
166
|
+
require.resolve('./jestSwcTransform'),
|
|
167
|
+
{
|
|
168
|
+
jsc: {
|
|
169
|
+
parser: {
|
|
170
|
+
syntax: 'ecmascript',
|
|
171
|
+
jsx: true,
|
|
172
|
+
},
|
|
173
|
+
transform: {
|
|
174
|
+
react: {
|
|
175
|
+
runtime: 'automatic',
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
'\\.(mts|cts|ts)$': [
|
|
182
|
+
require.resolve('./jestSwcTransform'),
|
|
183
|
+
{
|
|
184
|
+
...moduleOpts,
|
|
185
|
+
jsc: {
|
|
186
|
+
parser: {
|
|
187
|
+
syntax: 'typescript',
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
'\\.tsx$': [
|
|
193
|
+
require.resolve('./jestSwcTransform'),
|
|
194
|
+
{
|
|
195
|
+
jsc: {
|
|
196
|
+
parser: {
|
|
197
|
+
syntax: 'typescript',
|
|
198
|
+
tsx: true,
|
|
199
|
+
},
|
|
200
|
+
transform: {
|
|
201
|
+
react: {
|
|
202
|
+
runtime: 'automatic',
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
'\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$':
|
|
209
|
+
require.resolve('./jestFileTransform.js'),
|
|
210
|
+
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
|
|
211
|
+
};
|
|
134
212
|
if (FRONTEND_ROLES.includes(role)) {
|
|
135
|
-
return {
|
|
213
|
+
return {
|
|
214
|
+
testEnvironment: require.resolve('jest-environment-jsdom'),
|
|
215
|
+
transform,
|
|
216
|
+
};
|
|
136
217
|
}
|
|
137
|
-
return {
|
|
218
|
+
return {
|
|
219
|
+
testEnvironment: require.resolve('jest-environment-node'),
|
|
220
|
+
moduleFileExtensions: [...SRC_EXTS, 'json', 'node'],
|
|
221
|
+
// Jest doesn't let us dynamically detect type=module per transformed file,
|
|
222
|
+
// so we have to assume that if the entry point is ESM, all TS files are
|
|
223
|
+
// ESM.
|
|
224
|
+
//
|
|
225
|
+
// This means you can't switch a package to type=module until all of its
|
|
226
|
+
// monorepo dependencies are also type=module or does not contain any .ts
|
|
227
|
+
// files.
|
|
228
|
+
extensionsToTreatAsEsm:
|
|
229
|
+
pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'],
|
|
230
|
+
transform,
|
|
231
|
+
};
|
|
138
232
|
}
|
|
139
233
|
|
|
140
234
|
async function getProjectConfig(targetPath, extraConfig, extraOptions) {
|
|
@@ -160,64 +254,6 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) {
|
|
|
160
254
|
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
|
161
255
|
},
|
|
162
256
|
|
|
163
|
-
transform: {
|
|
164
|
-
'\\.(mjs|cjs|js)$': [
|
|
165
|
-
require.resolve('./jestSwcTransform'),
|
|
166
|
-
{
|
|
167
|
-
jsc: {
|
|
168
|
-
parser: {
|
|
169
|
-
syntax: 'ecmascript',
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
],
|
|
174
|
-
'\\.jsx$': [
|
|
175
|
-
require.resolve('./jestSwcTransform'),
|
|
176
|
-
{
|
|
177
|
-
jsc: {
|
|
178
|
-
parser: {
|
|
179
|
-
syntax: 'ecmascript',
|
|
180
|
-
jsx: true,
|
|
181
|
-
},
|
|
182
|
-
transform: {
|
|
183
|
-
react: {
|
|
184
|
-
runtime: 'automatic',
|
|
185
|
-
},
|
|
186
|
-
},
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
],
|
|
190
|
-
'\\.ts$': [
|
|
191
|
-
require.resolve('./jestSwcTransform'),
|
|
192
|
-
{
|
|
193
|
-
jsc: {
|
|
194
|
-
parser: {
|
|
195
|
-
syntax: 'typescript',
|
|
196
|
-
},
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
],
|
|
200
|
-
'\\.tsx$': [
|
|
201
|
-
require.resolve('./jestSwcTransform'),
|
|
202
|
-
{
|
|
203
|
-
jsc: {
|
|
204
|
-
parser: {
|
|
205
|
-
syntax: 'typescript',
|
|
206
|
-
tsx: true,
|
|
207
|
-
},
|
|
208
|
-
transform: {
|
|
209
|
-
react: {
|
|
210
|
-
runtime: 'automatic',
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
},
|
|
215
|
-
],
|
|
216
|
-
'\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$':
|
|
217
|
-
require.resolve('./jestFileTransform.js'),
|
|
218
|
-
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
|
|
219
|
-
},
|
|
220
|
-
|
|
221
257
|
// A bit more opinionated
|
|
222
258
|
testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`],
|
|
223
259
|
|
|
@@ -226,7 +262,7 @@ async function getProjectConfig(targetPath, extraConfig, extraOptions) {
|
|
|
226
262
|
: require.resolve('./jestCachingModuleLoader'),
|
|
227
263
|
|
|
228
264
|
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
|
|
229
|
-
...getRoleConfig(pkgJson.backstage?.role),
|
|
265
|
+
...getRoleConfig(pkgJson.backstage?.role, pkgJson),
|
|
230
266
|
};
|
|
231
267
|
|
|
232
268
|
options.setupFilesAfterEnv = options.setupFilesAfterEnv || [];
|
|
@@ -19,6 +19,11 @@ const { default: JestRuntime } = require('jest-runtime');
|
|
|
19
19
|
const scriptTransformCache = new Map();
|
|
20
20
|
|
|
21
21
|
module.exports = class CachingJestRuntime extends JestRuntime {
|
|
22
|
+
constructor(config, ...restAgs) {
|
|
23
|
+
super(config, ...restAgs);
|
|
24
|
+
this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts');
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
// This may or may not be a good idea. Theoretically I don't know why this would impact
|
|
23
28
|
// test correctness and flakiness, but it seems like it may introduce flakiness and strange failures.
|
|
24
29
|
// It does seem to speed up test execution by a fair amount though.
|
|
@@ -33,4 +38,13 @@ module.exports = class CachingJestRuntime extends JestRuntime {
|
|
|
33
38
|
}
|
|
34
39
|
return script;
|
|
35
40
|
}
|
|
41
|
+
|
|
42
|
+
// Unfortunately we need to use this unstable API to make sure that .js files
|
|
43
|
+
// are only loaded as modules where ESM is supported, i.e. Node.js packages.
|
|
44
|
+
unstable_shouldLoadAsEsm(path, ...restArgs) {
|
|
45
|
+
if (!this.allowLoadAsEsm) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
return super.unstable_shouldLoadAsEsm(path, ...restArgs);
|
|
49
|
+
}
|
|
36
50
|
};
|
|
@@ -23,10 +23,16 @@ function createTransformer(config) {
|
|
|
23
23
|
...config,
|
|
24
24
|
});
|
|
25
25
|
const process = (source, filePath, jestOptions) => {
|
|
26
|
+
// Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM
|
|
26
27
|
if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) {
|
|
27
28
|
return { code: source };
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
// Skip transformation of .mjs files, they should only be used if ESM support is available
|
|
32
|
+
if (filePath.endsWith('.mjs')) {
|
|
33
|
+
return { code: source };
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
return swcTransformer.process(source, filePath, jestOptions);
|
|
31
37
|
};
|
|
32
38
|
|
package/config/nodeTransform.cjs
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
const { pathToFileURL } = require('url');
|
|
17
18
|
const { transformSync } = require('@swc/core');
|
|
18
19
|
const { addHook } = require('pirates');
|
|
19
20
|
const { Module } = require('module');
|
|
@@ -55,7 +56,10 @@ addHook(
|
|
|
55
56
|
const transformed = transformSync(code, {
|
|
56
57
|
filename,
|
|
57
58
|
sourceMaps: 'inline',
|
|
58
|
-
module: {
|
|
59
|
+
module: {
|
|
60
|
+
type: 'commonjs',
|
|
61
|
+
ignoreDynamic: true,
|
|
62
|
+
},
|
|
59
63
|
jsc: {
|
|
60
64
|
target: 'es2022',
|
|
61
65
|
parser: {
|
|
@@ -76,3 +80,8 @@ addHook(
|
|
|
76
80
|
},
|
|
77
81
|
{ extensions: ['.js', '.cjs'], ignoreNodeModules: true },
|
|
78
82
|
);
|
|
83
|
+
|
|
84
|
+
// Register module hooks, used by "type": "module" in package.json, .mjs and
|
|
85
|
+
// .mts files, as well as dynamic import(...)s, although dynamic imports will be
|
|
86
|
+
// handled be the CommonJS hooks in this file if what it points to is CommonJS.
|
|
87
|
+
Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename));
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2024 The Backstage Authors
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { dirname, extname, resolve as resolvePath } from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { transformFile } from '@swc/core';
|
|
20
|
+
import { isBuiltin } from 'node:module';
|
|
21
|
+
import { readFile } from 'fs/promises';
|
|
22
|
+
import { existsSync } from 'fs';
|
|
23
|
+
|
|
24
|
+
// @ts-check
|
|
25
|
+
|
|
26
|
+
// No explicit file extension, no type in package.json
|
|
27
|
+
const DEFAULT_MODULE_FORMAT = 'commonjs';
|
|
28
|
+
|
|
29
|
+
// Source file extensions to look for when using bundle resolution strategy
|
|
30
|
+
const SRC_EXTS = ['.ts', '.js'];
|
|
31
|
+
const TS_EXTS = ['.ts', '.mts', '.cts'];
|
|
32
|
+
const moduleTypeTable = {
|
|
33
|
+
'.mjs': 'module',
|
|
34
|
+
'.mts': 'module',
|
|
35
|
+
'.cjs': 'commonjs',
|
|
36
|
+
'.cts': 'commonjs',
|
|
37
|
+
'.ts': undefined,
|
|
38
|
+
'.js': undefined,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/** @type {import('module').ResolveHook} */
|
|
42
|
+
export async function resolve(specifier, context, nextResolve) {
|
|
43
|
+
// Built-in modules are handled by the default resolver
|
|
44
|
+
if (isBuiltin(specifier)) {
|
|
45
|
+
return nextResolve(specifier, context);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const ext = extname(specifier);
|
|
49
|
+
|
|
50
|
+
// Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below.
|
|
51
|
+
if (ext === '.json' && !context.importAttributes?.type) {
|
|
52
|
+
const jsonResult = await nextResolve(specifier, context);
|
|
53
|
+
return {
|
|
54
|
+
...jsonResult,
|
|
55
|
+
format: 'commonjs',
|
|
56
|
+
importAttributes: { type: 'json' },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Anything else with an explicit extension is handled by the default
|
|
61
|
+
// resolver, except that we help determine the module type where needed.
|
|
62
|
+
if (ext !== '') {
|
|
63
|
+
return withDetectedModuleType(await nextResolve(specifier, context));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Other external modules are handled by the default resolver, but again we
|
|
67
|
+
// help determine the module type where needed.
|
|
68
|
+
if (!specifier.startsWith('.')) {
|
|
69
|
+
return withDetectedModuleType(await nextResolve(specifier, context));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The rest of this function handles the case of resolving imports that do not
|
|
73
|
+
// specify any extension and might point to a directory with an `index.*`
|
|
74
|
+
// file. We resolve those using the same logic as most JS bundlers would, with
|
|
75
|
+
// the addition of checking if there's an explicit module format listed in the
|
|
76
|
+
// closest `package.json` file.
|
|
77
|
+
//
|
|
78
|
+
// We use a bundle resolution strategy in order to keep code consistent across
|
|
79
|
+
// Backstage codebases that contains code both for Web and Node.js, and to
|
|
80
|
+
// support packages with common code that can be used in both environments.
|
|
81
|
+
try {
|
|
82
|
+
// This is expected to throw, but in the event that this module specifier is
|
|
83
|
+
// supported we prefer to use the default resolver.
|
|
84
|
+
return await nextResolve(specifier, context);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
|
|
87
|
+
const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`;
|
|
88
|
+
const resolved = await resolveWithoutExt(spec, context, nextResolve);
|
|
89
|
+
if (resolved) {
|
|
90
|
+
return withDetectedModuleType(resolved);
|
|
91
|
+
}
|
|
92
|
+
} else if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
93
|
+
const resolved = await resolveWithoutExt(specifier, context, nextResolve);
|
|
94
|
+
if (resolved) {
|
|
95
|
+
return withDetectedModuleType(resolved);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Unexpected error or no resolution found
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Populates the `format` field in the resolved object based on the closest `package.json` file.
|
|
106
|
+
*
|
|
107
|
+
* @param {import('module').ResolveFnOutput} resolved
|
|
108
|
+
* @returns {Promise<import('module').ResolveFnOutput>}
|
|
109
|
+
*/
|
|
110
|
+
async function withDetectedModuleType(resolved) {
|
|
111
|
+
// Already has an explicit format
|
|
112
|
+
if (resolved.format) {
|
|
113
|
+
return resolved;
|
|
114
|
+
}
|
|
115
|
+
// Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default.
|
|
116
|
+
if (resolved.format === null) {
|
|
117
|
+
return { ...resolved, format: DEFAULT_MODULE_FORMAT };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const ext = extname(resolved.url);
|
|
121
|
+
|
|
122
|
+
const explicitFormat = moduleTypeTable[ext];
|
|
123
|
+
if (explicitFormat) {
|
|
124
|
+
return {
|
|
125
|
+
...resolved,
|
|
126
|
+
format: explicitFormat,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// TODO(Rugvip): Afaik this should never happen and we can remove this check, but want it here for a little while to verify.
|
|
131
|
+
if (ext === '.js') {
|
|
132
|
+
throw new Error('Unexpected .js file without explicit format');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring
|
|
136
|
+
const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url));
|
|
137
|
+
if (!packageJsonPath) {
|
|
138
|
+
return resolved;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
142
|
+
return {
|
|
143
|
+
...resolved,
|
|
144
|
+
format: packageJson.type ?? DEFAULT_MODULE_FORMAT,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Find the closest package.json file from the given path.
|
|
150
|
+
*
|
|
151
|
+
* TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable.
|
|
152
|
+
* @param {string} startPath
|
|
153
|
+
* @returns {Promise<string | undefined>}
|
|
154
|
+
*/
|
|
155
|
+
async function findPackageJSON(startPath) {
|
|
156
|
+
let path = startPath;
|
|
157
|
+
|
|
158
|
+
// Some confidence check to avoid infinite loop
|
|
159
|
+
for (let i = 0; i < 1000; i++) {
|
|
160
|
+
const packagePath = resolvePath(path, 'package.json');
|
|
161
|
+
if (existsSync(packagePath)) {
|
|
162
|
+
return packagePath;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const newPath = dirname(path);
|
|
166
|
+
if (newPath === path) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
path = newPath;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new Error(
|
|
173
|
+
`Iteration limit reached when searching for package.json at ${startPath}`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** @type {import('module').ResolveHook} */
|
|
178
|
+
async function resolveWithoutExt(specifier, context, nextResolve) {
|
|
179
|
+
for (const tryExt of SRC_EXTS) {
|
|
180
|
+
try {
|
|
181
|
+
const resolved = await nextResolve(specifier + tryExt, {
|
|
182
|
+
...context,
|
|
183
|
+
format: 'commonjs',
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
...resolved,
|
|
187
|
+
format: moduleTypeTable[tryExt] ?? resolved.format,
|
|
188
|
+
};
|
|
189
|
+
} catch {
|
|
190
|
+
/* ignore */
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** @type {import('module').LoadHook} */
|
|
197
|
+
export async function load(url, context, nextLoad) {
|
|
198
|
+
// Non-file URLs are handled by the default loader
|
|
199
|
+
if (!url.startsWith('file://')) {
|
|
200
|
+
return nextLoad(url, context);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// JSON files loaded as CommonJS are handled by this custom loader, because
|
|
204
|
+
// the default one doesn't work. For JSON loading to work we'd need the
|
|
205
|
+
// synchronous hooks that aren't supported yet, or avoid using the CommonJS
|
|
206
|
+
// compatibility.
|
|
207
|
+
if (
|
|
208
|
+
context.format === 'commonjs' &&
|
|
209
|
+
context.importAttributes?.type === 'json'
|
|
210
|
+
) {
|
|
211
|
+
try {
|
|
212
|
+
// TODO(Rugvip): Make sure this is valid JSON
|
|
213
|
+
const content = await readFile(fileURLToPath(url), 'utf8');
|
|
214
|
+
return {
|
|
215
|
+
source: `module.exports = (${content})`,
|
|
216
|
+
format: 'commonjs',
|
|
217
|
+
shortCircuit: true,
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
// Let the default loader generate the error
|
|
221
|
+
return nextLoad(url, context);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const ext = extname(url);
|
|
226
|
+
|
|
227
|
+
// Non-TS files are handled by the default loader
|
|
228
|
+
if (!TS_EXTS.includes(ext)) {
|
|
229
|
+
return nextLoad(url, context);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const format = context.format ?? DEFAULT_MODULE_FORMAT;
|
|
233
|
+
|
|
234
|
+
// We have two choices at this point, we can either transform CommonJS files
|
|
235
|
+
// and return the transformed source code, or let the default loader handle
|
|
236
|
+
// them. If we transform them ourselves we will enter CommonJS compatibility
|
|
237
|
+
// mode in the new module system in Node.js, this effectively means all
|
|
238
|
+
// CommonJS loaded via `require` calls from this point will all be treated as
|
|
239
|
+
// if it was loaded via `import` calls from modules.
|
|
240
|
+
//
|
|
241
|
+
// The CommonJS compatibility layer will try to identify named exports and
|
|
242
|
+
// make them available directly, which is convenient as it avoids things like
|
|
243
|
+
// `import(...).then(m => m.default.foo)`, allowing you to instead write
|
|
244
|
+
// `import(...).then(m => m.foo)`. The compatibility layer doesn't always work
|
|
245
|
+
// all that well though, and can lead to module loading issues in many cases,
|
|
246
|
+
// especially for older code.
|
|
247
|
+
|
|
248
|
+
// This `if` block opts-out of using CommonJS compatibility mode by default,
|
|
249
|
+
// and instead leaves it to our existing loader to transform CommonJS. We do
|
|
250
|
+
// however use compatibility mode for the more explicit .cts file extension,
|
|
251
|
+
// allows for a way to opt-in to the new behavior.
|
|
252
|
+
//
|
|
253
|
+
// TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead
|
|
254
|
+
if (format === 'commonjs' && ext !== '.cts') {
|
|
255
|
+
return nextLoad(url, { ...context, format });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const transformed = await transformFile(fileURLToPath(url), {
|
|
259
|
+
sourceMaps: 'inline',
|
|
260
|
+
module: {
|
|
261
|
+
type: format === 'module' ? 'es6' : 'commonjs',
|
|
262
|
+
ignoreDynamic: true,
|
|
263
|
+
|
|
264
|
+
// This helps the Node.js CommonJS compat layer identify named exports.
|
|
265
|
+
exportInteropAnnotation: true,
|
|
266
|
+
},
|
|
267
|
+
jsc: {
|
|
268
|
+
target: 'es2022',
|
|
269
|
+
parser: {
|
|
270
|
+
syntax: 'typescript',
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
...context,
|
|
277
|
+
shortCircuit: true,
|
|
278
|
+
source: transformed.code,
|
|
279
|
+
format,
|
|
280
|
+
responseURL: url,
|
|
281
|
+
};
|
|
282
|
+
}
|