Conversation
✅ Deploy Preview for nx-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
✅ Deploy Preview for nx-dev ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
View your CI Pipeline Execution ↗ for commit a47532e
☁️ Nx Cloud last updated this comment at |
## Current Behavior Compat matrix lists Node 24, 22, 20 for Nx 22.x. No mention of Node 26. ## Expected Behavior Add Node 26 to Nx 22.x row. New aside notes Node 26 is on Current track, hits LTS Oct 2026, supported today via CI. ## Notes - There are new deprecation warnings for `module.register` usage, which will be removed in Node 28. This is fine for workspace that can use the default Node.js type-stripping (#35608), and for other workspaces we need swc/ts-node (or something else) to move away from the deprecated API before it goes away. - Also a deprecation warning for `fs.Stats`, but it's not in our code so it's from a dep or transitive dep. ## Related Issue(s) NXC-4374
5abe99f to
d665fa4
Compare
There was a problem hiding this comment.
Important
At least one additional CI pipeline execution has run since the conclusion below was written and it may no longer be applicable.
Nx Cloud is proposing a fix for your failed CI:
We fixed two failures introduced by the native TypeScript stripping PR: the playwright config template was generating __filename for non-TS-solution workspaces, which is a CJS-only global unavailable in ESM scope and breaks under the new default native strip mode — this is corrected by unconditionally using import.meta.dirname. We also ran Prettier on environment-variables.mdoc to fix the table alignment issues introduced when the new env-var rows were added without formatting.
Warning
- ❌ We could not verify this fix.
- The suggested diff is too large to display here, but you can view it on Nx Cloud ↗
Or Apply changes locally with:
npx nx-cloud apply-locally vzNm-hywI
Apply fix locally with your editor ↗ View interactive diff ↗
🎓 Learn more about Self-Healing CI on nx.dev
## Current Behavior Loading TS config files needs swc-node/ts-node. Native strip available since Node 22.6+ but gated behind opt-in NX_PREFER_NODE_STRIP_TYPES=true. ## Expected Behavior Native strip on by default on Node 22.6+. Opt out via NX_PREFER_NODE_STRIP_TYPES=false. New loadTsFile helper catches ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX (enum, runtime namespace, legacy decorators, etc.) and falls back to swc/ts-node transparently. Set NX_VERBOSE_LOGGING=true to log fallback. ## Related Issue(s) Fixes NXC-4299
## Current Behavior loadTsFile registered tsconfig-paths up front before native strip even ran. Several callers (webpack, eslint-plugin, angular, module-federation) still used registerTsProject + require with no fallback, so flipping the default broke configs with enums or namespaces. devkit imported loadTsFile unconditionally, breaking against nx@22 peers. New workspaces always installed @swc-node/register + @swc/core even though native strip is now the default loader. ## Expected Behavior loadTsFile tries require with no registration when native strip is preferred. On ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, registers swc/ts-node + tsconfig-paths and retries. NX_DISABLE_TSCONFIG_PATHS=true skips paths even on fallback. registerTsProject is now a noop under native strip (no paths). Cited callers migrated to loadTsFile. devkit guards loadTsFile typeof for cross-version peers. @nx/js init drops @swc-node/register + @swc/core; @swc/helpers stays. Fallback throws a clear error when neither swc nor ts-node is installed. e2e fallback assertion runs without daemon and merges stderr. ## Related Issue(s) Fixes NXC-4299
## Current Behavior nx-plugin-checks pre-warmed registerTsProject globally even though it became a noop under native strip. resolve-workspace-rules wrapped loadConfigFile with a redundant outer registerTsProject. version-actions and nx-plugin-checks passed an unguarded getRootTsConfigPath() into loadTsFile, which would only blow up on the fallback path when null. registerTsProject's behavior change under native strip wasn't documented. ## Expected Behavior Drop dead pre-warm in nx-plugin-checks - require sites already use loadTsFile. resolve-workspace-rules drops the outer wrapper and switches to loadTsFile when an explicit tsConfigPath is provided so the fallback context threads through. Callers that read getRootTsConfigPath() now guard for null and fall back to plain require. loadTsFile fails fast with a clear error if tsConfigPath is missing. registerTsProject JSDoc spells out the v23 behavior change and points migrators at loadTsFile. ## Related Issue(s) Fixes NXC-4299
…swc/helpers from init ## Current Behavior JSDoc claimed native strip is the default on Node 22.6+, but the unflagged default landed in Node 23 - 22.6-22.x still requires --experimental-strip-types. registerTsProject computed tsConfigPath even on the noop strip path. The legacy swc/ts-node branch had no comment marking it as legacy. loadTsFile required tsConfigPath as a positional arg, so callers had to thread getRootTsConfigPath() guards through. e2e tests passed NX_PREFER_NODE_STRIP_TYPES=true even though it's now the default. init still installed @swc/helpers. ## Expected Behavior JSDoc says Node 23+ default, 22.6-22.x with the experimental flag. registerTsProject defers tsConfigPath read until the legacy branch, and that branch is labeled as legacy. loadTsFile accepts an optional tsConfigPath and falls back to the workspace root tsconfig (only consulted on the swc/ts-node fallback path), with clear errors when neither is available. Callers drop their explicit getRootTsConfigPath calls. e2e drops the redundant env var (and the describe label is updated). init no longer installs @swc/helpers - SWC-bundler generators install it on demand. JSDoc also notes Node 22.12+ require(esm) support so ERR_REQUIRE_ESM rarely fires. ## Related Issue(s) Fixes NXC-4299
…escript ## Current Behavior Comments and JSDoc said "Node 22.6+" or "Node 23+ (22.6-22.x with --experimental-strip-types)" as the gating condition. This was wrong on two counts: Node 22.18+ LTS unflagged it too, and the actual runtime gate is process.features.typescript - the version cutoff is incidental. ## Expected Behavior Comments and JSDoc lead with process.features.typescript as the authoritative check, then note version coverage (Node 23.6+ unflagged, 22.18+ LTS unflagged, 22.6-22.17 with the experimental flag) as informational. Env-var reference matches. ## Related Issue(s) Fixes NXC-4299
## Current Behavior registerPluginTSTranspiler always registered swc-node or ts-node + tsconfig-paths up front, regardless of process.features.typescript or NX_PREFER_NODE_STRIP_TYPES. With swc-node no longer in default deps, nx report (and any other path that pre-warms the plugin transpiler) fell through to ts-node and emitted "Falling back to ts-node for local typescript execution" - even when native strip would have handled the plugin .ts file with no transpiler at all. ## Expected Behavior registerPluginTSTranspiler is a noop when isNativeStripPreferred() returns true. A sentinel keeps pluginTranspilerIsRegistered() honest so callers don't keep retrying. handleImport catches ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX from a plugin require and calls forceRegisterPluginTSTranspiler to wire up swc/ts-node + tsconfig-paths on demand, then retries. ## Related Issue(s) Fixes NXC-4299
Co-authored-by: jaysoo <jaysoo@users.noreply.github.com>
…e snapshot CI failures on the PR after the plugin transpiler change: - nx:test: workspace-context.spec.ts failed because handle-import.ts now eagerly imports register.ts which transitively pulls daemon/tmp-dir.ts. The spec only mocks workspaceDataDirectoryForWorkspace, so module-eval-time access to workspaceDataDirectory threw. - astro-docs:format: NX_PREFER_NODE_STRIP_TYPES description was long enough that the markdown table no longer matched prettier's printWidth. - vue:test: library.spec.ts snapshot still listed @swc-node/register, @swc/core, @swc/helpers in package.json devDependencies, but @nx/js init no longer adds them. handle-import.ts only requires the transpiler module from inside the ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX catch branch, so the daemon/logger chain stays out of the eager graph. Env-var doc reformatted by prettier. Vue lib snapshot drops the three SWC entries. Fixes NXC-4299
…der native strip ## Current Behavior loadTsFile and handleImport only fell back to swc/ts-node + tsconfig-paths on ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX. When a TypeScript config (e.g. apps/web/jest.config.cts) imported a workspace lib via a tsconfig path alias that wasn't surfaced through pnpm symlinks, native strip succeeded but Node's CJS resolver threw MODULE_NOT_FOUND, which propagated unchanged. nx report on a fresh init failed with "Cannot find module '@acme/test-utils'". ## Expected Behavior loadTsFile retries lazily: MODULE_NOT_FOUND registers tsconfig-paths only and retries; ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX registers swc/ts-node + tsconfig-paths and retries. Each registration is attempted at most once, so a file that hits both errors recovers in at most three attempts. handleImport applies the same fallback for plugin loads. NX_DISABLE_TSCONFIG_PATHS=true still skips the paths registration. NX_VERBOSE_LOGGING=true logs both flavors. ## Related Issue(s) Fixes NXC-4299
…onfig-paths can't fix it ## Current Behavior Native strip handles the .ts file fine, but Node's CJS/ESM resolver doesn't add the .ts extension when a config imports `from './foo'` and the adjacent file is `foo.ts`. That throws ERR_MODULE_NOT_FOUND. loadTsFile registered tsconfig-paths and retried, which doesn't help (paths are about aliases, not extensions), so the second attempt threw with no further escalation. Pre-PR, registerTsProject() registered swc-node up front and swc-node's resolver handled the .ts extension, so this loaded. ## Expected Behavior loadTsFile escalates: first MODULE_NOT_FOUND tries tsconfig-paths only; if that retry still fails (with MODULE_NOT_FOUND or strip error), it registers swc/ts-node + tsconfig-paths and retries again. Each registration kind runs at most once. e2e covers the extensionless relative import case. ## Related Issue(s) Fixes NXC-4299
… on unrecoverable strip failures ## Current Behavior loadTypeScriptModule had a dedicated .mts branch that called registerTsProject (now a noop under native strip) then dispatched straight to loadESM (dynamic import()). swc-node only hooks Module._extensions (CJS), so an .mts config with an enum threw ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX from Node's loader with no fallback. Pre-PR the legacy registerTsProject registered ts-node/esm via Module.register and that handled it. Unrecoverable load failures also gave no guidance about the env opt-out. ## Expected Behavior .ts and .mts both go through loadTsFile first. Node 22.12+ supports require() of synchronous ESM by default, and loadTsFile's lazy fallback registers swc-node + tsconfig-paths on demand (swc-node hooks .cts/.mts/.ts). Async-only ESM (top-level await) throws ERR_REQUIRE_ASYNC_MODULE / ERR_REQUIRE_ESM and falls through to dynamic import() with the legacy registerTsProject path. Unrecoverable failures (e.g. .mts with TLA + enum, where dynamic import bypasses swc-node's CJS hook) now append a hint pointing at NX_PREFER_NODE_STRIP_TYPES=false and the env-var docs page. e2e covers .mts + enum (recovers) and .mts + TLA + enum (fails with hint, then succeeds with the env opt-out). ## Related Issue(s) Fixes NXC-4299
…ssertion ## Current Behavior Opt-out hint linked to environment-variables#nx_prefer_node_strip_types, which doesn't resolve - the page anchor uses dashes. The .mts TLA-recovery e2e asserted the report output contained 'nx', when only the throw/no-throw matters. ## Expected Behavior Hint links to environment-variables#nx-prefer-node-strip-types. The opt-out branch of the TLA test asserts runCLI doesn't throw and nothing else. ## Related Issue(s) Fixes NXC-4299
…ort fallback path ## Current Behavior ESM configs (.mts, or .ts as ESM) that combine top-level await with TypeScript syntax native strip can't handle (enum, runtime namespace, legacy decorators, etc.) had no recovery path. TLA forced dispatch to dynamic import(), which doesn't traverse Module._extensions, so swc-node's CJS hook never saw the file. Native strip ran on the dynamic-import path and threw. The only workaround was NX_PREFER_NODE_STRIP_TYPES=false, which opts the entire process out of native strip. ## Expected Behavior New forceRegisterEsmLoader() helper performs a one-shot Module.register on @swc-node/register/esm (preferred) or ts-node/esm. devkit's loadTypeScriptModule calls it before falling through to loadESM on ERR_REQUIRE_ESM / ERR_REQUIRE_ASYNC_MODULE. TLA + unsupported syntax now recovers transparently when one of those packages is installed. The trade-off is documented at the helper and the call site: Module.register is global and one-shot per process, so calling it forfeits Node's native TypeScript stripping for every subsequent ESM resolution in the run. CJS require() is unaffected (different hook), so .cts via require keeps using native strip + swc-node's Module._extensions hook. The cost only kicks in if a config actually triggers the fallback. legacy registerTsProject's existing inline ts-node/esm registration is replaced by the shared helper (best-effort there). Devkit guards typeof forceRegisterEsmLoader for cross-version peers. If neither swc-node nor ts-node is installed, the helper throws a clear error pointing at install + NX_PREFER_NODE_STRIP_TYPES=false. e2e flips the .mts TLA + enum case from "fails with hint" to "recovers and emits 'Registering ESM TypeScript loader' in verbose logs". ## Related Issue(s) Fixes NXC-4299
…YNC_MODULE resolve-changelog-renderer routed every renderer path through loadTsFile, including .js. With NX_PREFER_NODE_STRIP_TYPES=false (or Node without native strip) and no workspace tsconfig, loadTsFile throws - a plain JS renderer in a non-TS workspace regressed from working to failing. Angular's loadModule helper only fell through to dynamic import on ERR_REQUIRE_ESM, missing Node 22.12+'s ERR_REQUIRE_ASYNC_MODULE for ESM with top-level await. resolve-changelog-renderer uses loadTsFile only when the renderer path has a .ts/.cts/.mts extension, falling back to require() otherwise. Angular's loadModule catches both ERR_REQUIRE_ESM and ERR_REQUIRE_ASYNC_MODULE and dispatches to dynamic import(). Fixes NXC-4299
…ight config ## Current Behavior Two e2e regressions from removing @swc/helpers and dropping CJS-by-default behavior of swc-node: 1. NestJS app tests failed with "Cannot find module '@swc/helpers/_/_ts_decorate'". Default @nx/jest setup transforms with @swc/jest, which emits @swc/helpers requires for decorator metadata. Workspaces using decorators (NestJS, Angular, etc.) need @swc/helpers resolvable at test time. 2. Generated playwright.config.ts used __filename, which is undefined in ESM. Pre-PR, swc-node's CJS Module._extensions hook always treated .ts as CJS so __filename worked everywhere. With native strip, .ts in a type:module workspace (TS solution setup) loads as ESM and __filename throws "__filename is not defined in ES module scope", blocking project graph computation across e2e suites. ## Expected Behavior @nx/js init re-adds @swc/helpers (kept @swc-node/register and @swc/core dropped - lazy loadTsFile registers them on demand). The playwright config generator passes isTsSolutionSetup into the template; the template emits import.meta.dirname for TS solution / ESM workspaces and __filename otherwise. nxE2EPreset already accepts either a directory or file path. ## Related Issue(s) Fixes NXC-4299
…rkspaces ## Current Behavior Generators emitted playwright.config.ts / cypress.config.ts using __filename and bare-specifier subpath imports. Pre-PR, swc-node's CJS hook treated all .ts as CJS, so these worked everywhere. With native Node TypeScript stripping the file's effective module type is honored, and in workspaces (or projects) with package.json type:module: - __filename throws "__filename is not defined in ES module scope" - Bare imports like '@nx/cypress/plugins/cypress-preset' fail strict ESM resolution because the package has no exports map mapping the bare path This blocked project graph computation across many e2e suites whenever a workspace had an e2e project with one of these configs. astro-docs:format also failed because env-vars.mdoc wasn't re-formatted after the recent merge. ## Expected Behavior Playwright config generator detects ESM context (TS solution setup OR project/workspace package.json type=module). When ESM, the template emits import.meta.dirname; otherwise __filename. Cypress addDefaultE2EConfig and addDefaultCTConfig branch the same way and emit the import as '@nx/cypress/plugins/cypress-preset.js' for ESM. env-vars.mdoc reformatted. ## Related Issue(s) Fixes NXC-4299
9472d7a to
6e7f3f1
Compare
…e config cleanly, restore tsconfig-paths in registerTsProject, recover .cts ESM-syntax via swc-node ## Current Behavior - Playwright generator emitted `playwright.config.ts`. In default `apps` workspaces the file had top-level imports so Nx's native strip auto-detected ESM and `__filename` blew up with ReferenceError. In `type: "module"` workspaces a CJS-shape `.ts` was rejected (no `require`/`module.exports` in ESM scope). An ESM-shape `.ts` (top-level `import` + `import.meta.dirname`) also broke under Playwright's pirates loader, which compiles to CJS leaving `import.meta` intact; Node then re-detects ESM from the compiled output and errors on `exports is not defined in ES module scope` (the cascade across e2e-angular, e2e-cypress, e2e-eslint, e2e-jest, e2e-nx, e2e-react, e2e-remix, e2e-vite, e2e-vue, e2e-web). - `registerTsProject(tsConfigPath)` became a no-op under native strip - skipped BOTH transpiler and tsconfig-paths registration. Native strip handles transpilation, but path mapping is orthogonal: user code that explicitly calls `registerTsProject` to resolve `paths` aliases (e.g. Jest `globalSetup` requiring `@my-org/lib`) silently broke with MODULE_NOT_FOUND. - Legacy `.cts` files using ESM `export` syntax fail Node's strict CJS parser with SyntaxError. No fallback path existed under native strip, so configs that worked pre-v23 (via swc-node's CJS hook compiling away ESM syntax) break. - js-strip-types e2e asserted via `nx report`, which catches ProjectGraphError and exits 0 even when configs fail to load - so the regression above went undetected. jest.test.ts also wrote `export default` into a .cts fixture, broken for the same reason. ## Expected Behavior - Playwright generator now emits `playwright.config.cts`. Node forces `.cts` to CommonJS regardless of workspace `type`, so the file loads cleanly under Playwright's pirates runtime AND Nx's native strip. Playwright auto-discovers `.cts` via its configLoader extension list (`.ts/.js/.mts/.mjs/.cts/.cjs`). The Nx playwright plugin already globs all six extensions, so no plugin changes needed. - `registerTsProject` still skips the transpiler under native strip but always registers tsconfig-paths. `registerTsConfigPaths` short-circuits when the tsconfig has no `paths` entries, so package-manager-workspace setups (resolve via symlinks, no aliases) pay only the one-time tsconfig read - no per-require hook overhead. Workspaces with `paths` get the alias resolution they explicitly asked for. - loadTsFile detects SyntaxError on .cts/.cjs (new isCjsSyntaxError) and escalates to swc/ts-node, restoring pre-v23 behavior for ESM-syntax-in-CJS files. - js-strip-types swaps `nx report` for `nx graph --file=...` + JSON-graph assertions. Adds: regression test for playwright config emission, new .cts ESM-syntax recovery case, afterEach cleanup so plugin-worker state doesn't leak between fallback tests, .cts+require() extensionless-import scenario that actually exercises the MODULE_NOT_FOUND path. jest.test.ts .cts fixture now uses module.exports. Affected unit specs and snapshots in angular/nuxt/react/remix/vue/web updated to expect `.cts`. ## Related Issue(s) Fixes NXC-4299
Current Behavior
Loading
.tsconfig files always registered@swc-node/register(orts-node) andtsconfig-pathsup front, behind an opt-inNX_PREFER_NODE_STRIP_TYPES=trueflag. New workspaces shipped@swc-node/register,@swc/core,@swc/helperseven though Node 22.6+ can strip types natively.Expected Behavior
Native Node.js TypeScript stripping is now the default loader for
.tsconfig files; swc/ts-node andtsconfig-pathsonly register when native fails.process.features.typescriptis truthy (Node 23.6+ unflagged, 22.18+ LTS unflagged, 22.6-22.17 with--experimental-strip-types). Opt out viaNX_PREFER_NODE_STRIP_TYPES=false.loadTsFile(filePath, tsConfigPath?)helper triesrequire()with no registration. Retries lazily on the specific error:MODULE_NOT_FOUND/ERR_MODULE_NOT_FOUND→ registertsconfig-pathsonly.ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX(enum, runtime namespace, legacy decorators, etc.) → register swc/ts-node +tsconfig-paths.MODULE_NOT_FOUNDaftertsconfig-pathsalready tried (e.g. extensionlessimport './foo'wherefoo.tsis adjacent — Node's resolver doesn't add.ts) → escalate to swc/ts-node +tsconfig-paths.tsConfigPathdefaults to the workspace root tsconfig..tsand.mtsare unified throughloadTsFile. Node 22.12+ supportsrequire()of synchronous ESM, and swc-node'sModule._extensionshook covers.cts/.mts/.ts. Async-only ESM (top-level await) throwsERR_REQUIRE_ASYNC_MODULE/ERR_REQUIRE_ESMand falls through to dynamicimport(). On that path, devkit callsforceRegisterEsmLoader(one-shotModule.registerof@swc-node/register/esmorts-node/esm) so configs that combine TLA with unsupported TS syntax (enum, runtime namespace, etc.) still recover.registerTsProjectandregisterPluginTSTranspilerare noops under native strip.handleImportapplies the same lazy fallback for plugin loads.NX_DISABLE_TSCONFIG_PATHS=trueskipstsconfig-pathseven on fallback (for workspaces relying on package manager workspaces).NX_VERBOSE_LOGGING=truelogs each fallback trigger, including the ESM loader registration.NX_PREFER_NODE_STRIP_TYPES=falseand the env-var docs page.loadConfigFile, webpack, eslint-plugin, angular builders, module-federation) migrated toloadTsFile.@nx/jsinit no longer installs@swc-node/register,@swc/core, or@swc/helpers. Bundler-specific generators (@nx/js:lib --bundler=swc,setup-build,convert-to-swc) install@swc/helperson demand.Trade-off documented in code
Module.registeris global and one-shot per process. OnceforceRegisterEsmLoaderruns, every subsequent ESM resolution in the run is routed through the registered loader, forfeiting native strip for the dynamic-import path. CJSrequire()is unaffected (different hook), so.cts/.tsvia require keep using native strip + swc-node'sModule._extensionshook. The helper is only called when a config actually triggers the ESM fallback, so workspaces that don't hit it pay nothing. If neither@swc-node/registernorts-nodeis installed, the helper throws a clear error pointing at install + theNX_PREFER_NODE_STRIP_TYPES=falseenv opt-out.Related Issue(s)
Fixes NXC-4299