diff --git a/packages/cypress/src/plugins/plugin.spec.ts b/packages/cypress/src/plugins/plugin.spec.ts index adb4b904cbcd4..1d9b6badad5af 100644 --- a/packages/cypress/src/plugins/plugin.spec.ts +++ b/packages/cypress/src/plugins/plugin.spec.ts @@ -6,12 +6,6 @@ import { TempFs } from 'nx/src/internal-testing-utils/temp-fs'; import { join } from 'path'; import { nxE2EPreset } from '../../plugins/cypress-preset'; -// Jest can't handle the dynamic import, and mocking it doesn't work either. -// 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) => require(m.split('?')[0]); - describe('@nx/cypress/plugin', () => { let createNodesFunction = createNodes[1]; let context: CreateNodesContext; diff --git a/packages/cypress/src/plugins/plugin.ts b/packages/cypress/src/plugins/plugin.ts index f42e82dfc3f14..a9fe3bf7ff8d4 100644 --- a/packages/cypress/src/plugins/plugin.ts +++ b/packages/cypress/src/plugins/plugin.ts @@ -8,10 +8,9 @@ import { TargetConfiguration, writeJsonFile, } from '@nx/devkit'; -import { dirname, extname, join, relative } from 'path'; -import { registerTsProject } from '@nx/js/src/internal'; +import { dirname, join, relative } from 'path'; -import { getLockFileName, getRootTsConfigPath } from '@nx/js'; +import { getLockFileName } from '@nx/js'; import { CypressExecutorOptions } from '../executors/cypress/cypress.impl'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; @@ -20,7 +19,7 @@ import { globWithWorkspaceContext } from 'nx/src/utils/workspace-context'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { NX_PLUGIN_OPTIONS } from '../utils/symbols'; -import { getCypressConfig } from '../utils/load-config-file'; +import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; export interface CypressPluginOptions { ciTargetName?: string; @@ -152,7 +151,7 @@ async function buildCypressTargets( options: CypressPluginOptions, context: CreateNodesContext ) { - const cypressConfig = await getCypressConfig( + const cypressConfig = await loadConfigFile( join(context.workspaceRoot, configFilePath) ); diff --git a/packages/cypress/src/utils/load-config-file.ts b/packages/cypress/src/utils/load-config-file.ts deleted file mode 100644 index 2fa8872a974bb..0000000000000 --- a/packages/cypress/src/utils/load-config-file.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { extname } from 'path'; -import { getRootTsConfigPath } from '@nx/js'; -import { registerTsProject } from '@nx/js/src/internal'; - -export let dynamicImport = new Function( - 'modulePath', - 'return import(modulePath);' -); - -export async function getCypressConfig(configFilePath: string): Promise { - let module: any; - if (extname(configFilePath) === '.ts') { - const tsConfigPath = getRootTsConfigPath(); - - if (tsConfigPath) { - const unregisterTsProject = registerTsProject(tsConfigPath); - try { - module = await dynamicImport(configFilePath); - } finally { - unregisterTsProject(); - } - } else { - module = await dynamicImport(configFilePath); - } - } else { - module = await dynamicImport(configFilePath); - } - return module.default ?? module; -} diff --git a/packages/devkit/src/utils/config-utils.ts b/packages/devkit/src/utils/config-utils.ts new file mode 100644 index 0000000000000..0763a8507e180 --- /dev/null +++ b/packages/devkit/src/utils/config-utils.ts @@ -0,0 +1,78 @@ +import { extname, join } from 'path'; +import { existsSync } from 'fs'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { workspaceRoot } from 'nx/src/devkit-exports'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { registerTsProject } from 'nx/src/plugins/js/utils/register'; + +export let dynamicImport = new Function( + 'modulePath', + 'return import(modulePath);' +); + +export async function loadConfigFile( + configFilePath: string +): Promise { + { + let module: any; + + if (extname(configFilePath) === '.ts') { + const tsConfigPath = getRootTsConfigPath(); + if (tsConfigPath) { + const unregisterTsProject = registerTsProject(tsConfigPath); + try { + module = await load(configFilePath); + } finally { + unregisterTsProject(); + } + } else { + module = await load(configFilePath); + } + } else { + module = await load(configFilePath); + } + return module.default ?? module; + } +} + +export function getRootTsConfigPath(): string | null { + const tsConfigFileName = getRootTsConfigFileName(); + return tsConfigFileName ? join(workspaceRoot, tsConfigFileName) : null; +} + +export function getRootTsConfigFileName(): string | null { + for (const tsConfigName of ['tsconfig.base.json', 'tsconfig.json']) { + const pathExists = existsSync(join(workspaceRoot, tsConfigName)); + if (pathExists) { + return tsConfigName; + } + } + + return null; +} + +/** + * 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]) { + for (const k of Object.keys(require.cache)) { + delete require.cache[k]; + } + } + + 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); + } catch (e: any) { + if (e.code === 'ERR_REQUIRE_ESM') { + // If `require` fails to load ESM, try dynamic `import()`. + return await dynamicImport(`${path}?t=${Date.now()}`); + } + + // Re-throw all other errors + throw e; + } +} diff --git a/packages/playwright/src/plugins/plugin.spec.ts b/packages/playwright/src/plugins/plugin.spec.ts index 98739fb3b597f..9c104564ba30d 100644 --- a/packages/playwright/src/plugins/plugin.spec.ts +++ b/packages/playwright/src/plugins/plugin.spec.ts @@ -4,12 +4,6 @@ import { TempFs } from '@nx/devkit/internal-testing-utils'; import { createNodes } from './plugin'; import { PlaywrightTestConfig } from '@playwright/test'; -// Jest can't handle the dynamic import, and mocking it doesn't work either. -// 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) => require(m.split('?')[0]); - describe('@nx/playwright/plugin', () => { let createNodesFunction = createNodes[1]; let context: CreateNodesContext; diff --git a/packages/playwright/src/plugins/plugin.ts b/packages/playwright/src/plugins/plugin.ts index 65bdb94246f8a..6b77beae44b44 100644 --- a/packages/playwright/src/plugins/plugin.ts +++ b/packages/playwright/src/plugins/plugin.ts @@ -17,9 +17,9 @@ import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash import type { PlaywrightTestConfig } from '@playwright/test'; import { getFilesInDirectoryUsingContext } from 'nx/src/utils/workspace-context'; import { minimatch } from 'minimatch'; -import { loadPlaywrightConfig } from '../utils/load-config-file'; import { projectGraphCacheDirectory } from 'nx/src/utils/cache-directory'; import { getLockFileName } from '@nx/js'; +import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; export interface PlaywrightPluginOptions { targetName?: string; @@ -106,7 +106,12 @@ async function buildPlaywrightTargets( options: NormalizedOptions, context: CreateNodesContext ) { - const playwrightConfig: PlaywrightTestConfig = await loadPlaywrightConfig( + // Playwright forbids importing the `@playwright/test` module twice. This would affect running the tests, + // but we're just reading the config so let's delete the variable they are using to detect this. + // See: https://github.com/microsoft/playwright/pull/11218/files + delete (process as any)['__pw_initiator__']; + + const playwrightConfig = await loadConfigFile( join(context.workspaceRoot, configFilePath) ); diff --git a/packages/playwright/src/utils/load-config-file.ts b/packages/playwright/src/utils/load-config-file.ts deleted file mode 100644 index b9ecaca793786..0000000000000 --- a/packages/playwright/src/utils/load-config-file.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { extname } from 'path'; -import { getRootTsConfigPath } from '@nx/js'; -import { registerTsProject } from '@nx/js/src/internal'; - -import type { PlaywrightTestConfig } from '@playwright/test'; - -export let dynamicImport = new Function( - 'modulePath', - 'return import(modulePath);' -); - -export async function loadPlaywrightConfig( - configFilePath -): Promise { - { - let module: any; - const configPathWithTimestamp = `${configFilePath}?t=${Date.now()}`; - if (extname(configFilePath) === '.ts') { - const tsConfigPath = getRootTsConfigPath(); - - if (tsConfigPath) { - const unregisterTsProject = registerTsProject(tsConfigPath); - try { - module = await dynamicImport(configPathWithTimestamp); - } finally { - unregisterTsProject(); - } - } else { - module = await dynamicImport(configPathWithTimestamp); - } - } else { - module = await dynamicImport(configPathWithTimestamp); - } - return module.default ?? module; - } -} - -const packageInstallationDirectories = ['node_modules', '.yarn']; diff --git a/packages/remix/src/plugins/plugin.ts b/packages/remix/src/plugins/plugin.ts index cc44a63636f1a..93075b44af5fd 100644 --- a/packages/remix/src/plugins/plugin.ts +++ b/packages/remix/src/plugins/plugin.ts @@ -3,18 +3,18 @@ import { type CreateDependencies, type CreateNodes, type CreateNodesContext, - type TargetConfiguration, detectPackageManager, readJsonFile, + type TargetConfiguration, writeJsonFile, } from '@nx/devkit'; import { calculateHashForCreateNodes } from '@nx/devkit/src/utils/calculate-hash-for-create-nodes'; import { getNamedInputs } from '@nx/devkit/src/utils/get-named-inputs'; -import { getLockFileName, getRootTsConfigPath } from '@nx/js'; -import { registerTsProject } from '@nx/js/src/internal'; +import { getLockFileName } from '@nx/js'; import { type AppConfig } from '@remix-run/dev'; -import { join, dirname } from 'path'; +import { dirname, join } from 'path'; import { existsSync, readdirSync } from 'fs'; +import { loadConfigFile } from '@nx/devkit/src/utils/config-utils'; const cachePath = join(projectGraphCacheDirectory, 'remix.hash'); const targetsCache = existsSync(cachePath) ? readTargetsCache() : {}; @@ -181,24 +181,7 @@ async function getServerBuildPath( workspaceRoot: string ): Promise { const configPath = join(workspaceRoot, configFilePath); - let appConfig: AppConfig = {}; - try { - let appConfigModule: any; - try { - appConfigModule = await Function( - `return import("${configPath}?t=${Date.now()}")` - )(); - } catch { - appConfigModule = require(configPath); - } - - appConfig = appConfigModule?.default || appConfigModule; - } catch (error) { - throw new Error( - `Error loading Remix config at ${configFilePath}\n${String(error)}` - ); - } - + let appConfig = await loadConfigFile(configPath); return appConfig.serverBuildPath ?? 'build/index.js'; }