feat(core): add support for loading .ts files using ESM (#21268)

This commit is contained in:
Jack Hsu 2024-01-31 08:49:06 -05:00 committed by GitHub
parent f0d93d0e43
commit 7a83e12234
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 76 additions and 24 deletions

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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();
}); });

View File

@ -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];
}
});
}