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) {
// 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';
try {
performance.mark('init-local');
monkeyPatchRequire();
if (workspace.type !== 'nx' && shouldDelegateToAngularCLI()) {
@ -229,3 +251,36 @@ function monkeyPatchRequire() {
// 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');
let ts: typeof import('typescript');
let isTsEsmLoaderRegistered = false;
/**
* 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.
@ -43,6 +45,19 @@ export function registerTsProject(
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 () => {
for (const fn of cleanupFunctions) {
fn();

View File

@ -8,7 +8,7 @@ import { PlaywrightTestConfig } from '@playwright/test';
// we overwrite the dynamic import function to use the regular syntax, which
// jest does handle.
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', () => {
let createNodesFunction = createNodes[1];
@ -34,7 +34,7 @@ describe('@nx/playwright/plugin', () => {
});
afterEach(() => {
tempFs.cleanup();
// tempFs.cleanup();
jest.resetModules();
});

View File

@ -14,43 +14,25 @@ export async function loadPlaywrightConfig(
): Promise<PlaywrightTestConfig> {
{
let module: any;
const configPathWithTimestamp = `${configFilePath}?t=${Date.now()}`;
if (extname(configFilePath) === '.ts') {
const tsConfigPath = getRootTsConfigPath();
if (tsConfigPath) {
const unregisterTsProject = registerTsProject(tsConfigPath);
try {
// Require's cache doesn't notice when the file is updated, and
// 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);
module = await dynamicImport(configPathWithTimestamp);
} finally {
unregisterTsProject();
}
} else {
module = await dynamicImport(configFilePath);
module = await dynamicImport(configPathWithTimestamp);
}
} else {
module = await dynamicImport(configFilePath);
module = await dynamicImport(configPathWithTimestamp);
}
return module.default ?? module;
}
}
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];
}
});
}