From bd88b0efe48dc1006adb8df2a005828579f5a47d Mon Sep 17 00:00:00 2001 From: Nicolas Marien Date: Fri, 16 May 2025 17:58:34 +0200 Subject: [PATCH] feat(devkit): allow requiring cts config files (#31103) When migrating our project to esm, we encountered an issue with the playright plugin, but more generally with the `loadConfigFile` from the devkit. Our configuration is a `.cts` file, but it's not treated as commonjs: `__dirname` and `__filename` are not available. ![CleanShot 2025-05-07 at 15 31 04@2x](https://github.com/user-attachments/assets/e8299b4e-153b-4cb4-98b7-d806e537ab12) ## Expected Behavior `.cts` files are interpreted as commonJS files when in a module context. --------- Co-authored-by: Jack Hsu --- packages/devkit/src/utils/config-utils.ts | 111 +++++++++++++++------- 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/packages/devkit/src/utils/config-utils.ts b/packages/devkit/src/utils/config-utils.ts index 8ee479c06d..fc0c7ff6df 100644 --- a/packages/devkit/src/utils/config-utils.ts +++ b/packages/devkit/src/utils/config-utils.ts @@ -1,8 +1,8 @@ -import { dirname, extname, join, sep } from 'path'; import { existsSync, readdirSync } from 'fs'; import { pathToFileURL } from 'node:url'; import { workspaceRoot } from 'nx/src/devkit-exports'; import { registerTsProject } from 'nx/src/devkit-internals'; +import { dirname, extname, join, sep } from 'path'; export let dynamicImport = new Function( 'modulePath', @@ -12,28 +12,68 @@ export let dynamicImport = new Function( export async function loadConfigFile( configFilePath: string ): Promise { - { - let module: any; + const extension = extname(configFilePath); + const module = await loadModule(configFilePath, extension); + return module.default ?? module; +} - if (extname(configFilePath) === '.ts') { - const siblingFiles = readdirSync(dirname(configFilePath)); - const tsConfigPath = siblingFiles.includes('tsconfig.json') - ? join(dirname(configFilePath), 'tsconfig.json') - : getRootTsConfigPath(); - if (tsConfigPath) { - const unregisterTsProject = registerTsProject(tsConfigPath); - try { - module = await load(configFilePath); - } finally { - unregisterTsProject(); - } - } else { - module = await load(configFilePath); - } - } else { - module = await load(configFilePath); +async function loadModule(path: string, extension: string): Promise { + if (isTypeScriptFile(extension)) { + return await loadTypeScriptModule(path, extension); + } + return await loadJavaScriptModule(path, extension); +} + +function isTypeScriptFile(extension: string): boolean { + return extension.endsWith('ts'); +} + +async function loadTypeScriptModule( + path: string, + extension: string +): Promise { + const tsConfigPath = getTypeScriptConfigPath(path); + + if (tsConfigPath) { + const unregisterTsProject = registerTsProject(tsConfigPath); + try { + return await loadModuleByExtension(path, extension); + } finally { + unregisterTsProject(); } - return module.default ?? module; + } + + return await loadModuleByExtension(path, extension); +} + +function getTypeScriptConfigPath(path: string): string | null { + const siblingFiles = readdirSync(dirname(path)); + return siblingFiles.includes('tsconfig.json') + ? join(dirname(path), 'tsconfig.json') + : getRootTsConfigPath(); +} + +async function loadJavaScriptModule( + path: string, + extension: string +): Promise { + return await loadModuleByExtension(path, extension); +} + +async function loadModuleByExtension( + path: string, + extension: string +): Promise { + switch (extension) { + case '.cts': + case '.cjs': + return await loadCommonJS(path); + case '.mjs': + return await loadESM(path); + default: + // For both .ts and .mts files, try to load them as CommonJS first, then try ESM. + // It's possible that the file is written like ESM (e.g. using `import`) but uses CJS features like `__dirname` or `__filename`. + return await load(path); } } @@ -66,27 +106,34 @@ export function clearRequireCache(): void { } } -/** - * Load the module after ensuring that the require cache is cleared. - */ async function load(path: string): Promise { - // Clear cache if the path is in the cache - if (require.cache[path]) { - clearRequireCache(); - } - try { // Try using `require` first, which works for CJS modules. // Modules are CJS unless it is named `.mjs` or `package.json` sets type to "module". - return require(path); + return loadCommonJS(path); } catch (e: any) { if (e.code === 'ERR_REQUIRE_ESM') { // If `require` fails to load ESM, try dynamic `import()`. ESM requires file url protocol for handling absolute paths. - const pathAsFileUrl = pathToFileURL(path).pathname; - return await dynamicImport(`${pathAsFileUrl}?t=${Date.now()}`); + return loadESM(path); } // Re-throw all other errors throw e; } } + +/** + * Load the module after ensuring that the require cache is cleared. + */ +async function loadCommonJS(path: string): Promise { + // Clear cache if the path is in the cache + if (require.cache[path]) { + clearRequireCache(); + } + return require(path); +} + +async function loadESM(path: string): Promise { + const pathAsFileUrl = pathToFileURL(path).pathname; + return await dynamicImport(`${pathAsFileUrl}?t=${Date.now()}`); +}