Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

nx

Package Overview
Dependencies
Maintainers
8
Versions
2573
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

nx - npm Package Compare versions

Comparing version
23.0.0-beta.15
to
23.0.0-beta.16
+7
-19
dist/src/command-l...ase/utils/resolve-changelog-renderer.js

@@ -5,3 +5,2 @@ "use strict";

const register_1 = require("../../../plugins/js/utils/register");
const typescript_1 = require("../../../plugins/js/utils/typescript");
const utils_1 = require("../../../tasks-runner/utils");

@@ -17,20 +16,9 @@ const workspace_root_1 = require("../../../utils/workspace-root");

});
// Try and load the provided (or default) changelog renderer
let ChangelogRendererClass;
let cleanupTranspiler = () => { };
try {
const rootTsconfigPath = (0, typescript_1.getRootTsConfigPath)();
if (rootTsconfigPath) {
cleanupTranspiler = (0, register_1.registerTsProject)(rootTsconfigPath);
}
const r = require(interpolatedChangelogRendererPath);
ChangelogRendererClass = r.default || r;
}
catch (err) {
throw err;
}
finally {
cleanupTranspiler();
}
return ChangelogRendererClass;
// TS renderers go through loadTsFile (native-strip -> swc/ts-node + paths).
// JS renderers use require() with a lazy tsconfig-paths fallback so workspace
// alias imports still resolve, without paying registration cost up front.
const r = /\.[cm]?ts$/.test(interpolatedChangelogRendererPath)
? (0, register_1.loadTsFile)(interpolatedChangelogRendererPath)
: (0, register_1.requireWithTsconfigFallback)(interpolatedChangelogRendererPath);
return r.default || r;
}

@@ -7,3 +7,2 @@ "use strict";

const register_1 = require("../../../plugins/js/utils/register");
const typescript_1 = require("../../../plugins/js/utils/typescript");
const utils_1 = require("../../../tasks-runner/utils");

@@ -56,8 +55,5 @@ const workspace_root_1 = require("../../../utils/workspace-root");

