diff --git a/packages/nx/bin/init-local.ts b/packages/nx/bin/init-local.ts index f9cfcf9e40..0d015ba4d8 100644 --- a/packages/nx/bin/init-local.ts +++ b/packages/nx/bin/init-local.ts @@ -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; + } +} diff --git a/packages/nx/src/plugins/js/utils/register.ts b/packages/nx/src/plugins/js/utils/register.ts index 0a8cfb764d..67109d6d2c 100644 --- a/packages/nx/src/plugins/js/utils/register.ts +++ b/packages/nx/src/plugins/js/utils/register.ts @@ -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(); diff --git a/packages/playwright/src/plugins/plugin.spec.ts b/packages/playwright/src/plugins/plugin.spec.ts index 7353f5f86a..98739fb3b5 100644 --- a/packages/playwright/src/plugins/plugin.spec.ts +++ b/packages/playwright/src/plugins/plugin.spec.ts @@ -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(); }); diff --git a/packages/playwright/src/utils/load-config-file.ts b/packages/playwright/src/utils/load-config-file.ts index 37a02dd917..b9ecaca793 100644 --- a/packages/playwright/src/utils/load-config-file.ts +++ b/packages/playwright/src/utils/load-config-file.ts @@ -14,43 +14,25 @@ export async function loadPlaywrightConfig( ): Promise { { 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]; - } - }); -}