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.  ## Expected Behavior `.cts` files are interpreted as commonJS files when in a module context. --------- Co-authored-by: Jack Hsu <jack.hsu@gmail.com>
This commit is contained in:
parent
1709b107bb
commit
bd88b0efe4
@ -1,8 +1,8 @@
|
|||||||
import { dirname, extname, join, sep } from 'path';
|
|
||||||
import { existsSync, readdirSync } from 'fs';
|
import { existsSync, readdirSync } from 'fs';
|
||||||
import { pathToFileURL } from 'node:url';
|
import { pathToFileURL } from 'node:url';
|
||||||
import { workspaceRoot } from 'nx/src/devkit-exports';
|
import { workspaceRoot } from 'nx/src/devkit-exports';
|
||||||
import { registerTsProject } from 'nx/src/devkit-internals';
|
import { registerTsProject } from 'nx/src/devkit-internals';
|
||||||
|
import { dirname, extname, join, sep } from 'path';
|
||||||
|
|
||||||
export let dynamicImport = new Function(
|
export let dynamicImport = new Function(
|
||||||
'modulePath',
|
'modulePath',
|
||||||
@ -12,28 +12,68 @@ export let dynamicImport = new Function(
|
|||||||
export async function loadConfigFile<T extends object = any>(
|
export async function loadConfigFile<T extends object = any>(
|
||||||
configFilePath: string
|
configFilePath: string
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
{
|
const extension = extname(configFilePath);
|
||||||
let module: any;
|
const module = await loadModule(configFilePath, extension);
|
||||||
|
return module.default ?? module;
|
||||||
|
}
|
||||||
|
|
||||||
if (extname(configFilePath) === '.ts') {
|
async function loadModule(path: string, extension: string): Promise<any> {
|
||||||
const siblingFiles = readdirSync(dirname(configFilePath));
|
if (isTypeScriptFile(extension)) {
|
||||||
const tsConfigPath = siblingFiles.includes('tsconfig.json')
|
return await loadTypeScriptModule(path, extension);
|
||||||
? join(dirname(configFilePath), 'tsconfig.json')
|
}
|
||||||
: getRootTsConfigPath();
|
return await loadJavaScriptModule(path, extension);
|
||||||
if (tsConfigPath) {
|
}
|
||||||
const unregisterTsProject = registerTsProject(tsConfigPath);
|
|
||||||
try {
|
function isTypeScriptFile(extension: string): boolean {
|
||||||
module = await load(configFilePath);
|
return extension.endsWith('ts');
|
||||||
} finally {
|
}
|
||||||
unregisterTsProject();
|
|
||||||
}
|
async function loadTypeScriptModule(
|
||||||
} else {
|
path: string,
|
||||||
module = await load(configFilePath);
|
extension: string
|
||||||
}
|
): Promise<any> {
|
||||||
} else {
|
const tsConfigPath = getTypeScriptConfigPath(path);
|
||||||
module = await load(configFilePath);
|
|
||||||
|
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<any> {
|
||||||
|
return await loadModuleByExtension(path, extension);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadModuleByExtension(
|
||||||
|
path: string,
|
||||||
|
extension: string
|
||||||
|
): Promise<any> {
|
||||||
|
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<any> {
|
async function load(path: string): Promise<any> {
|
||||||
// Clear cache if the path is in the cache
|
|
||||||
if (require.cache[path]) {
|
|
||||||
clearRequireCache();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try using `require` first, which works for CJS modules.
|
// Try using `require` first, which works for CJS modules.
|
||||||
// Modules are CJS unless it is named `.mjs` or `package.json` sets type to "module".
|
// Modules are CJS unless it is named `.mjs` or `package.json` sets type to "module".
|
||||||
return require(path);
|
return loadCommonJS(path);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
if (e.code === 'ERR_REQUIRE_ESM') {
|
if (e.code === 'ERR_REQUIRE_ESM') {
|
||||||
// If `require` fails to load ESM, try dynamic `import()`. ESM requires file url protocol for handling absolute paths.
|
// If `require` fails to load ESM, try dynamic `import()`. ESM requires file url protocol for handling absolute paths.
|
||||||
const pathAsFileUrl = pathToFileURL(path).pathname;
|
return loadESM(path);
|
||||||
return await dynamicImport(`${pathAsFileUrl}?t=${Date.now()}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-throw all other errors
|
// Re-throw all other errors
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load the module after ensuring that the require cache is cleared.
|
||||||
|
*/
|
||||||
|
async function loadCommonJS(path: string): Promise<any> {
|
||||||
|
// Clear cache if the path is in the cache
|
||||||
|
if (require.cache[path]) {
|
||||||
|
clearRequireCache();
|
||||||
|
}
|
||||||
|
return require(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadESM(path: string): Promise<any> {
|
||||||
|
const pathAsFileUrl = pathToFileURL(path).pathname;
|
||||||
|
return await dynamicImport(`${pathAsFileUrl}?t=${Date.now()}`);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user