else {
let cleanupTranspiler;
if (versionActionsPath.endsWith('.ts')) {
cleanupTranspiler = (0, register_1.registerTsProject)((0, typescript_1.getRootTsConfigPath)());
}
const loaded = require(versionActionsPath);
cleanupTranspiler?.();
const loaded = /\.[cm]?ts$/.test(versionActionsPath)
? (0, register_1.loadTsFile)(versionActionsPath)
: (0, register_1.requireWithTsconfigFallback)(versionActionsPath);
VersionActionsClass = loaded.default ?? loaded;

@@ -64,0 +60,0 @@ if (!VersionActionsClass) {

@@ -9,4 +9,4 @@ "use strict";

const resolve_exports_1 = require("resolve.exports");
const register_1 = require("../plugins/js/utils/register");
const packages_1 = require("../plugins/js/utils/packages");
const plugins_1 = require("../project-graph/plugins");
const path_2 = require("../utils/path");

@@ -23,6 +23,10 @@ /**

const modulePath = resolveImplementation(implementationModulePath, directory, packageName, projects);
if ((0, path_1.extname)(modulePath) === '.ts') {
(0, plugins_1.registerPluginTSTranspiler)();
}
const module = require(modulePath);
// Route .ts entrypoints through loadTsFile so the native-strip ->
// swc/ts-node fallback chain runs. Plain require() bypasses the matcher
// set and bubbles errors like extensionless `./schema` imports (strict
// ESM resolution failures) straight to the CLI. JS entrypoints use
// requireWithTsconfigFallback so workspace-alias imports still resolve.
const module = /\.[cm]?ts$/.test(modulePath)
? (0, register_1.loadTsFile)(modulePath)
: (0, register_1.requireWithTsconfigFallback)(modulePath);
return implementationExportName

@@ -29,0 +33,0 @@ ? module[implementationExportName]

@@ -28,3 +28,3 @@ /**

export * from './project-graph/error-types';
export { registerTsProject } from './plugins/js/utils/register';
export { registerTsProject, loadTsFile, forceRegisterEsmLoader, requireWithTsconfigFallback, } from './plugins/js/utils/register';
export { interpolate } from './tasks-runner/utils';

@@ -31,0 +31,0 @@ export { isCI } from './utils/is-ci';

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.handleImport = exports.signalToCode = exports.globalSpinner = exports.readYamlFile = exports.isUsingPrettierInTree = exports.isCI = exports.interpolate = exports.registerTsProject = exports.LoadedNxPlugin = exports.retrieveProjectConfigurations = exports.findProjectForPath = exports.createProjectRootMappings = exports.createProjectRootMappingsFromProjectConfigurations = exports.hashMultiGlobWithWorkspaceContext = exports.hashWithWorkspaceContext = exports.hashObject = exports.splitByColons = exports.installPackageToTmpAsync = exports.installPackageToTmp = exports.readModulePackageJson = exports.stripIndent = exports.sortObjectByKeys = exports.combineOptionsForExecutor = exports.splitTarget = exports.getIgnoreObjectForTree = exports.normalizeTargetDefaultsAgainstRootMaps = exports.readTargetDefaultsForTarget = exports.findMatchingConfigFiles = exports.readProjectConfigurationsFromRootMap = exports.mergeTargetConfigurations = exports.retrieveProjectConfigurationsWithAngularProjects = exports.calculateDefaultProjectName = exports.readNxJsonFromDisk = exports.parseExecutor = exports.getExecutorInformation = exports.createTempNpmDirectory = void 0;
exports.emitPluginWorkerLog = exports.safeWriteFileCache = exports.PluginCache = exports.handleImport = exports.signalToCode = exports.globalSpinner = exports.readYamlFile = exports.isUsingPrettierInTree = exports.isCI = exports.interpolate = exports.requireWithTsconfigFallback = exports.forceRegisterEsmLoader = exports.loadTsFile = exports.registerTsProject = exports.LoadedNxPlugin = exports.retrieveProjectConfigurations = exports.findProjectForPath = exports.createProjectRootMappings = exports.createProjectRootMappingsFromProjectConfigurations = exports.hashMultiGlobWithWorkspaceContext = exports.hashWithWorkspaceContext = exports.hashObject = exports.splitByColons = exports.installPackageToTmpAsync = exports.installPackageToTmp = exports.readModulePackageJson = exports.stripIndent = exports.sortObjectByKeys = exports.combineOptionsForExecutor = exports.splitTarget = exports.getIgnoreObjectForTree = exports.normalizeTargetDefaultsAgainstRootMaps = exports.readTargetDefaultsForTarget = exports.findMatchingConfigFiles = exports.readProjectConfigurationsFromRootMap = exports.mergeTargetConfigurations = exports.retrieveProjectConfigurationsWithAngularProjects = exports.calculateDefaultProjectName = exports.readNxJsonFromDisk = exports.parseExecutor = exports.getExecutorInformation = exports.createTempNpmDirectory = void 0;
const tslib_1 = require("tslib");

@@ -62,2 +62,5 @@ /**

Object.defineProperty(exports, "registerTsProject", { enumerable: true, get: function () { return register_1.registerTsProject; } });
Object.defineProperty(exports, "loadTsFile", { enumerable: true, get: function () { return register_1.loadTsFile; } });
Object.defineProperty(exports, "forceRegisterEsmLoader", { enumerable: true, get: function () { return register_1.forceRegisterEsmLoader; } });
Object.defineProperty(exports, "requireWithTsconfigFallback", { enumerable: true, get: function () { return register_1.requireWithTsconfigFallback; } });
var utils_1 = require("./tasks-runner/utils");

@@ -64,0 +67,0 @@ Object.defineProperty(exports, "interpolate", { enumerable: true, get: function () { return utils_1.interpolate; } });

import type { TsConfigOptions } from 'ts-node';
import type { CompilerOptions } from 'typescript';
/**
* Force-register an ESM loader (`@swc-node/register/esm` if available, else
* `ts-node/esm`) via `Module.register` so dynamic `import()` of TS files
* goes through a transpiler.
*
* **IMPORTANT — global side effect:** `Module.register` is one-shot per
* process and applies to *every* subsequent ESM resolution in the process.
* Calling this trades native Node.js TypeScript stripping for transpiled
* loading on the dynamic-import path for the rest of the run. CJS
* `require()` is unaffected (different hook), so `.cts` files via require
* keep using native strip + swc-node's `Module._extensions` hook.
*
* Required for the niche case where an ESM config (`.mts` or `.ts` resolved
* as ESM) combines top-level await with TypeScript syntax that native strip
* can't handle (`enum`, runtime `namespace`, etc.). TLA forces dynamic
* `import()`, which bypasses the CJS hook chain - the only way to intercept
* is `Module.register`.
*
* Idempotent: subsequent calls are no-ops.
*
* Throws if neither `@swc-node/register` nor `ts-node` is installed.
*/
export declare function forceRegisterEsmLoader(): void;
/**
* Whether Nx will defer to Node's native TypeScript stripping for the next
* `.ts` load. Mirrors the gate used by `loadTsFile`/`registerTsProject` so
* other registration sites (e.g. plugin transpiler) can stay aligned.
*/
export declare function isNativeStripPreferred(): boolean;
/**
* Optionally, if swc-node and tsconfig-paths are available in the current workspace, apply the require

@@ -11,2 +40,13 @@ * register hooks so that .ts files can be used for writing custom workspace projects.

*
* Behavior change in v23: when the runtime exposes native TypeScript type
* stripping (`process.features.typescript`), this function skips the
* transpiler (Node handles `.ts` directly) but still registers
* `tsconfig-paths`. Path mapping is orthogonal to transpilation - callers
* relying on tsconfig `paths` for workspace alias resolution still get them.
* For loading a `.ts` file whose syntax native strip can't handle (`enum`,
* runtime `namespace`, legacy decorators, etc.), use `loadTsFile`, which
* registers swc/ts-node + tsconfig-paths on demand. To restore the legacy
* behavior (always register swc/ts-node + tsconfig-paths up front), set
* `NX_PREFER_NODE_STRIP_TYPES=false`.
*
* @returns cleanup function

@@ -31,2 +71,75 @@ */

/**
* Node.js throws this code when native type stripping hits an unsupported
* construct (enum, runtime namespace, legacy decorators, import = require,
* parameter properties on older Node, etc.).
*
* Exported for tests.
*/
export declare function isNativeTypeStripError(err: unknown): boolean;
/**
* A SyntaxError thrown while parsing a forced-CJS file (`.cts`/`.cjs`) as
* CommonJS - typically ESM syntax in a CJS file (e.g. `export default` in
* `.cts`). Pre-v23 this worked because swc-node's CJS hook compiled away the
* ESM syntax; under native strip swc-node isn't registered, so the file
* reaches Node's strict CJS parser. swc-node tolerates ESM syntax in `.cts`
* (`register()` forces `module: commonjs` regardless of extension), so
* escalating to the swc/ts-node fallback recovers the legacy behavior.
*/
export declare function isCjsSyntaxError(err: unknown, filePath: string): boolean;
/**
* A ReferenceError from Node treating a `.ts`/`.mts` file as ESM and the file
* relying on a CJS-only global: `require`, `__dirname`, or `__filename`.
* Pre-v23 swc-node compiled `.ts` to CJS where these globals exist; under
* native strip Node detects ESM via `import`/`export` syntax and these globals
* are undefined. Registering swc/ts-node compiles ESM->CJS and restores the
* legacy globals.
*/
export declare function isRequireInEsmScopeError(err: unknown, filePath: string): boolean;
export declare function isTsEsmSyntaxError(err: unknown, filePath: string): boolean;
export declare function isTsEsmNamedExportLinkageError(err: unknown, filePath: string): boolean;
/**
* Load a TypeScript file via `require()`.
*
* When the runtime exposes native TypeScript stripping
* (`process.features.typescript`) and the user hasn't opted out via
* `NX_PREFER_NODE_STRIP_TYPES=false`, the file loads directly with no
* swc/ts-node and no tsconfig-paths registration. If Node throws on an
* unsupported construct (enum, runtime namespace, legacy decorators, etc.),
* this registers swc/ts-node + tsconfig-paths and retries - matching the
* pre-v23 registration. Set `NX_DISABLE_TSCONFIG_PATHS=true` to skip
* tsconfig-paths even on fallback (useful when relying on package manager
* workspaces). Set `NX_VERBOSE_LOGGING=true` to log when fallback triggers.
*
* When native strip is opted out (`NX_PREFER_NODE_STRIP_TYPES=false` or
* unsupported Node), uses the legacy `registerTsProject` path.
*
* `tsConfigPath` is only consulted on the swc/ts-node fallback path (for
* compilerOptions) and for tsconfig-paths registration. Native strip ignores
* it. When omitted, defaults to the workspace root tsconfig.
*
* Note on ESM: Node 22.12+ supports `require()` of synchronous ESM by default,
* so most ESM `.ts` configs load via this function without issue. Modules
* that use top-level await throw `ERR_REQUIRE_ASYNC_MODULE` and must be
* loaded with dynamic `import()` instead. `ERR_REQUIRE_ESM` (legacy code)
* bubbles unchanged for the rare case it still fires, so async-aware callers
* can dispatch to `import()`.
*
* @returns the loaded module
*/
export declare function loadTsFile<T = any>(filePath: string, tsConfigPath?: string): T;
/**
* Plain `require()` with a lazy `tsconfig-paths` fallback. Use for files that
* are NOT TypeScript (no transpilation needed) but may still import workspace
* packages through TS path aliases (e.g. a `.js` changelog renderer that
* `require`s `@my-org/lib`).
*
* `tsconfig-paths` is only registered after the first `require()` fails with
* a module-resolution error, so workspaces that resolve aliases through
* package-manager symlinks pay nothing. Set `NX_DISABLE_TSCONFIG_PATHS=true`
* to skip the fallback entirely.
*
* @returns the loaded module
*/
export declare function requireWithTsconfigFallback<T = any>(filePath: string, tsConfigPath?: string): T;
/**
* Register ts-node or swc-node given a set of compiler options.

@@ -33,0 +146,0 @@ *

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.forceRegisterEsmLoader = forceRegisterEsmLoader;
exports.isNativeStripPreferred = isNativeStripPreferred;
exports.registerTsProject = registerTsProject;

@@ -7,2 +9,9 @@ exports.getSwcTranspiler = getSwcTranspiler;

exports.getTranspiler = getTranspiler;
exports.isNativeTypeStripError = isNativeTypeStripError;
exports.isCjsSyntaxError = isCjsSyntaxError;
exports.isRequireInEsmScopeError = isRequireInEsmScopeError;
exports.isTsEsmSyntaxError = isTsEsmSyntaxError;
exports.isTsEsmNamedExportLinkageError = isTsEsmNamedExportLinkageError;
exports.loadTsFile = loadTsFile;
exports.requireWithTsconfigFallback = requireWithTsconfigFallback;
exports.registerTranspiler = registerTranspiler;

@@ -21,2 +30,70 @@ exports.registerTsConfigPaths = registerTsConfigPaths;

/**
* Force-register an ESM loader (`@swc-node/register/esm` if available, else
* `ts-node/esm`) via `Module.register` so dynamic `import()` of TS files
* goes through a transpiler.
*
* **IMPORTANT — global side effect:** `Module.register` is one-shot per
* process and applies to *every* subsequent ESM resolution in the process.
* Calling this trades native Node.js TypeScript stripping for transpiled
* loading on the dynamic-import path for the rest of the run. CJS
* `require()` is unaffected (different hook), so `.cts` files via require
* keep using native strip + swc-node's `Module._extensions` hook.
*
* Required for the niche case where an ESM config (`.mts` or `.ts` resolved
* as ESM) combines top-level await with TypeScript syntax that native strip
* can't handle (`enum`, runtime `namespace`, etc.). TLA forces dynamic
* `import()`, which bypasses the CJS hook chain - the only way to intercept
* is `Module.register`.
*
* Idempotent: subsequent calls are no-ops.
*
* Throws if neither `@swc-node/register` nor `ts-node` is installed.
*/
function forceRegisterEsmLoader() {
ensureEsmLoaderRegistered({ required: true });
}
function ensureEsmLoaderRegistered(opts) {
if (isTsEsmLoaderRegistered)
return;
const module = require('node:module');
if (typeof module.register !== 'function') {
if (opts.required) {
throw new Error(`${logger_1.NX_PREFIX} Module.register is not available in this Node.js version - cannot register an ESM loader for the TypeScript fallback. ${STRIP_TYPES_OPT_OUT_HINT}`);
}
return;
}
// ts-node reads compilerOptions from this env var. Setting nodenext
// module/resolution avoids surprises when ts-node is the chosen loader.
process.env.TS_NODE_COMPILER_OPTIONS ??= JSON.stringify({
moduleResolution: 'nodenext',
module: 'nodenext',
});
// Prefer @swc-node/register/esm (faster) over ts-node/esm.
const swcEsm = tryResolveLoader('@swc-node/register/esm');
const tsNodeEsm = tryResolveLoader('ts-node/esm');
const loaderPath = swcEsm ?? tsNodeEsm;
if (!loaderPath) {
if (opts.required) {
throw new Error(`${logger_1.NX_PREFIX} Cannot register an ESM TypeScript loader to fall back from native stripping. Install @swc-node/register or ts-node, or ${STRIP_TYPES_OPT_OUT_HINT}`);
}
isTsEsmLoaderRegistered = true;
return;
}
if (process.env.NX_VERBOSE_LOGGING === 'true') {
const loaderName = swcEsm ? '@swc-node/register/esm' : 'ts-node/esm';
logger_1.logger.warn((0, logger_1.stripIndent)(`${logger_1.NX_PREFIX} Registering ESM TypeScript loader ${loaderName}. All subsequent ESM imports in this process will go through it - native Node.js TypeScript stripping is forfeited for the dynamic-import path.`));
}
const url = require('node:url');
module.register(url.pathToFileURL(loaderPath));
isTsEsmLoaderRegistered = true;
}
function tryResolveLoader(specifier) {
try {
return require.resolve(specifier);
}
catch {
return null;
}
}
/**
* tsx is a utility to run TypeScript files in node which is growing in popularity:

@@ -56,6 +133,10 @@ * https://tsx.is

/**
* Whether the current Node.js version supports native TypeScript execution
* via type stripping (Node 22.6+).
* Whether the current Node.js runtime exposes native TypeScript type
* stripping. This is the authoritative gate - it correctly handles every
* way Node ships TS support:
* - Node 23.6+: unflagged, on by default
* - Node 22.18+ LTS: backported unflagged
* - Node 22.6-22.17 + `--experimental-strip-types` (or `--experimental-transform-types`)
*
* process.features.typescript is 'strip' | 'transform' | false in Node 22.6+
* `process.features.typescript` is `'strip' | 'transform' | false`.
*/

@@ -65,17 +146,33 @@ const nodeSupportsNativeTypescript = !!process.features

/**
* When process.features.typescript is truthy and the user has opted in via
* NX_PREFER_NODE_STRIP_TYPES=true, we can skip registering swc-node or ts-node
* transpilers since Node.js will handle TypeScript natively.
* When process.features.typescript is truthy, default to letting Node.js
* handle TypeScript natively via type stripping and skip registering swc-node
* or ts-node. Users can opt out by setting NX_PREFER_NODE_STRIP_TYPES=false.
*
* This can significantly improve performance when loading TypeScript config files, but there are some things
* that won't work. See: https://nodejs.org/api/typescript.html#full-typescript-support
* Some constructs (enum, runtime namespace, legacy decorators,
* import = require, parameter properties, etc.) aren't supported by native
* type stripping. `loadTsFile` catches these failures and falls back to
* registering swc/ts-node + tsconfig-paths automatically.
*
* TODO(v23): We should turn this on by default, but look at if need to fallback to SWC/ts-node if it fails.
* See: https://nodejs.org/api/typescript.html#full-typescript-support
*/
const preferNodeStripTypes = (() => {
if (process.env.NX_PREFER_NODE_STRIP_TYPES !== 'true') {
if (!nodeSupportsNativeTypescript) {
return false;
}
return nodeSupportsNativeTypescript;
return process.env.NX_PREFER_NODE_STRIP_TYPES !== 'false';
})();
/**
* Skip tsconfig-paths registration on the swc/ts-node fallback path. Useful
* for workspaces relying on package manager workspaces (pnpm, yarn, npm) for
* project linking, where tsconfig path aliases aren't needed.
*/
const disableTsConfigPaths = process.env.NX_DISABLE_TSCONFIG_PATHS === 'true';
/**
* Whether Nx will defer to Node's native TypeScript stripping for the next
* `.ts` load. Mirrors the gate used by `loadTsFile`/`registerTsProject` so
* other registration sites (e.g. plugin transpiler) can stay aligned.
*/
function isNativeStripPreferred() {
return preferNodeStripTypes;
}
function registerTsProject(path, configFilename) {

@@ -87,8 +184,19 @@ // See explanation alongside isInvokedByTsx declaration

const tsConfigPath = configFilename ? (0, path_1.join)(path, configFilename) : path;
// See explanation alongside preferNodeStripTypes declaration
// When using Node.js native type stripping, skip transpiler registration
// but still register tsconfig-paths for path mapping support
// Under native strip we skip the transpiler (Node handles `.ts` directly)
// but still register tsconfig-paths. Path mapping is orthogonal to
// transpilation: code calling `registerTsProject` for path aliases
// (e.g. test setup files requiring `@my-org/lib`) gets nothing back if
// both are skipped. Package-manager-workspace symlinks aren't a
// universal substitute - explicit tsconfig `paths` configs still need
// runtime alias resolution. Callers needing the transpiler for
// unsupported syntax (enum, runtime namespace, legacy decorators, etc.)
// should use `loadTsFile` instead, which registers swc/ts-node +
// tsconfig-paths on demand.
if (preferNodeStripTypes) {
return registerTsConfigPaths(tsConfigPath);
}
// Legacy path: prior to v23, Nx always registered swc-node/ts-node and
// tsconfig-paths to load .ts config files. v23+ prefers Node's built-in
// type stripping; this branch only runs when the user opted out via
// NX_PREFER_NODE_STRIP_TYPES=false or when strip is unavailable.
const { compilerOptions, tsConfigRaw } = readCompilerOptions(tsConfigPath);

@@ -99,21 +207,6 @@ const cleanupFunctions = [

];
// 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) {
// We need a way to ensure that `.ts` files are treated as ESM not CJS.
// Since there is no way to pass compilerOptions like we do with the programmatic API, we should default
// the environment variable that ts-node checks.
process.env.TS_NODE_COMPILER_OPTIONS ??= JSON.stringify({
moduleResolution: 'nodenext',
module: 'nodenext',
});
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;
}
// Best-effort ESM loader registration so dynamic import() of .ts/.mts
// files goes through a transpiler. No-op if no ESM loader package is
// installed.
ensureEsmLoaderRegistered({ required: false });
return () => {

@@ -263,2 +356,303 @@ for (const fn of cleanupFunctions) {

/**
* Node.js throws this code when native type stripping hits an unsupported
* construct (enum, runtime namespace, legacy decorators, import = require,
* parameter properties on older Node, etc.).
*
* Exported for tests.
*/
function isNativeTypeStripError(err) {
if (!err || typeof err !== 'object')
return false;
return (err.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX');
}
/**
* Module resolution failures - typically a tsconfig path alias that hasn't
* been registered yet, or a workspace lib not surfaced via package manager
* symlinks. CJS uses `MODULE_NOT_FOUND`, ESM uses `ERR_MODULE_NOT_FOUND`.
*/
function isModuleNotFoundError(err) {
if (!err || typeof err !== 'object')
return false;
const code = err.code;
return code === 'MODULE_NOT_FOUND' || code === 'ERR_MODULE_NOT_FOUND';
}
/**
* A SyntaxError thrown while parsing a forced-CJS file (`.cts`/`.cjs`) as
* CommonJS - typically ESM syntax in a CJS file (e.g. `export default` in
* `.cts`). Pre-v23 this worked because swc-node's CJS hook compiled away the
* ESM syntax; under native strip swc-node isn't registered, so the file
* reaches Node's strict CJS parser. swc-node tolerates ESM syntax in `.cts`
* (`register()` forces `module: commonjs` regardless of extension), so
* escalating to the swc/ts-node fallback recovers the legacy behavior.
*/
function isCjsSyntaxError(err, filePath) {
if (!(err instanceof SyntaxError))
return false;
return filePath.endsWith('.cts') || filePath.endsWith('.cjs');
}
/**
* A ReferenceError from Node treating a `.ts`/`.mts` file as ESM and the file
* relying on a CJS-only global: `require`, `__dirname`, or `__filename`.
* Pre-v23 swc-node compiled `.ts` to CJS where these globals exist; under
* native strip Node detects ESM via `import`/`export` syntax and these globals
* are undefined. Registering swc/ts-node compiles ESM->CJS and restores the
* legacy globals.
*/
function isRequireInEsmScopeError(err, filePath) {
if (!(err instanceof ReferenceError))
return false;
if (!(filePath.endsWith('.ts') || filePath.endsWith('.mts')))
return false;
// Node's exact phrasing varies across versions / strip modes. Match the
// bare-name form too (e.g. `__dirname is not defined`) so the fallback
// still triggers when the trailing "in ES module scope" is absent.
const msg = err.message;
return /(require|__dirname|__filename) is not defined/.test(msg);
}
function isTsEsmSyntaxError(err, filePath) {
if (!(err instanceof SyntaxError))
return false;
if (!filePath.endsWith('.ts'))
return false;
// Node has multiple phrasings for ESM-in-CJS-scope syntax errors depending
// on whether the offending token is `import` or `export` and which parser
// path triggered: "Cannot use import statement outside a module" or
// "Unexpected token 'export'" / "Unexpected token 'import'". swc-node's
// CJS hook compiles ESM->CJS regardless of the surface error, so all of
// these should escalate to the same fallback.
const msg = err.message;
return (msg.includes('Cannot use import statement outside a module') ||
/Unexpected token ['"](export|import)['"]/.test(msg));
}
function isTsEsmNamedExportLinkageError(err, filePath) {
if (!(err instanceof SyntaxError))
return false;
return ((filePath.endsWith('.ts') || filePath.endsWith('.mts')) &&
err.message.includes('does not provide an export named'));
}
/**
* Hint appended to errors that the lazy fallback couldn't recover from.
* Points users at the env opt-out for cases native strip can't reach (e.g.
* ESM with top-level await + unsupported TS syntax, where swc-node's CJS
* Module._extensions hook can't intercept dynamic `import()`).
*/
const NX_PREFER_NODE_STRIP_TYPES_DOCS_URL = 'https://nx.dev/docs/reference/environment-variables#nx-prefer-node-strip-types';
const STRIP_TYPES_OPT_OUT_HINT = `Set NX_PREFER_NODE_STRIP_TYPES=false to opt out of Node's native TypeScript stripping and use swc/ts-node instead. See ${NX_PREFER_NODE_STRIP_TYPES_DOCS_URL}`;
/**
* Load a TypeScript file via `require()`.
*
* When the runtime exposes native TypeScript stripping
* (`process.features.typescript`) and the user hasn't opted out via
* `NX_PREFER_NODE_STRIP_TYPES=false`, the file loads directly with no
* swc/ts-node and no tsconfig-paths registration. If Node throws on an
* unsupported construct (enum, runtime namespace, legacy decorators, etc.),
* this registers swc/ts-node + tsconfig-paths and retries - matching the
* pre-v23 registration. Set `NX_DISABLE_TSCONFIG_PATHS=true` to skip
* tsconfig-paths even on fallback (useful when relying on package manager
* workspaces). Set `NX_VERBOSE_LOGGING=true` to log when fallback triggers.
*
* When native strip is opted out (`NX_PREFER_NODE_STRIP_TYPES=false` or
* unsupported Node), uses the legacy `registerTsProject` path.
*
* `tsConfigPath` is only consulted on the swc/ts-node fallback path (for
* compilerOptions) and for tsconfig-paths registration. Native strip ignores
* it. When omitted, defaults to the workspace root tsconfig.
*
* Note on ESM: Node 22.12+ supports `require()` of synchronous ESM by default,
* so most ESM `.ts` configs load via this function without issue. Modules
* that use top-level await throw `ERR_REQUIRE_ASYNC_MODULE` and must be
* loaded with dynamic `import()` instead. `ERR_REQUIRE_ESM` (legacy code)
* bubbles unchanged for the rare case it still fires, so async-aware callers
* can dispatch to `import()`.
*
* @returns the loaded module
*/
function loadTsFile(filePath, tsConfigPath) {
const resolvedTsConfigPath = tsConfigPath ?? (0, typescript_1.getRootTsConfigPath)();
if (isInvokedByTsx) {
return require(filePath);
}
if (!preferNodeStripTypes) {
if (!resolvedTsConfigPath) {
throw new Error(`${logger_1.NX_PREFIX} loadTsFile could not find a workspace tsconfig while loading ${filePath} on the swc/ts-node path. Pass an explicit tsConfigPath or add a tsconfig.base.json/tsconfig.json at the workspace root.`);
}
const cleanup = registerTsProject(resolvedTsConfigPath);
try {
return require(filePath);
}
finally {
cleanup();
}
}
// Native strip path: no registration up front. pnpm/npm/yarn workspaces
// resolve aliases without tsconfig-paths. On failure, lazy-register what
// the specific error code indicates is needed and retry:
// - MODULE_NOT_FOUND -> first try tsconfig-paths (alias resolution).
// If that still fails (e.g. extensionless `import './foo'` when
// `foo.ts` is adjacent - Node's resolver doesn't add `.ts`), escalate
// to swc/ts-node which handles `.ts` extension resolution.
// - ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX -> register swc/ts-node + tsconfig-paths
// Each registration kind runs at most once, so a file recovers in at
// most three attempts.
let pathsRegistered = false;
let transpilerRegistered = false;
const cleanups = [];
const registerTranspilerFallback = (err) => {
if (!swcNodeInstalled && !tsNodeInstalled) {
const original = err instanceof Error ? err.message : String(err);
throw new Error(`${logger_1.NX_PREFIX} ${filePath} could not be loaded under Node's native TypeScript stripping (${original}). Install @swc-node/register and @swc/core (or ts-node) to enable the swc/ts-node fallback, or ${STRIP_TYPES_OPT_OUT_HINT}`);
}
if (!resolvedTsConfigPath) {
throw new Error(`${logger_1.NX_PREFIX} ${filePath} requires the swc/ts-node fallback but no workspace tsconfig was found. Pass an explicit tsConfigPath or add a tsconfig.base.json/tsconfig.json at the workspace root. ${STRIP_TYPES_OPT_OUT_HINT}`);
}
if (!pathsRegistered && !disableTsConfigPaths) {
cleanups.push(registerTsConfigPaths(resolvedTsConfigPath));
pathsRegistered = true;
}
const { compilerOptions, tsConfigRaw } = readCompilerOptions(resolvedTsConfigPath);
cleanups.push(registerTranspiler(compilerOptions, tsConfigRaw));
transpilerRegistered = true;
};
try {
for (let attempt = 1;; attempt++) {
try {
if (attempt > 1) {
try {
delete require.cache[require.resolve(filePath)];
}
catch {
// require.resolve may throw if the failed load never reached cache
}
}
return require(filePath);
}
catch (err) {
// Cheap fallback first: register tsconfig-paths and retry.
if (isModuleNotFoundError(err) &&
!pathsRegistered &&
!disableTsConfigPaths &&
resolvedTsConfigPath) {
logFallback(filePath, err, 'Module not found; registering tsconfig-paths and retrying.');
cleanups.push(registerTsConfigPaths(resolvedTsConfigPath));
pathsRegistered = true;
continue;
}
// Heavy fallback: register swc/ts-node (+ paths) and retry. Triggered
// by:
// - strip-types failure (`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX`)
// - module resolution failure that tsconfig-paths alone can't fix
// (extensionless `./foo` -> `./foo.ts`)
// - SyntaxError in a `.cts`/`.cjs` file (ESM syntax in a forced-CJS
// file). swc-node compiles ESM->CJS regardless of extension.
// - SyntaxError from Node parsing a `.ts` config with ESM syntax as
// CJS. swc/ts-node preserves the pre-v23 behavior for these files.
// - ReferenceError from Node treating a `.ts`/`.mts` config as ESM
// when it contains legacy CJS `require`.
// - SyntaxError from native ESM linkage when generated TS config
// imports a type-only symbol as a runtime named export.
if ((isNativeTypeStripError(err) ||
isModuleNotFoundError(err) ||
isCjsSyntaxError(err, filePath) ||
isTsEsmSyntaxError(err, filePath) ||
isRequireInEsmScopeError(err, filePath) ||
isTsEsmNamedExportLinkageError(err, filePath)) &&
!transpilerRegistered) {
logFallback(filePath, err, isNativeTypeStripError(err)
? 'Native Node.js TypeScript stripping failed; falling back to swc/ts-node + tsconfig-paths.'
: isCjsSyntaxError(err, filePath)
? 'ESM syntax in forced-CJS file; falling back to swc/ts-node + tsconfig-paths.'
: isTsEsmSyntaxError(err, filePath)
? 'ESM syntax in TypeScript file parsed as CommonJS; falling back to swc/ts-node + tsconfig-paths.'
: isRequireInEsmScopeError(err, filePath)
? 'CommonJS require in native ESM TypeScript file; falling back to swc/ts-node + tsconfig-paths.'
: isTsEsmNamedExportLinkageError(err, filePath)
? 'Native ESM named export linkage failed; falling back to swc/ts-node + tsconfig-paths.'
: 'Module not found after tsconfig-paths; falling back to swc/ts-node + tsconfig-paths.');
registerTranspilerFallback(err);
continue;
}
throw augmentLoadFailure(filePath, err);
}
}
}
finally {
for (const fn of cleanups)
fn();
}
}
/**
* Plain `require()` with a lazy `tsconfig-paths` fallback. Use for files that
* are NOT TypeScript (no transpilation needed) but may still import workspace
* packages through TS path aliases (e.g. a `.js` changelog renderer that
* `require`s `@my-org/lib`).
*
* `tsconfig-paths` is only registered after the first `require()` fails with
* a module-resolution error, so workspaces that resolve aliases through
* package-manager symlinks pay nothing. Set `NX_DISABLE_TSCONFIG_PATHS=true`
* to skip the fallback entirely.
*
* @returns the loaded module
*/
function requireWithTsconfigFallback(filePath, tsConfigPath) {
try {
return require(filePath);
}
catch (err) {
if (!isModuleNotFoundError(err) || disableTsConfigPaths) {
throw err;
}
const resolvedTsConfigPath = tsConfigPath ?? (0, typescript_1.getRootTsConfigPath)();
if (!resolvedTsConfigPath) {
throw err;
}
const cleanup = registerTsConfigPaths(resolvedTsConfigPath);
try {
delete require.cache[require.resolve(filePath)];
}
catch {
// require.resolve may throw if the failed load never reached cache
}
try {
return require(filePath);
}
finally {
cleanup();
}
}
}
/**
* Append the `NX_PREFER_NODE_STRIP_TYPES=false` opt-out hint so users know
* there's an escape hatch for cases native strip can't reach (e.g. ESM with
* top-level await + unsupported TS syntax). Skipped for:
* - ESM-redispatch signals callers expect to handle
* (`ERR_REQUIRE_ESM`, `ERR_REQUIRE_ASYNC_MODULE`)
* - plain module-resolution failures (`MODULE_NOT_FOUND`,
* `ERR_MODULE_NOT_FOUND`) - disabling strip-types doesn't fix a missing
* module, the hint just misleads.
*/
function augmentLoadFailure(filePath, err) {
if (!(err instanceof Error))
return err;
const code = err.code;
if (code === 'ERR_REQUIRE_ESM' ||
code === 'ERR_REQUIRE_ASYNC_MODULE' ||
code === 'MODULE_NOT_FOUND' ||
code === 'ERR_MODULE_NOT_FOUND') {
return err;
}
if (err.message.includes(NX_PREFER_NODE_STRIP_TYPES_DOCS_URL)) {
return err;
}
err.message = `${err.message}\n\n${logger_1.NX_PREFIX} Failed to load ${filePath} under Node's native TypeScript stripping. ${STRIP_TYPES_OPT_OUT_HINT}`;
return err;
}
function logFallback(filePath, err, summary) {
if (process.env.NX_VERBOSE_LOGGING !== 'true') {
return;
}
const message = err instanceof Error ? err.message : String(err);
logger_1.logger.warn((0, logger_1.stripIndent)(`${logger_1.NX_PREFIX} ${summary} (${filePath})
${message}`));
}
/**
* Register ts-node or swc-node given a set of compiler options.

@@ -276,3 +670,3 @@ *

// If Node.js natively supports TypeScript (22.6+), no transpiler is needed.
// Don't warn — Node will handle .ts files via type stripping.
// Don't warn - Node will handle .ts files via type stripping.
if (!nodeSupportsNativeTypescript) {

@@ -301,2 +695,12 @@ warnNoTranspiler();

if (tsConfigResult.resultType === 'success') {
// Short-circuit when the tsconfig has no `paths` entries. Installing
// tsconfig-paths' resolver hook adds a per-require cost on every
// module load; in package-manager-workspace setups (which resolve via
// symlinks instead of TS path mappings), the hook never has anything
// to do. Avoid paying that overhead on workspaces that don't use
// `paths`.
if (!tsConfigResult.paths ||
Object.keys(tsConfigResult.paths).length === 0) {
return () => { };
}
return tsconfigPaths.register({

@@ -303,0 +707,0 @@ baseUrl: resolvePathsBaseUrl(tsConfigPath),

@@ -5,5 +5,17 @@ export declare let unregisterPluginTSTranspiler: (() => void) | null;

* with some default settings which work well for Nx plugins.
*
* When the runtime supports native TypeScript stripping and the user hasn't
* opted out, this is a noop - Node loads plugin `.ts` files directly. The
* lazy fallback in `handleImport` will call `forceRegisterPluginTSTranspiler`
* if a plugin uses unsupported syntax (enum, runtime namespace, etc.).
*/
export declare function registerPluginTSTranspiler(): void;
/**
* Always register swc-node or ts-node + tsconfig-paths, ignoring the native
* strip preference. Called from the fallback path in `handleImport` when a
* plugin `.ts` file throws `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` under native
* stripping.
*/
export declare function forceRegisterPluginTSTranspiler(): void;
export declare function pluginTranspilerIsRegistered(): boolean;
export declare function cleanupPluginTSTranspiler(): void;

@@ -5,2 +5,3 @@ "use strict";

exports.registerPluginTSTranspiler = registerPluginTSTranspiler;
exports.forceRegisterPluginTSTranspiler = forceRegisterPluginTSTranspiler;
exports.pluginTranspilerIsRegistered = pluginTranspilerIsRegistered;

@@ -14,7 +15,43 @@ exports.cleanupPluginTSTranspiler = cleanupPluginTSTranspiler;

exports.unregisterPluginTSTranspiler = null;
const NOOP = () => { };
/**
* Register swc-node or ts-node if they are not currently registered
* with some default settings which work well for Nx plugins.
*
* When the runtime supports native TypeScript stripping and the user hasn't
* opted out, this is a noop - Node loads plugin `.ts` files directly. The
* lazy fallback in `handleImport` will call `forceRegisterPluginTSTranspiler`
* if a plugin uses unsupported syntax (enum, runtime namespace, etc.).
*/
function registerPluginTSTranspiler() {
if (exports.unregisterPluginTSTranspiler !== null) {
return;
}
if ((0, register_1.isNativeStripPreferred)()) {
// Sentinel so pluginTranspilerIsRegistered() reports true and callers
// don't keep retrying. The actual transpiler stays unregistered until
// a fallback forces it.
exports.unregisterPluginTSTranspiler = NOOP;
return;
}
doRegisterPluginTSTranspiler();
}
/**
* Always register swc-node or ts-node + tsconfig-paths, ignoring the native
* strip preference. Called from the fallback path in `handleImport` when a
* plugin `.ts` file throws `ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX` under native
* stripping.
*/
function forceRegisterPluginTSTranspiler() {
// If the previous registration was the native-strip sentinel, throw it
// out so we actually wire up swc/ts-node.
if (exports.unregisterPluginTSTranspiler === NOOP) {
exports.unregisterPluginTSTranspiler = null;
}
if (exports.unregisterPluginTSTranspiler !== null) {
return;
}
doRegisterPluginTSTranspiler();
}
function doRegisterPluginTSTranspiler() {
// Get the first tsconfig that matches the allowed set

@@ -21,0 +58,0 @@ const tsConfigName = [

@@ -6,3 +6,6 @@ /**

* Falls back to real import() for ESM-only packages that
* throw ERR_REQUIRE_ESM.
* throw ERR_REQUIRE_ESM. When loading a workspace plugin's `.ts` entry
* under native TypeScript stripping, also falls back to swc/ts-node if
* the file uses constructs Node can't strip (enum, runtime namespace,
* legacy decorators, etc.).
*

@@ -9,0 +12,0 @@ * @param modulePath - The module specifier (relative, absolute, or package name)

@@ -5,2 +5,3 @@ "use strict";

const path_1 = require("path");
const STRIP_TYPES_DOCS_URL = 'https://nx.dev/docs/reference/environment-variables#nx-prefer-node-strip-types';
/**

@@ -11,3 +12,6 @@ * Dynamically imports a module using CJS require().

* Falls back to real import() for ESM-only packages that
* throw ERR_REQUIRE_ESM.
* throw ERR_REQUIRE_ESM. When loading a workspace plugin's `.ts` entry
* under native TypeScript stripping, also falls back to swc/ts-node if
* the file uses constructs Node can't strip (enum, runtime namespace,
* legacy decorators, etc.).
*

@@ -29,7 +33,57 @@ * @param modulePath - The module specifier (relative, absolute, or package name)

catch (e) {
if (e.code === 'ERR_REQUIRE_ESM') {
// ERR_REQUIRE_ESM (legacy) and ERR_REQUIRE_ASYNC_MODULE (Node 22.12+,
// ESM with top-level await) both indicate the module must be loaded via
// dynamic import(). Native strip handles `.ts` extension resolution on
// the import() path, so this recovers TLA plugin entry points without
// requiring swc-node or ts-node to be installed.
if (e.code === 'ERR_REQUIRE_ESM' || e.code === 'ERR_REQUIRE_ASYNC_MODULE') {
return import(resolvedPath);
}
// Mirror `loadTsFile`'s fallback set (register.ts). Plugin loads hit a
// wider failure surface than .ts config loads because plugin sources are
// often ESM and import type-only symbols from `@nx/devkit` as runtime
// named exports. Lazy-require the matchers to keep register/transpiler
// (and their daemon/logger deps) out of module-eval-time graphs.
const matchers = require('../plugins/js/utils/register');
if (e?.code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' ||
e?.code === 'MODULE_NOT_FOUND' ||
e?.code === 'ERR_MODULE_NOT_FOUND' ||
matchers.isTsEsmSyntaxError(e, resolvedPath) ||
matchers.isTsEsmNamedExportLinkageError(e, resolvedPath) ||
matchers.isCjsSyntaxError(e, resolvedPath) ||
matchers.isRequireInEsmScopeError(e, resolvedPath)) {
// Lazy-require to avoid pulling register/transpiler (and their
// daemon/logger transitive deps) into module-eval-time graphs.
const { forceRegisterPluginTSTranspiler } = require('../project-graph/plugins/transpiler');
forceRegisterPluginTSTranspiler();
try {
delete require.cache[require.resolve(normalizedPath)];
}
catch {
// require.resolve may throw if the failed load never reached cache
}
try {
return require(normalizedPath);
}
catch (retryErr) {
// Mirror the initial-catch behavior: if the compiled output surfaces
// TLA or ESM-only-as-CJS, dispatch to dynamic import() instead of
// re-throwing. Without this, a plugin whose source needed the
// transpiler fallback AND emits top-level await fails here instead of
// loading.
if (retryErr?.code === 'ERR_REQUIRE_ESM' ||
retryErr?.code === 'ERR_REQUIRE_ASYNC_MODULE') {
return import(resolvedPath);
}
if (retryErr instanceof Error) {
// Lazy-require NX_PREFIX so we don't pull logger -> daemon into
// module-eval-time graphs.
const { NX_PREFIX } = require('./logger');
retryErr.message = `${retryErr.message}\n\n${NX_PREFIX} Failed to load ${normalizedPath} under Node's native TypeScript stripping. Set NX_PREFER_NODE_STRIP_TYPES=false to opt out and use swc/ts-node instead. See ${STRIP_TYPES_DOCS_URL}`;
}
throw retryErr;
}
}
throw e;
}
}

@@ -165,3 +165,3 @@ {

"23-0-0-consolidate-release-tag-config": {
"version": "23.0.0-beta.0",
"version": "23.0.0-beta.16",
"description": "Consolidates any remaining legacy releaseTag* flat properties into the nested releaseTag object. The flat properties were removed in Nx 23.",

@@ -168,0 +168,0 @@ "implementation": "./dist/src/migrations/update-23-0-0/consolidate-release-tag-config"

{
"name": "nx",
"version": "23.0.0-beta.15",
"version": "23.0.0-beta.16",
"private": false,

@@ -173,12 +173,12 @@ "type": "commonjs",

"optionalDependencies": {
"@nx/nx-darwin-arm64": "23.0.0-beta.15",
"@nx/nx-darwin-x64": "23.0.0-beta.15",
"@nx/nx-freebsd-x64": "23.0.0-beta.15",
"@nx/nx-linux-arm-gnueabihf": "23.0.0-beta.15",
"@nx/nx-linux-arm64-gnu": "23.0.0-beta.15",
"@nx/nx-linux-arm64-musl": "23.0.0-beta.15",
"@nx/nx-linux-x64-gnu": "23.0.0-beta.15",
"@nx/nx-linux-x64-musl": "23.0.0-beta.15",
"@nx/nx-win32-arm64-msvc": "23.0.0-beta.15",
"@nx/nx-win32-x64-msvc": "23.0.0-beta.15"
"@nx/nx-darwin-arm64": "23.0.0-beta.16",
"@nx/nx-darwin-x64": "23.0.0-beta.16",
"@nx/nx-freebsd-x64": "23.0.0-beta.16",
"@nx/nx-linux-arm-gnueabihf": "23.0.0-beta.16",
"@nx/nx-linux-arm64-gnu": "23.0.0-beta.16",
"@nx/nx-linux-arm64-musl": "23.0.0-beta.16",
"@nx/nx-linux-x64-gnu": "23.0.0-beta.16",
"@nx/nx-linux-x64-musl": "23.0.0-beta.16",
"@nx/nx-win32-arm64-msvc": "23.0.0-beta.16",
"@nx/nx-win32-x64-msvc": "23.0.0-beta.16"
},

@@ -185,0 +185,0 @@ "nx-migrations": {

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet