Skip to content

feat(devkit): allow requiring cts config files #31103

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 16, 2025
Merged
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 76 additions & 30 deletions packages/devkit/src/utils/config-utils.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -12,28 +12,67 @@ export let dynamicImport = new Function(
export async function loadConfigFile<T extends object = any>(
configFilePath: string
): Promise<T> {
{
let module: any;

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);
const extension = extname(configFilePath);
const module = await loadModule(configFilePath, extension);
return module.default ?? module;
}

async function loadModule(path: string, extension: string): Promise<any> {
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<any> {
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<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 '.mts':
case '.mjs':
return await loadESM(path);
default:
return await load(path);
}
}

Expand Down Expand Up @@ -70,23 +109,30 @@ export function clearRequireCache(): void {
* Load the module after ensuring that the require cache is cleared.
*/
async function load(path: string): Promise<any> {
// 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;
}
}

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