feat(core): add support for loading .ts files using ESM (#21268)
This commit is contained in:
parent
f0d93d0e43
commit
7a83e12234
@ -13,10 +13,32 @@ import * as Mod from 'module';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export function initLocal(workspace: WorkspaceTypeAndRoot) {
|
export function initLocal(workspace: WorkspaceTypeAndRoot) {
|
||||||
|
// If module.register is not available, we need to restart the process with the experimental ESM loader.
|
||||||
|
// Otherwise, usage of `registerTsProject` will not work for `.ts` files using ESM.
|
||||||
|
// TODO: Remove this once Node 18 is out of LTS (March 2024).
|
||||||
|
if (shouldRestartWithExperimentalTsEsmLoader()) {
|
||||||
|
const child = require('child_process').fork(
|
||||||
|
require.resolve('./nx'),
|
||||||
|
process.argv.slice(2),
|
||||||
|
{
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
RESTARTED_WITH_EXPERIMENTAL_TS_ESM_LOADER: '1',
|
||||||
|
},
|
||||||
|
execArgv: execArgvWithExperimentalLoaderOptions(),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
child.on('close', (code: number | null) => {
|
||||||
|
if (code !== 0 && code !== null) process.exit(code);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
process.env.NX_CLI_SET = 'true';
|
process.env.NX_CLI_SET = 'true';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
performance.mark('init-local');
|
performance.mark('init-local');
|
||||||
|
|
||||||
monkeyPatchRequire();
|
monkeyPatchRequire();
|
||||||
|
|
||||||
if (workspace.type !== 'nx' && shouldDelegateToAngularCLI()) {
|
if (workspace.type !== 'nx' && shouldDelegateToAngularCLI()) {
|
||||||
@ -229,3 +251,36 @@ function monkeyPatchRequire() {
|
|||||||
// do some side-effect of your own
|
// do some side-effect of your own
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldRestartWithExperimentalTsEsmLoader(): boolean {
|
||||||
|
// Already restarted with experimental loader
|
||||||
|
if (process.env.RESTARTED_WITH_EXPERIMENTAL_TS_ESM_LOADER === '1')
|
||||||
|
return false;
|
||||||
|
const nodeVersion = parseInt(process.versions.node.split('.')[0]);
|
||||||
|
// `--experimental-loader` is only supported in Nodejs >= 16 so there is no point restarting for older versions
|
||||||
|
if (nodeVersion < 16) return false;
|
||||||
|
// Node 20.6.0 adds `module.register`, otherwise we need to restart process with "--experimental-loader ts-node/esm".
|
||||||
|
return (
|
||||||
|
!require('node:module').register &&
|
||||||
|
moduleResolves('ts-node/esm') &&
|
||||||
|
moduleResolves('typescript')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function execArgvWithExperimentalLoaderOptions() {
|
||||||
|
return [
|
||||||
|
...process.execArgv,
|
||||||
|
'--no-warnings',
|
||||||
|
'--experimental-loader',
|
||||||
|
'ts-node/esm',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function moduleResolves(packageName: string) {
|
||||||
|
try {
|
||||||
|
require.resolve(packageName);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const swcNodeInstalled = packageIsInstalled('@swc-node/register');
|
|||||||
const tsNodeInstalled = packageIsInstalled('ts-node/register');
|
const tsNodeInstalled = packageIsInstalled('ts-node/register');
|
||||||
let ts: typeof import('typescript');
|
let ts: typeof import('typescript');
|
||||||
|
|
||||||
|
let isTsEsmLoaderRegistered = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require
|
* Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require
|
||||||
* register hooks so that .ts files can be used for writing custom workspace projects.
|
* register hooks so that .ts files can be used for writing custom workspace projects.
|
||||||
@ -43,6 +45,19 @@ export function registerTsProject(
|
|||||||
registerTranspiler(compilerOptions),
|
registerTranspiler(compilerOptions),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Add ESM support for `.ts` files.
|
||||||
|
// NOTE: There is no cleanup function for this, as it's not possible to unregister the loader.
|
||||||
|
// Based on limited testing, it doesn't seem to matter if we register it multiple times, but just in
|
||||||
|
// case let's keep a flag to prevent it.
|
||||||
|
if (!isTsEsmLoaderRegistered) {
|
||||||
|
const module = require('node:module');
|
||||||
|
if (module.register && packageIsInstalled('ts-node/esm')) {
|
||||||
|
const url = require('node:url');
|
||||||
|
module.register(url.pathToFileURL(require.resolve('ts-node/esm')));
|
||||||
|
}
|
||||||
|
isTsEsmLoaderRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
for (const fn of cleanupFunctions) {
|
for (const fn of cleanupFunctions) {
|
||||||
fn();
|
fn();
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { PlaywrightTestConfig } from '@playwright/test';
|
|||||||
// we overwrite the dynamic import function to use the regular syntax, which
|
// we overwrite the dynamic import function to use the regular syntax, which
|
||||||
// jest does handle.
|
// jest does handle.
|
||||||
import * as lcf from '../utils/load-config-file';
|
import * as lcf from '../utils/load-config-file';
|
||||||
(lcf as any).dynamicImport = (m) => import(m);
|
(lcf as any).dynamicImport = (m) => require(m.split('?')[0]);
|
||||||
|
|
||||||
describe('@nx/playwright/plugin', () => {
|
describe('@nx/playwright/plugin', () => {
|
||||||
let createNodesFunction = createNodes[1];
|
let createNodesFunction = createNodes[1];
|
||||||
@ -34,7 +34,7 @@ describe('@nx/playwright/plugin', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
tempFs.cleanup();
|
// tempFs.cleanup();
|
||||||
jest.resetModules();
|
jest.resetModules();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -14,43 +14,25 @@ export async function loadPlaywrightConfig(
|
|||||||
): Promise<PlaywrightTestConfig> {
|
): Promise<PlaywrightTestConfig> {
|
||||||
{
|
{
|
||||||
let module: any;
|
let module: any;
|
||||||
|
const configPathWithTimestamp = `${configFilePath}?t=${Date.now()}`;
|
||||||
if (extname(configFilePath) === '.ts') {
|
if (extname(configFilePath) === '.ts') {
|
||||||
const tsConfigPath = getRootTsConfigPath();
|
const tsConfigPath = getRootTsConfigPath();
|
||||||
|
|
||||||
if (tsConfigPath) {
|
if (tsConfigPath) {
|
||||||
const unregisterTsProject = registerTsProject(tsConfigPath);
|
const unregisterTsProject = registerTsProject(tsConfigPath);
|
||||||
try {
|
try {
|
||||||
// Require's cache doesn't notice when the file is updated, and
|
module = await dynamicImport(configPathWithTimestamp);
|
||||||
// this function is ran during daemon operation. If the config file
|
|
||||||
// is updated, we need to read its new contents, so we need to clear the cache.
|
|
||||||
// We can't just delete the cache entry for the config file, because
|
|
||||||
// it might have imports that need to be updated as well.
|
|
||||||
clearRequireCache();
|
|
||||||
// ts-node doesn't support dynamic import, so we need to use require
|
|
||||||
module = require(configFilePath);
|
|
||||||
} finally {
|
} finally {
|
||||||
unregisterTsProject();
|
unregisterTsProject();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
module = await dynamicImport(configFilePath);
|
module = await dynamicImport(configPathWithTimestamp);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
module = await dynamicImport(configFilePath);
|
module = await dynamicImport(configPathWithTimestamp);
|
||||||
}
|
}
|
||||||
return module.default ?? module;
|
return module.default ?? module;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
const packageInstallationDirectories = ['node_modules', '.yarn'];
|
||||||
|
|
||||||
function clearRequireCache() {
|
|
||||||
Object.keys(require.cache).forEach((key: string) => {
|
|
||||||
// We don't want to clear the require cache of installed packages.
|
|
||||||
// Clearing them can cause some issues when running Nx without the daemon
|
|
||||||
// and may cause issues for other packages that use the module state
|
|
||||||
// in some to store cached information.
|
|
||||||
if (!packageInstallationDirectories.some((dir) => key.includes(dir))) {
|
|
||||||
delete require.cache[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user