Move config files to CLI modules with lazy proxies
Move jest config files to cli-module-test-jest/config and node transform + webpack-public-path to cli-module-build/config. Replace originals in @backstage/cli/config with lazy proxies that forward to the appropriate module or throw if it is not installed. Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com> Made-with: Cursor
This commit is contained in:
@@ -14,36 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
function getJestMajorVersion() {
|
||||
const jestVersion = require('jest/package.json').version;
|
||||
const majorVersion = parseInt(jestVersion.split('.')[0], 10);
|
||||
return majorVersion;
|
||||
}
|
||||
|
||||
function getJestEnvironment() {
|
||||
const majorVersion = getJestMajorVersion();
|
||||
|
||||
if (majorVersion >= 30) {
|
||||
try {
|
||||
require.resolve('@jest/environment-jsdom-abstract');
|
||||
require.resolve('jsdom');
|
||||
} catch {
|
||||
throw new Error(
|
||||
'Jest 30+ requires @jest/environment-jsdom-abstract and jsdom. ' +
|
||||
'Please install them as dev dependencies.',
|
||||
);
|
||||
}
|
||||
return require.resolve('./jest-environment-jsdom');
|
||||
}
|
||||
try {
|
||||
require.resolve('jest-environment-jsdom');
|
||||
} catch {
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/getJestEnvironment');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'Jest 29 requires jest-environment-jsdom. ' +
|
||||
'Please install it as a dev dependency.',
|
||||
'@backstage/cli-module-test-jest is required to use the jest environment configuration. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
return require.resolve('jest-environment-jsdom');
|
||||
throw e;
|
||||
}
|
||||
|
||||
module.exports = { getJestMajorVersion, getJestEnvironment };
|
||||
|
||||
@@ -14,48 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const JSDOMEnvironment = require('@jest/environment-jsdom-abstract').default;
|
||||
const jsdom = require('jsdom');
|
||||
|
||||
/**
|
||||
* A custom JSDOM environment that extends the abstract base and applies
|
||||
* fixes for Web API globals that are missing or incorrectly implemented
|
||||
* in JSDOM.
|
||||
*
|
||||
* Based on https://github.com/mswjs/jest-fixed-jsdom
|
||||
*/
|
||||
class FixedJSDOMEnvironment extends JSDOMEnvironment {
|
||||
constructor(config, context) {
|
||||
super(config, context, jsdom);
|
||||
|
||||
// Fix Web API globals that JSDOM doesn't properly expose
|
||||
this.global.TextDecoder = TextDecoder;
|
||||
this.global.TextEncoder = TextEncoder;
|
||||
this.global.TextDecoderStream = TextDecoderStream;
|
||||
this.global.TextEncoderStream = TextEncoderStream;
|
||||
this.global.ReadableStream = ReadableStream;
|
||||
|
||||
this.global.Blob = Blob;
|
||||
this.global.Headers = Headers;
|
||||
this.global.FormData = FormData;
|
||||
this.global.Request = Request;
|
||||
this.global.Response = Response;
|
||||
this.global.fetch = fetch;
|
||||
this.global.AbortController = AbortController;
|
||||
this.global.AbortSignal = AbortSignal;
|
||||
this.global.structuredClone = structuredClone;
|
||||
this.global.URL = URL;
|
||||
this.global.URLSearchParams = URLSearchParams;
|
||||
|
||||
this.global.BroadcastChannel = BroadcastChannel;
|
||||
this.global.TransformStream = TransformStream;
|
||||
this.global.WritableStream = WritableStream;
|
||||
|
||||
// Needed to ensure `e instanceof Error` works as expected with errors thrown from
|
||||
// any of the native APIs above. Without this, the JSDOM `Error` is what the test
|
||||
// code will use for comparison with `e`, which fails the instanceof check.
|
||||
this.global.Error = Error;
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jest-environment-jsdom');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest JSDOM environment. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
module.exports = FixedJSDOMEnvironment;
|
||||
|
||||
+7
-402
@@ -14,409 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const fs = require('fs-extra');
|
||||
const path = require('node:path');
|
||||
const crypto = require('node:crypto');
|
||||
const glob = require('node:util').promisify(require('glob'));
|
||||
const { version } = require('../package.json');
|
||||
const paths = require('@backstage/cli-common').findPaths(process.cwd());
|
||||
const {
|
||||
getJestEnvironment,
|
||||
getJestMajorVersion,
|
||||
} = require('./getJestEnvironment');
|
||||
|
||||
const SRC_EXTS = ['ts', 'js', 'tsx', 'jsx', 'mts', 'cts', 'mjs', 'cjs'];
|
||||
|
||||
const FRONTEND_ROLES = [
|
||||
'frontend',
|
||||
'web-library',
|
||||
'common-library',
|
||||
'frontend-plugin',
|
||||
'frontend-plugin-module',
|
||||
];
|
||||
|
||||
const NODE_ROLES = [
|
||||
'backend',
|
||||
'cli',
|
||||
'cli-module',
|
||||
'node-library',
|
||||
'backend-plugin',
|
||||
'backend-plugin-module',
|
||||
];
|
||||
|
||||
const envOptions = {
|
||||
oldTests: Boolean(process.env.BACKSTAGE_OLD_TESTS),
|
||||
};
|
||||
|
||||
try {
|
||||
require.resolve('react-dom/client', {
|
||||
paths: [paths.targetRoot],
|
||||
});
|
||||
process.env.HAS_REACT_DOM_CLIENT = true;
|
||||
} catch {
|
||||
/* ignored */
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of config keys that are valid for project-level config.
|
||||
* Jest will complain if we forward any other root configuration to the projects.
|
||||
*
|
||||
* @type {Array<keyof import('@jest/types').Config.ProjectConfig>}
|
||||
*/
|
||||
const projectConfigKeys = [
|
||||
'automock',
|
||||
'cache',
|
||||
'cacheDirectory',
|
||||
'clearMocks',
|
||||
'collectCoverageFrom',
|
||||
'coverageDirectory',
|
||||
'coveragePathIgnorePatterns',
|
||||
'cwd',
|
||||
'dependencyExtractor',
|
||||
'detectLeaks',
|
||||
'detectOpenHandles',
|
||||
'displayName',
|
||||
'errorOnDeprecated',
|
||||
'extensionsToTreatAsEsm',
|
||||
'fakeTimers',
|
||||
'filter',
|
||||
'forceCoverageMatch',
|
||||
'globalSetup',
|
||||
'globalTeardown',
|
||||
'globals',
|
||||
'haste',
|
||||
'id',
|
||||
'injectGlobals',
|
||||
'moduleDirectories',
|
||||
'moduleFileExtensions',
|
||||
'moduleNameMapper',
|
||||
'modulePathIgnorePatterns',
|
||||
'modulePaths',
|
||||
'openHandlesTimeout',
|
||||
'preset',
|
||||
'prettierPath',
|
||||
'resetMocks',
|
||||
'resetModules',
|
||||
'resolver',
|
||||
'restoreMocks',
|
||||
'rootDir',
|
||||
'roots',
|
||||
'runner',
|
||||
'runtime',
|
||||
'sandboxInjectedGlobals',
|
||||
'setupFiles',
|
||||
'setupFilesAfterEnv',
|
||||
'skipFilter',
|
||||
'skipNodeResolution',
|
||||
'slowTestThreshold',
|
||||
'snapshotResolver',
|
||||
'snapshotSerializers',
|
||||
'snapshotFormat',
|
||||
'testEnvironment',
|
||||
'testEnvironmentOptions',
|
||||
'testMatch',
|
||||
'testLocationInResults',
|
||||
'testPathIgnorePatterns',
|
||||
'testRegex',
|
||||
'testRunner',
|
||||
'transform',
|
||||
'transformIgnorePatterns',
|
||||
'watchPathIgnorePatterns',
|
||||
'unmockedModulePathPatterns',
|
||||
'workerIdleMemoryLimit',
|
||||
];
|
||||
|
||||
const transformIgnorePattern = [
|
||||
'@material-ui',
|
||||
'ajv',
|
||||
'core-js',
|
||||
'jest-.*',
|
||||
'jsdom',
|
||||
'knex',
|
||||
'react',
|
||||
'react-dom',
|
||||
'highlight\\.js',
|
||||
'prismjs',
|
||||
'json-schema',
|
||||
'react-use/lib',
|
||||
'typescript',
|
||||
].join('|');
|
||||
|
||||
// Provides additional config that's based on the role of the target package
|
||||
function getRoleConfig(role, pkgJson) {
|
||||
// Only Node.js package roles support native ESM modules, frontend and common
|
||||
// packages are always transpiled to CommonJS.
|
||||
const moduleOpts = NODE_ROLES.includes(role)
|
||||
? {
|
||||
module: {
|
||||
ignoreDynamic: true,
|
||||
exportInteropAnnotation: true,
|
||||
},
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const transform = {
|
||||
'\\.(mjs|cjs|js)$': [
|
||||
require.resolve('./jestSwcTransform'),
|
||||
{
|
||||
...moduleOpts,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'ecmascript',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'\\.jsx$': [
|
||||
require.resolve('./jestSwcTransform'),
|
||||
{
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'ecmascript',
|
||||
jsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'\\.(mts|cts|ts)$': [
|
||||
require.resolve('./jestSwcTransform'),
|
||||
{
|
||||
...moduleOpts,
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'\\.tsx$': [
|
||||
require.resolve('./jestSwcTransform'),
|
||||
{
|
||||
jsc: {
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
tsx: true,
|
||||
},
|
||||
transform: {
|
||||
react: {
|
||||
runtime: 'automatic',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
'\\.(bmp|gif|jpg|jpeg|png|ico|webp|frag|xml|svg|eot|woff|woff2|ttf)$':
|
||||
require.resolve('./jestFileTransform.js'),
|
||||
'\\.(yaml)$': require.resolve('./jestYamlTransform'),
|
||||
};
|
||||
if (FRONTEND_ROLES.includes(role)) {
|
||||
return {
|
||||
testEnvironment: getJestEnvironment(),
|
||||
// The caching module loader is only used to speed up frontend tests,
|
||||
// as it breaks real dynamic imports of ESM modules.
|
||||
runtime: envOptions.oldTests
|
||||
? undefined
|
||||
: require.resolve('./jestCachingModuleLoader'),
|
||||
transform,
|
||||
};
|
||||
}
|
||||
return {
|
||||
testEnvironment: require.resolve('jest-environment-node'),
|
||||
moduleFileExtensions: [...SRC_EXTS, 'json', 'node'],
|
||||
// Jest doesn't let us dynamically detect type=module per transformed file,
|
||||
// so we have to assume that if the entry point is ESM, all TS files are
|
||||
// ESM.
|
||||
//
|
||||
// This means you can't switch a package to type=module until all of its
|
||||
// monorepo dependencies are also type=module or does not contain any .ts
|
||||
// files.
|
||||
extensionsToTreatAsEsm:
|
||||
pkgJson.type === 'module' ? ['.ts', '.mts'] : ['.mts'],
|
||||
transform,
|
||||
};
|
||||
}
|
||||
|
||||
async function getProjectConfig(targetPath, extraConfig, extraOptions) {
|
||||
const configJsPath = path.resolve(targetPath, 'jest.config.js');
|
||||
const configTsPath = path.resolve(targetPath, 'jest.config.ts');
|
||||
// If the package has it's own jest config, we use that instead.
|
||||
if (await fs.pathExists(configJsPath)) {
|
||||
return require(configJsPath);
|
||||
} else if (await fs.pathExists(configTsPath)) {
|
||||
return require(configTsPath);
|
||||
}
|
||||
|
||||
// Jest config can be defined both in the root package.json and within each package. The root config
|
||||
// gets forwarded to us through the `extraConfig` parameter, while the package config is read here.
|
||||
// If they happen to be the same the keys will simply override each other.
|
||||
// The merging of the configs is shallow, meaning e.g. all transforms are replaced if new ones are defined.
|
||||
const pkgJson = await fs.readJson(path.resolve(targetPath, 'package.json'));
|
||||
|
||||
const options = {
|
||||
...extraConfig,
|
||||
rootDir: path.resolve(targetPath, 'src'),
|
||||
moduleNameMapper: {
|
||||
'\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'),
|
||||
},
|
||||
|
||||
// A bit more opinionated
|
||||
testMatch: [`**/*.test.{${SRC_EXTS.join(',')}}`],
|
||||
|
||||
transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`],
|
||||
...getRoleConfig(pkgJson.backstage?.role, pkgJson),
|
||||
};
|
||||
|
||||
options.setupFilesAfterEnv = options.setupFilesAfterEnv || [];
|
||||
|
||||
if (
|
||||
extraOptions.rejectFrontendNetworkRequests &&
|
||||
FRONTEND_ROLES.includes(pkgJson.backstage?.role)
|
||||
) {
|
||||
// By adding this first we ensure that it's possible to for example override
|
||||
// fetch with a mock in a custom setup file
|
||||
options.setupFilesAfterEnv.unshift(
|
||||
require.resolve('./jestRejectNetworkRequests.js'),
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jest');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use this jest configuration. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
options.testEnvironment === getJestEnvironment() &&
|
||||
getJestMajorVersion() < 30 // Only needed when not running the custom env for Jest 30+
|
||||
) {
|
||||
// FIXME https://github.com/jsdom/jsdom/issues/1724
|
||||
options.setupFilesAfterEnv.unshift(require.resolve('cross-fetch/polyfill'));
|
||||
}
|
||||
|
||||
// Use src/setupTests.* as the default location for configuring test env
|
||||
for (const ext of SRC_EXTS) {
|
||||
if (fs.existsSync(path.resolve(targetPath, `src/setupTests.${ext}`))) {
|
||||
options.setupFilesAfterEnv.push(`<rootDir>/setupTests.${ext}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const config = Object.assign(options, pkgJson.jest);
|
||||
|
||||
// The config id is a cache key that lets us share the jest cache across projects.
|
||||
// If no explicit id was configured, generated one based on the configuration.
|
||||
if (!config.id) {
|
||||
const configHash = crypto
|
||||
.createHash('sha256')
|
||||
.update(version)
|
||||
.update(Buffer.alloc(1))
|
||||
.update(JSON.stringify(config.transform).replaceAll(paths.targetRoot, ''))
|
||||
.digest('hex');
|
||||
config.id = `backstage_cli_${configHash}`;
|
||||
}
|
||||
|
||||
return config;
|
||||
throw e;
|
||||
}
|
||||
|
||||
// This loads the root jest config, which in turn will either refer to a single
|
||||
// configuration for the current package, or a collection of configurations for
|
||||
// the target workspace packages
|
||||
async function getRootConfig() {
|
||||
const rootPkgJson = await fs.readJson(
|
||||
paths.resolveTargetRoot('package.json'),
|
||||
);
|
||||
|
||||
const baseCoverageConfig = {
|
||||
coverageDirectory: paths.resolveTarget('coverage'),
|
||||
coverageProvider: envOptions.oldTests ? 'v8' : 'babel',
|
||||
collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'],
|
||||
};
|
||||
|
||||
const { rejectFrontendNetworkRequests, ...rootOptions } =
|
||||
rootPkgJson.jest ?? {};
|
||||
const extraRootOptions = {
|
||||
rejectFrontendNetworkRequests,
|
||||
};
|
||||
|
||||
const ws = rootPkgJson.workspaces;
|
||||
const workspacePatterns = Array.isArray(ws) ? ws : ws?.packages;
|
||||
|
||||
// Check if we're running within a specific monorepo package. In that case just get the single project config.
|
||||
if (!workspacePatterns || paths.targetRoot !== paths.targetDir) {
|
||||
return getProjectConfig(
|
||||
paths.targetDir,
|
||||
{
|
||||
...baseCoverageConfig,
|
||||
...rootOptions,
|
||||
},
|
||||
extraRootOptions,
|
||||
);
|
||||
}
|
||||
|
||||
const globalRootConfig = { ...baseCoverageConfig };
|
||||
const globalProjectConfig = {};
|
||||
|
||||
for (const [key, value] of Object.entries(rootOptions)) {
|
||||
if (projectConfigKeys.includes(key)) {
|
||||
globalProjectConfig[key] = value;
|
||||
} else {
|
||||
globalRootConfig[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
// If the target package is a workspace root, we find all packages in the
|
||||
// workspace and load those in as separate jest projects instead.
|
||||
const projectPaths = await Promise.all(
|
||||
workspacePatterns.map(pattern =>
|
||||
glob(path.join(paths.targetRoot, pattern)),
|
||||
),
|
||||
).then(_ => _.flat());
|
||||
|
||||
let projects = await Promise.all(
|
||||
projectPaths.flat().map(async projectPath => {
|
||||
const packagePath = path.resolve(projectPath, 'package.json');
|
||||
if (!(await fs.pathExists(packagePath))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// We check for the presence of "backstage-cli test" in the package test
|
||||
// script to determine whether a given package should be tested
|
||||
const packageData = await fs.readJson(packagePath);
|
||||
const testScript = packageData.scripts && packageData.scripts.test;
|
||||
const isSupportedTestScript =
|
||||
testScript?.includes('backstage-cli test') ||
|
||||
testScript?.includes('backstage-cli package test');
|
||||
if (testScript && isSupportedTestScript) {
|
||||
return await getProjectConfig(
|
||||
projectPath,
|
||||
{
|
||||
...globalProjectConfig,
|
||||
displayName: packageData.name,
|
||||
},
|
||||
extraRootOptions,
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}),
|
||||
).then(cs => cs.filter(Boolean));
|
||||
|
||||
const cache = global.__backstageCli_jestSuccessCache;
|
||||
if (cache) {
|
||||
projects = await cache.filterConfigs(projects, globalRootConfig);
|
||||
}
|
||||
const watchProjectFilter = global.__backstageCli_watchProjectFilter;
|
||||
if (watchProjectFilter) {
|
||||
projects = await watchProjectFilter.filter(projects);
|
||||
}
|
||||
|
||||
return {
|
||||
rootDir: paths.targetRoot,
|
||||
projects,
|
||||
testResultsProcessor: cache
|
||||
? require.resolve('./jestCacheResultProcessor.cjs')
|
||||
: undefined,
|
||||
...globalRootConfig,
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = getRootConfig();
|
||||
|
||||
@@ -14,10 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
module.exports = async results => {
|
||||
const cache = global.__backstageCli_jestSuccessCache;
|
||||
if (cache) {
|
||||
await cache.reportResults(results);
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestCacheResultProcessor.cjs');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest cache result processor. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
return results;
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* Copyright 2023 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,22 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// 'jest-runtime' is included with jest and should be kept in sync with the installed jest version
|
||||
// eslint-disable-next-line @backstage/no-undeclared-imports
|
||||
const { default: JestRuntime } = require('jest-runtime');
|
||||
|
||||
module.exports = class CachingJestRuntime extends JestRuntime {
|
||||
constructor(config, ...restArgs) {
|
||||
super(config, ...restArgs);
|
||||
this.allowLoadAsEsm = config.extensionsToTreatAsEsm.includes('.mts');
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestCachingModuleLoader');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest caching module loader. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
|
||||
// Unfortunately we need to use this unstable API to make sure that .js files
|
||||
// are only loaded as modules where ESM is supported, i.e. Node.js packages.
|
||||
unstable_shouldLoadAsEsm(path, ...restArgs) {
|
||||
if (!this.allowLoadAsEsm) {
|
||||
return false;
|
||||
}
|
||||
return super.unstable_shouldLoadAsEsm(path, ...restArgs);
|
||||
}
|
||||
};
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -14,31 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const path = require('node:path');
|
||||
|
||||
module.exports = {
|
||||
process(src, filename) {
|
||||
const assetFilename = JSON.stringify(path.basename(filename));
|
||||
|
||||
if (filename.match(/\.icon\.svg$/)) {
|
||||
return {
|
||||
code: `const React = require('react');
|
||||
const SvgIcon = require('@material-ui/core/SvgIcon').default;
|
||||
module.exports = {
|
||||
__esModule: true,
|
||||
default: props => React.createElement(SvgIcon, props, {
|
||||
$$typeof: Symbol.for('react.element'),
|
||||
type: 'svg',
|
||||
ref: ref,
|
||||
key: null,
|
||||
props: Object.assign({}, props, {
|
||||
children: ${assetFilename}
|
||||
})
|
||||
})
|
||||
};`,
|
||||
};
|
||||
}
|
||||
|
||||
return { code: `module.exports = ${assetFilename};` };
|
||||
},
|
||||
};
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestFileTransform');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest file transform. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -14,57 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const http = require('node:http');
|
||||
const https = require('node:https');
|
||||
|
||||
const errorMessage = 'Network requests are not allowed in tests';
|
||||
|
||||
const origHttpAgent = http.globalAgent;
|
||||
const origHttpsAgent = https.globalAgent;
|
||||
const origFetch = global.fetch;
|
||||
const origXMLHttpRequest = global.XMLHttpRequest;
|
||||
|
||||
http.globalAgent = new http.Agent({
|
||||
lookup() {
|
||||
throw new Error(errorMessage);
|
||||
},
|
||||
});
|
||||
|
||||
https.globalAgent = new https.Agent({
|
||||
lookup() {
|
||||
throw new Error(errorMessage);
|
||||
},
|
||||
});
|
||||
|
||||
const BLOCKING_FETCH_SYMBOL = Symbol.for(
|
||||
'backstage.jestRejectNetworkRequests.blockingFetch',
|
||||
);
|
||||
|
||||
if (global.fetch) {
|
||||
const blockingFetch = async (input, init) => {
|
||||
// If global.fetch still has our marker, block the request
|
||||
if (global.fetch[BLOCKING_FETCH_SYMBOL]) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
// MSW (or something else) wrapped us - pass through
|
||||
return origFetch(input, init);
|
||||
};
|
||||
blockingFetch[BLOCKING_FETCH_SYMBOL] = true;
|
||||
global.fetch = blockingFetch;
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestRejectNetworkRequests');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest network request rejection. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (global.XMLHttpRequest) {
|
||||
global.XMLHttpRequest = class {
|
||||
constructor() {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Reset overrides after each suite to make sure we don't pollute the test environment
|
||||
afterAll(() => {
|
||||
http.globalAgent = origHttpAgent;
|
||||
https.globalAgent = origHttpsAgent;
|
||||
global.fetch = origFetch;
|
||||
global.XMLHttpRequest = origXMLHttpRequest;
|
||||
});
|
||||
|
||||
@@ -14,74 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { createHash } = require('node:crypto');
|
||||
const { transform } = require('sucrase');
|
||||
const sucrasePkg = require('sucrase/package.json');
|
||||
|
||||
const ESM_REGEX = /\b(?:import|export)\b/;
|
||||
|
||||
function createTransformer(config) {
|
||||
const process = (source, filePath) => {
|
||||
let transforms;
|
||||
|
||||
if (filePath.endsWith('.esm.js')) {
|
||||
transforms = ['imports'];
|
||||
} else if (filePath.endsWith('.js')) {
|
||||
// This is a very rough filter to avoid transforming things that we quickly
|
||||
// can be sure are definitely not ESM modules.
|
||||
if (ESM_REGEX.test(source)) {
|
||||
transforms = ['imports', 'jsx']; // JSX within .js is currently allowed
|
||||
}
|
||||
} else if (filePath.endsWith('.jsx')) {
|
||||
transforms = ['jsx', 'imports'];
|
||||
} else if (filePath.endsWith('.ts')) {
|
||||
transforms = ['typescript', 'imports'];
|
||||
} else if (filePath.endsWith('.tsx')) {
|
||||
transforms = ['typescript', 'jsx', 'imports'];
|
||||
}
|
||||
|
||||
// Only apply the jest transform to the test files themselves
|
||||
if (transforms && filePath.includes('.test.')) {
|
||||
transforms.push('jest');
|
||||
}
|
||||
|
||||
if (transforms) {
|
||||
const { code, sourceMap: map } = transform(source, {
|
||||
transforms,
|
||||
filePath,
|
||||
disableESTransforms: true,
|
||||
sourceMapOptions: {
|
||||
compiledFilename: filePath,
|
||||
},
|
||||
});
|
||||
if (config.enableSourceMaps) {
|
||||
const b64 = Buffer.from(JSON.stringify(map), 'utf8').toString('base64');
|
||||
const suffix = `//# sourceMappingURL=data:application/json;charset=utf-8;base64,${b64}`;
|
||||
// Include both inline and object source maps, as inline source maps are
|
||||
// needed for support of some editor integrations.
|
||||
return { code: `${code}\n${suffix}`, map };
|
||||
}
|
||||
// We only return the `map` result if source maps are enabled, as they
|
||||
// have a negative impact on the coverage accuracy.
|
||||
return { code };
|
||||
}
|
||||
|
||||
return { code: source };
|
||||
};
|
||||
|
||||
const getCacheKey = sourceText => {
|
||||
return createHash('sha256')
|
||||
.update(sourceText)
|
||||
.update(Buffer.alloc(1))
|
||||
.update(sucrasePkg.version)
|
||||
.update(Buffer.alloc(1))
|
||||
.update(JSON.stringify(config))
|
||||
.update(Buffer.alloc(1))
|
||||
.update('1') // increment whenever the transform logic in this file changes
|
||||
.digest('hex');
|
||||
};
|
||||
|
||||
return { process, getCacheKey };
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestSucraseTransform');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest Sucrase transform. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
module.exports = { createTransformer };
|
||||
|
||||
@@ -13,32 +13,15 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
const { createTransformer: createSwcTransformer } = require('@swc/jest');
|
||||
|
||||
const ESM_REGEX = /\b(?:import|export)\b/;
|
||||
|
||||
function createTransformer(config) {
|
||||
const swcTransformer = createSwcTransformer({
|
||||
inputSourceMap: false,
|
||||
...config,
|
||||
});
|
||||
const process = (source, filePath, jestOptions) => {
|
||||
// Skip transformation of .js files without ESM syntax, we never transform from CJS to ESM
|
||||
if (filePath.endsWith('.js') && !ESM_REGEX.test(source)) {
|
||||
return { code: source };
|
||||
}
|
||||
|
||||
// Skip transformation of .mjs files, they should only be used if ESM support is available
|
||||
if (jestOptions.supportsStaticESM && filePath.endsWith('.mjs')) {
|
||||
return { code: source };
|
||||
}
|
||||
|
||||
return swcTransformer.process(source, filePath, jestOptions);
|
||||
};
|
||||
|
||||
const getCacheKey = swcTransformer.getCacheKey;
|
||||
|
||||
return { process, getCacheKey };
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestSwcTransform');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest SWC transform. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
module.exports = { createTransformer };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -14,27 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const yaml = require('yaml');
|
||||
const crypto = require('node:crypto');
|
||||
|
||||
function createTransformer(config) {
|
||||
const process = source => {
|
||||
const json = JSON.stringify(yaml.parse(source), null, 2);
|
||||
return { code: `module.exports = ${json}`, map: null };
|
||||
};
|
||||
|
||||
const getCacheKey = sourceText => {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(sourceText)
|
||||
.update(Buffer.alloc(1))
|
||||
.update(JSON.stringify(config))
|
||||
.update(Buffer.alloc(1))
|
||||
.update('1') // increment whenever the transform logic in this file changes
|
||||
.digest('hex');
|
||||
};
|
||||
|
||||
return { process, getCacheKey };
|
||||
try {
|
||||
module.exports = require('@backstage/cli-module-test-jest/config/jestYamlTransform');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-test-jest is required to use the jest YAML transform. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
module.exports = { createTransformer };
|
||||
|
||||
@@ -14,74 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { pathToFileURL } = require('node:url');
|
||||
const { transformSync } = require('@swc/core');
|
||||
const { addHook } = require('pirates');
|
||||
const { Module } = require('node:module');
|
||||
|
||||
// This hooks into module resolution and overrides imports of packages that
|
||||
// exist in the linked workspace to instead be resolved from the linked workspace.
|
||||
if (process.env.BACKSTAGE_CLI_LINKED_WORKSPACE) {
|
||||
const { join: joinPath } = require('node:path');
|
||||
const { getPackagesSync } = require('@manypkg/get-packages');
|
||||
const { packages: linkedPackages, root: linkedRoot } = getPackagesSync(
|
||||
process.env.BACKSTAGE_CLI_LINKED_WORKSPACE,
|
||||
);
|
||||
|
||||
// Matches all packages in the linked workspaces, as well as sub-path exports from them
|
||||
const replacementRegex = new RegExp(
|
||||
`^(?:${linkedPackages
|
||||
.map(pkg => pkg.packageJson.name)
|
||||
.join('|')})(?:/.*)?$`,
|
||||
);
|
||||
|
||||
const origLoad = Module._load;
|
||||
Module._load = function requireHook(request, parent) {
|
||||
if (!replacementRegex.test(request)) {
|
||||
return origLoad.call(this, request, parent);
|
||||
}
|
||||
|
||||
// The package import that we're overriding will always existing in the root
|
||||
// node_modules of the linked workspace, so it's enough to override the
|
||||
// parent paths with that single entry
|
||||
return origLoad.call(this, request, {
|
||||
...parent,
|
||||
paths: [joinPath(linkedRoot.dir, 'node_modules')],
|
||||
});
|
||||
};
|
||||
try {
|
||||
require('@backstage/cli-module-build/config/nodeTransform.cjs');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-build is required to use the node transform. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
addHook(
|
||||
(code, filename) => {
|
||||
const transformed = transformSync(code, {
|
||||
filename,
|
||||
sourceMaps: 'inline',
|
||||
module: {
|
||||
type: 'commonjs',
|
||||
ignoreDynamic: true,
|
||||
},
|
||||
jsc: {
|
||||
target: 'es2023',
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
},
|
||||
},
|
||||
});
|
||||
process.send?.({ type: 'watch', path: filename });
|
||||
return transformed.code;
|
||||
},
|
||||
{ extensions: ['.ts', '.cts'], ignoreNodeModules: true },
|
||||
);
|
||||
|
||||
addHook(
|
||||
(code, filename) => {
|
||||
process.send?.({ type: 'watch', path: filename });
|
||||
return code;
|
||||
},
|
||||
{ extensions: ['.js', '.cjs'], ignoreNodeModules: true },
|
||||
);
|
||||
|
||||
// Register module hooks, used by "type": "module" in package.json, .mjs and
|
||||
// .mts files, as well as dynamic import(...)s, although dynamic imports will be
|
||||
// handled be the CommonJS hooks in this file if what it points to is CommonJS.
|
||||
Module.register('./nodeTransformHooks.mjs', pathToFileURL(__filename));
|
||||
|
||||
@@ -14,281 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { dirname, extname, resolve as resolvePath } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { transformFile } from '@swc/core';
|
||||
import { isBuiltin } from 'node:module';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { existsSync } from 'node:fs';
|
||||
|
||||
// @ts-check
|
||||
|
||||
// No explicit file extension, no type in package.json
|
||||
const DEFAULT_MODULE_FORMAT = 'commonjs';
|
||||
|
||||
// Source file extensions to look for when using bundle resolution strategy
|
||||
const SRC_EXTS = ['.ts', '.js'];
|
||||
const TS_EXTS = ['.ts', '.mts', '.cts'];
|
||||
const moduleTypeTable = {
|
||||
'.mjs': 'module',
|
||||
'.mts': 'module',
|
||||
'.cjs': 'commonjs',
|
||||
'.cts': 'commonjs',
|
||||
'.ts': undefined,
|
||||
'.js': undefined,
|
||||
};
|
||||
|
||||
/** @type {import('module').ResolveHook} */
|
||||
export async function resolve(specifier, context, nextResolve) {
|
||||
// Built-in modules are handled by the default resolver
|
||||
if (isBuiltin(specifier)) {
|
||||
return nextResolve(specifier, context);
|
||||
}
|
||||
|
||||
const ext = extname(specifier);
|
||||
|
||||
// Unless there's an explicit import attribute, JSON files are loaded with our custom loader that's defined below.
|
||||
if (ext === '.json' && !context.importAttributes?.type) {
|
||||
const jsonResult = await nextResolve(specifier, context);
|
||||
return {
|
||||
...jsonResult,
|
||||
format: 'commonjs',
|
||||
importAttributes: { type: 'json' },
|
||||
};
|
||||
}
|
||||
|
||||
// Anything else with an explicit extension is handled by the default
|
||||
// resolver, except that we help determine the module type where needed.
|
||||
if (ext !== '') {
|
||||
return withDetectedModuleType(await nextResolve(specifier, context));
|
||||
}
|
||||
|
||||
// Other external modules are handled by the default resolver, but again we
|
||||
// help determine the module type where needed.
|
||||
if (!specifier.startsWith('.')) {
|
||||
return withDetectedModuleType(await nextResolve(specifier, context));
|
||||
}
|
||||
|
||||
// The rest of this function handles the case of resolving imports that do not
|
||||
// specify any extension and might point to a directory with an `index.*`
|
||||
// file. We resolve those using the same logic as most JS bundlers would, with
|
||||
// the addition of checking if there's an explicit module format listed in the
|
||||
// closest `package.json` file.
|
||||
//
|
||||
// We use a bundle resolution strategy in order to keep code consistent across
|
||||
// Backstage codebases that contains code both for Web and Node.js, and to
|
||||
// support packages with common code that can be used in both environments.
|
||||
try {
|
||||
// This is expected to throw, but in the event that this module specifier is
|
||||
// supported we prefer to use the default resolver.
|
||||
return await nextResolve(specifier, context);
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_UNSUPPORTED_DIR_IMPORT') {
|
||||
const spec = `${specifier}${specifier.endsWith('/') ? '' : '/'}index`;
|
||||
const resolved = await resolveWithoutExt(spec, context, nextResolve);
|
||||
if (resolved) {
|
||||
return withDetectedModuleType(resolved);
|
||||
}
|
||||
} else if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
||||
const resolved = await resolveWithoutExt(specifier, context, nextResolve);
|
||||
if (resolved) {
|
||||
return withDetectedModuleType(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
// Unexpected error or no resolution found
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates the `format` field in the resolved object based on the closest `package.json` file.
|
||||
*
|
||||
* @param {import('module').ResolveFnOutput} resolved
|
||||
* @returns {Promise<import('module').ResolveFnOutput>}
|
||||
*/
|
||||
async function withDetectedModuleType(resolved) {
|
||||
// Already has an explicit format
|
||||
if (resolved.format) {
|
||||
return resolved;
|
||||
}
|
||||
// Happens in Node.js v22 when there's a package.json without an explicit "type" field. Use the default.
|
||||
if (resolved.format === null) {
|
||||
return { ...resolved, format: DEFAULT_MODULE_FORMAT };
|
||||
}
|
||||
|
||||
const ext = extname(resolved.url);
|
||||
|
||||
const explicitFormat = moduleTypeTable[ext];
|
||||
if (explicitFormat) {
|
||||
return {
|
||||
...resolved,
|
||||
format: explicitFormat,
|
||||
};
|
||||
}
|
||||
|
||||
// Under normal circumstances .js files should reliably have a format and so
|
||||
// we should only reach this point for .ts files. However, if additional
|
||||
// custom loaders are being used the format may not be detected for .js files
|
||||
// either. As such we don't restrict the file format at this point.
|
||||
|
||||
// TODO(Rugvip): Does this need caching? kept it simple for now but worth exploring
|
||||
const packageJsonPath = await findPackageJSON(fileURLToPath(resolved.url));
|
||||
if (!packageJsonPath) {
|
||||
return resolved;
|
||||
}
|
||||
|
||||
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
||||
return {
|
||||
...resolved,
|
||||
format: packageJson.type ?? DEFAULT_MODULE_FORMAT,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the closest package.json file from the given path.
|
||||
*
|
||||
* TODO(Rugvip): This can be replaced with the Node.js built-in with the same name once it is stable.
|
||||
* @param {string} startPath
|
||||
* @returns {Promise<string | undefined>}
|
||||
*/
|
||||
async function findPackageJSON(startPath) {
|
||||
let path = startPath;
|
||||
|
||||
// Some confidence check to avoid infinite loop
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
const packagePath = resolvePath(path, 'package.json');
|
||||
if (existsSync(packagePath)) {
|
||||
return packagePath;
|
||||
}
|
||||
|
||||
const newPath = dirname(path);
|
||||
if (newPath === path) {
|
||||
return undefined;
|
||||
}
|
||||
path = newPath;
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Iteration limit reached when searching for package.json at ${startPath}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** @type {import('module').ResolveHook} */
|
||||
async function resolveWithoutExt(specifier, context, nextResolve) {
|
||||
for (const tryExt of SRC_EXTS) {
|
||||
try {
|
||||
const resolved = await nextResolve(specifier + tryExt, {
|
||||
...context,
|
||||
format: 'commonjs',
|
||||
});
|
||||
return {
|
||||
...resolved,
|
||||
format: moduleTypeTable[tryExt] ?? resolved.format,
|
||||
};
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** @type {import('module').LoadHook} */
|
||||
export async function load(url, context, nextLoad) {
|
||||
// Non-file URLs are handled by the default loader
|
||||
if (!url.startsWith('file://')) {
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
|
||||
// JSON files loaded as CommonJS are handled by this custom loader, because
|
||||
// the default one doesn't work. For JSON loading to work we'd need the
|
||||
// synchronous hooks that aren't supported yet, or avoid using the CommonJS
|
||||
// compatibility.
|
||||
if (
|
||||
context.format === 'commonjs' &&
|
||||
context.importAttributes?.type === 'json'
|
||||
) {
|
||||
try {
|
||||
// TODO(Rugvip): Make sure this is valid JSON
|
||||
const content = await readFile(fileURLToPath(url), 'utf8');
|
||||
return {
|
||||
source: `module.exports = (${content})`,
|
||||
format: 'commonjs',
|
||||
shortCircuit: true,
|
||||
};
|
||||
} catch {
|
||||
// Let the default loader generate the error
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
}
|
||||
|
||||
const ext = extname(url);
|
||||
|
||||
// Non-TS files are handled by the default loader
|
||||
if (!TS_EXTS.includes(ext)) {
|
||||
return nextLoad(url, context);
|
||||
}
|
||||
|
||||
const format = context.format ?? DEFAULT_MODULE_FORMAT;
|
||||
|
||||
// We have two choices at this point, we can either transform CommonJS files
|
||||
// and return the transformed source code, or let the default loader handle
|
||||
// them. If we transform them ourselves we will enter CommonJS compatibility
|
||||
// mode in the new module system in Node.js, this effectively means all
|
||||
// CommonJS loaded via `require` calls from this point will all be treated as
|
||||
// if it was loaded via `import` calls from modules.
|
||||
//
|
||||
// The CommonJS compatibility layer will try to identify named exports and
|
||||
// make them available directly, which is convenient as it avoids things like
|
||||
// `import(...).then(m => m.default.foo)`, allowing you to instead write
|
||||
// `import(...).then(m => m.foo)`. The compatibility layer doesn't always work
|
||||
// all that well though, and can lead to module loading issues in many cases,
|
||||
// especially for older code.
|
||||
|
||||
// This `if` block opts-out of using CommonJS compatibility mode by default,
|
||||
// and instead leaves it to our existing loader to transform CommonJS. We do
|
||||
// however use compatibility mode for the more explicit .cts file extension,
|
||||
// allows for a way to opt-in to the new behavior.
|
||||
//
|
||||
// TODO(Rugvip): Once the synchronous hooks API is available for us to use, we might be able to adopt that instead
|
||||
if (format === 'commonjs' && ext !== '.cts') {
|
||||
return nextLoad(url, { ...context, format });
|
||||
}
|
||||
|
||||
// If the Node.js version we're running supports TypeScript, i.e. type
|
||||
// stripping, we hand over to the default loader. This is done for all cases
|
||||
// except if we're loading a .ts file that's been resolved to CommonJS format.
|
||||
// This is because these files aren't actually CommonJS in the Backstage build
|
||||
// system, and need to be transformed to CommonJS.
|
||||
if (
|
||||
format === 'module-typescript' ||
|
||||
(format === 'module-commonjs' && ext !== '.ts')
|
||||
) {
|
||||
return nextLoad(url, { ...context, format });
|
||||
}
|
||||
|
||||
const transformed = await transformFile(fileURLToPath(url), {
|
||||
sourceMaps: 'inline',
|
||||
module: {
|
||||
type: format === 'module' ? 'es6' : 'commonjs',
|
||||
ignoreDynamic: true,
|
||||
|
||||
// This helps the Node.js CommonJS compat layer identify named exports.
|
||||
exportInteropAnnotation: true,
|
||||
},
|
||||
jsc: {
|
||||
target: 'es2023',
|
||||
parser: {
|
||||
syntax: 'typescript',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
...context,
|
||||
shortCircuit: true,
|
||||
source: transformed.code,
|
||||
format,
|
||||
responseURL: url,
|
||||
};
|
||||
}
|
||||
export {
|
||||
resolve,
|
||||
load,
|
||||
} from '@backstage/cli-module-build/config/nodeTransformHooks.mjs';
|
||||
|
||||
@@ -14,18 +14,14 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
// This script is used to pick up and set the public path of the Webpack bundle
|
||||
// at runtime. The meta tag is injected by the app build, but only present in
|
||||
// the `index.html.tmpl` file. The runtime value of the meta tag is populated by
|
||||
// the app backend, when it templates the final `index.html` file.
|
||||
//
|
||||
// This is needed for additional chunks to use the correct public path, and it
|
||||
// is not possible to set the `__webpack_public_path__` variable outside of the
|
||||
// build itself. The Webpack output also does not read any <base> tags or
|
||||
// similar, this seems to be the only way to dynamically configure the public
|
||||
// path at runtime.
|
||||
const el = document.querySelector('meta[name="backstage-public-path"]');
|
||||
const path = el?.getAttribute('content');
|
||||
if (path) {
|
||||
__webpack_public_path__ = path;
|
||||
try {
|
||||
require('@backstage/cli-module-build/config/webpack-public-path');
|
||||
} catch (e) {
|
||||
if (e.code === 'MODULE_NOT_FOUND') {
|
||||
throw new Error(
|
||||
'@backstage/cli-module-build is required to use the webpack public path configuration. ' +
|
||||
'Please install it as a dependency.',
|
||||
);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
@@ -49,6 +49,8 @@
|
||||
"dependencies": {
|
||||
"@backstage/cli-common": "workspace:^",
|
||||
"@backstage/cli-defaults": "workspace:^",
|
||||
"@backstage/cli-module-build": "workspace:^",
|
||||
"@backstage/cli-module-test-jest": "workspace:^",
|
||||
"@backstage/cli-node": "workspace:^",
|
||||
"@backstage/errors": "workspace:^",
|
||||
"@backstage/eslint-plugin": "workspace:^",
|
||||
|
||||
Reference in New Issue
Block a user