Skip to content

feat(core): enable native Node.js TypeScript stripping by default#35608

Open
jaysoo wants to merge 19 commits into
masterfrom
NXC-4299
Open

feat(core): enable native Node.js TypeScript stripping by default#35608
jaysoo wants to merge 19 commits into
masterfrom
NXC-4299

Conversation

@jaysoo
Copy link
Copy Markdown
Member

@jaysoo jaysoo commented May 7, 2026

Current Behavior

Loading .ts config files always registered @swc-node/register (or ts-node) and tsconfig-paths up front, behind an opt-in NX_PREFER_NODE_STRIP_TYPES=true flag. New workspaces shipped @swc-node/register, @swc/core, @swc/helpers even though Node 22.6+ can strip types natively.

Expected Behavior

Native Node.js TypeScript stripping is now the default loader for .ts config files; swc/ts-node and tsconfig-paths only register when native fails.

  • Default flips when process.features.typescript is truthy (Node 23.6+ unflagged, 22.18+ LTS unflagged, 22.6-22.17 with --experimental-strip-types). Opt out via NX_PREFER_NODE_STRIP_TYPES=false.
  • New loadTsFile(filePath, tsConfigPath?) helper tries require() with no registration. Retries lazily on the specific error:
    • MODULE_NOT_FOUND / ERR_MODULE_NOT_FOUND → register tsconfig-paths only.
    • ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX (enum, runtime namespace, legacy decorators, etc.) → register swc/ts-node + tsconfig-paths.
    • MODULE_NOT_FOUND after tsconfig-paths already tried (e.g. extensionless import './foo' where foo.ts is adjacent — Node's resolver doesn't add .ts) → escalate to swc/ts-node + tsconfig-paths.
    • Each registration runs at most once, so a file recovers in ≤3 attempts. tsConfigPath defaults to the workspace root tsconfig.
  • .ts and .mts are unified through loadTsFile. Node 22.12+ supports require() of synchronous ESM, and swc-node's Module._extensions hook covers .cts/.mts/.ts. Async-only ESM (top-level await) throws ERR_REQUIRE_ASYNC_MODULE / ERR_REQUIRE_ESM and falls through to dynamic import(). On that path, devkit calls forceRegisterEsmLoader (one-shot Module.register of @swc-node/register/esm or ts-node/esm) so configs that combine TLA with unsupported TS syntax (enum, runtime namespace, etc.) still recover.
  • registerTsProject and registerPluginTSTranspiler are noops under native strip. handleImport applies the same lazy fallback for plugin loads.
  • NX_DISABLE_TSCONFIG_PATHS=true skips tsconfig-paths even on fallback (for workspaces relying on package manager workspaces).
  • NX_VERBOSE_LOGGING=true logs each fallback trigger, including the ESM loader registration.
  • Unrecoverable failures append a hint pointing at NX_PREFER_NODE_STRIP_TYPES=false and the env-var docs page.
  • Cited callers (changelog renderer, version actions, devkit loadConfigFile, webpack, eslint-plugin, angular builders, module-federation) migrated to loadTsFile.
  • @nx/js init 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/helpers on demand.

Trade-off documented in code

Module.register is global and one-shot per process. Once forceRegisterEsmLoader runs, every subsequent ESM resolution in the run is routed through the registered loader, forfeiting native strip for the dynamic-import path. CJS require() is unaffected (different hook), so .cts/.ts via require keep using native strip + swc-node's Module._extensions hook. 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/register nor ts-node is installed, the helper throws a clear error pointing at install + the NX_PREFER_NODE_STRIP_TYPES=false env opt-out.

Related Issue(s)

Fixes NXC-4299

@jaysoo jaysoo requested a review from a team as a code owner May 7, 2026 17:45
@jaysoo jaysoo requested a review from leosvelperez May 7, 2026 17:45
@netlify
Copy link
Copy Markdown

netlify Bot commented May 7, 2026

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit a47532e
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a029c22bf5e3d0008afa5ce
😎 Deploy Preview https://deploy-preview-35608--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link
Copy Markdown

netlify Bot commented May 7, 2026

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit a47532e
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a029c2219d3400008c6913e
😎 Deploy Preview https://deploy-preview-35608--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud
Copy link
Copy Markdown
Contributor

nx-cloud Bot commented May 7, 2026

View your CI Pipeline Execution ↗ for commit a47532e

Command Status Duration Result
nx affected --targets=lint,test,build,e2e,e2e-c... ❌ Failed 39m 21s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 4s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 17s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 23s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 8s View ↗

☁️ Nx Cloud last updated this comment at 2026-05-12 04:02:13 UTC

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

@jaysoo jaysoo marked this pull request as draft May 7, 2026 18:57
@jaysoo jaysoo marked this pull request as ready for review May 7, 2026 19:52
FrozenPandaz pushed a commit that referenced this pull request May 8, 2026
## 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
Comment thread packages/angular/src/executors/utilities/module-loader.ts Outdated
@jaysoo jaysoo force-pushed the NXC-4299 branch 2 times, most recently from 5abe99f to d665fa4 Compare May 8, 2026 20:16
nx-cloud[bot]

This comment was marked as outdated.

nx-cloud[bot]

This comment was marked as outdated.

Copy link
Copy Markdown
Contributor

@nx-cloud nx-cloud Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ↗

Apply fix via Nx Cloud  Reject fix via 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

jaysoo and others added 13 commits May 11, 2026 09:27
## 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
jaysoo added 5 commits May 11, 2026 09:27
…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
@jaysoo jaysoo force-pushed the NXC-4299 branch 5 times, most recently from 9472d7a to 6e7f3f1 Compare May 12, 2026 00:26
…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant