Skip to content

fix(core): pin typescript in preset dependencies so npm cannot hoist typescript 7 - #36497

Merged
FrozenPandaz merged 1 commit into
masterfrom
fix/cnw-pin-typescript-preset
Jul 29, 2026
Merged

fix(core): pin typescript in preset dependencies so npm cannot hoist typescript 7#36497
FrozenPandaz merged 1 commit into
masterfrom
fix/cnw-pin-typescript-preset

Conversation

@FrozenPandaz

Copy link
Copy Markdown
Contributor

Current Behavior

create-nx-workspace fails for most framework presets:

✔ Installing dependencies with npm
✖ Creating your workspace in test

 NX   Failed to create workspace

Failed to create a workspace:
 NX   ts.readConfigFile is not a function

TypeScript 7 is now latest on npm. Its main entry point exports only version and versionMajorMinor — the compiler API moved to typescript/unstable/* and is not a drop-in replacement (no readConfigFile, parseJsonConfigFileContent, resolveModuleName, createProgram, or createCompilerHost anywhere in those subpaths, including today's 7.1.0-dev nightly).

@phenomnomnominal/tsquery declares typescript: >3.0.0 as a peer dependency. npm auto-installs peers, so when the new workspace's package.json has no typescript entry, npm resolves that peer to the newest major — 7.x — and hoists it to the workspace root. The preset generator then reaches getNeededCompilerOptionOverrides and calls ts.readConfigFile on a module that no longer has it.

Whether this bites depends on npm hoisting order, which is why only some presets break:

next:   @nx/next → tsquery → typescript@7.0.2      ← depth 1, wins the root slot
        @nx/next → @nx/eslint → typescript@6.0.3   ← depth 2, gets nested

node:   @nx/node → @nx/eslint → typescript@6.0.3   ← wins the root slot
        @nx/node → @nx/jest → tsquery              ← deduped to 6.0.3

angular, nest and web-components already pinned typescript in their preset dependencies and were unaffected. Verified on create-nx-workspace@23.1.0 with npm:

preset result
angular-monorepo (pinned) exit 0, root typescript 6.0.3
next (unpinned) exit 1, ts.readConfigFile is not a function

Expected Behavior

getPresetDependencies pins typescript for the remaining presets that scaffold a TypeScript project: express, next (+ standalone), vue, nuxt, react (+ standalone), react-native, expo, node, ts and ts-standalone. The pin lands in package.json before the first install, so npm resolves tsquery's peer against it instead of against latest.

This adds nothing to the final workspace. @nx/js:init already installs the same ~6.0.3; the pin only changes when it is written — before the install rather than after — which is what npm needs in order to resolve the peer correctly.

apps and npm are split out of the case they shared with the TS presets and deliberately left unpinned: neither runs a preset generator (preset.ts returns immediately for apps, and new.ts skips generatePreset for npm), so @nx/js:init never runs and typescript would be net-new there. Neither pulls tsquery, so neither is exposed.

ts-standalone is the only preset that forwards js to its generator (preset.ts:324) and the only one that prompts for JS vs TS, so its pin mirrors @nx/js:init and is skipped when js is set.

Notes for reviewers

  • This makes workspace creation deterministic; it does not add TypeScript 7 support. A workspace that installs TypeScript 7 deliberately still hits the same TypeError from the project graph via the bare require('typescript') in packages/nx/src/plugins/js/utils/typescript.ts. That is Nx breaks with TypeScript 7: programmatic API completely restructured #36306 and is out of scope here.
  • Tests: 3 existing assertions in new.spec.ts (react/vue/nuxt) updated to include typescript, matching the existing angular assertion. 3 new tests cover the split — apps/npm stay TypeScript-free, ts gets the pin, and ts-standalone --js does not.

Related Issue(s)

Related to #36306 (Nx does not yet support the TypeScript 7 API). That issue is not fixed by this PR and should stay open.

Fixes N/A


View session information ↗

…typescript 7

`@phenomnomnominal/tsquery` declares `typescript: >3.0.0` as a peer dependency.
npm auto-installs peers, so with no `typescript` in the new workspace's
package.json it resolves that peer to the latest major — now 7.x — and hoists
it to the workspace root. TypeScript 7's entry point only exports `version` and
`versionMajorMinor`; the compiler API moved to `typescript/unstable/*`. The
preset generator then fails with `ts.readConfigFile is not a function`.

Whether this bites depends on hoisting order. `@nx/next` and `@nx/react` depend
on tsquery directly, so its permissive peer wins the root slot over
`@nx/eslint`'s concrete `~6.0.3`. In the node preset tsquery sits one level
deeper and `@nx/eslint` wins instead, which is why only some presets break.

Pinning `typescript` alongside the other preset dependencies puts a concrete
range in package.json before the first install, so npm resolves the peer to it.
angular, nest and web-components already did this. This adds nothing new to the
final workspace — `@nx/js:init` already installs the same version — it just
lands before the install rather than after.

The apps and npm presets are excluded and split out of the branch they shared
with ts and ts-standalone: neither runs a preset generator, so `@nx/js:init`
never runs and typescript would be net-new there. ts-standalone is the only
preset that prompts for JS vs TS, so it mirrors `@nx/js:init` and skips the pin
when `js` is set.
@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 3531a60
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a692888b3e54e00082d3189
😎 Deploy Preview https://deploy-preview-36497--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.

@netlify

netlify Bot commented Jul 28, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 3531a60
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a69288815b5c800074f3097
😎 Deploy Preview https://deploy-preview-36497--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.

@nx-cloud

nx-cloud Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 3531a60

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

☁️ Nx Cloud last updated this comment at 2026-07-28 23:05:33 UTC

@FrozenPandaz
FrozenPandaz marked this pull request as ready for review July 28, 2026 22:26
@FrozenPandaz
FrozenPandaz requested a review from a team as a code owner July 28, 2026 22:26
@FrozenPandaz
FrozenPandaz requested a review from MaxKless July 28, 2026 22:26
@FrozenPandaz
FrozenPandaz merged commit 0abf7a4 into master Jul 29, 2026
26 checks passed
@FrozenPandaz
FrozenPandaz deleted the fix/cnw-pin-typescript-preset branch July 29, 2026 03:43
rapaglaz pushed a commit to rapaglaz/nx that referenced this pull request Jul 29, 2026
…typescript 7 (nrwl#36497)

## Current Behavior

`create-nx-workspace` fails for most framework presets:

```
✔ Installing dependencies with npm
✖ Creating your workspace in test

 NX   Failed to create workspace

Failed to create a workspace:
 NX   ts.readConfigFile is not a function
```

TypeScript 7 is now `latest` on npm. Its main entry point exports only
`version` and `versionMajorMinor` — the compiler API moved to
`typescript/unstable/*` and is not a drop-in replacement (no
`readConfigFile`, `parseJsonConfigFileContent`, `resolveModuleName`,
`createProgram`, or `createCompilerHost` anywhere in those subpaths,
including today's `7.1.0-dev` nightly).

`@phenomnomnominal/tsquery` declares `typescript: >3.0.0` as a peer
dependency. npm auto-installs peers, so when the new workspace's
`package.json` has no `typescript` entry, npm resolves that peer to the
newest major — 7.x — and hoists it to the workspace root. The preset
generator then reaches `getNeededCompilerOptionOverrides` and calls
`ts.readConfigFile` on a module that no longer has it.

Whether this bites depends on npm hoisting order, which is why only some
presets break:

```
next:   @nx/next → tsquery → typescript@7.0.2      ← depth 1, wins the root slot
        @nx/next → @nx/eslint → typescript@6.0.3   ← depth 2, gets nested

node:   @nx/node → @nx/eslint → typescript@6.0.3   ← wins the root slot
        @nx/node → @nx/jest → tsquery              ← deduped to 6.0.3
```

`angular`, `nest` and `web-components` already pinned `typescript` in
their preset dependencies and were unaffected. Verified on
`create-nx-workspace@23.1.0` with npm:

| preset | result |
| --- | --- |
| `angular-monorepo` (pinned) | exit 0, root `typescript` 6.0.3 |
| `next` (unpinned) | exit 1, `ts.readConfigFile is not a function` |

## Expected Behavior

`getPresetDependencies` pins `typescript` for the remaining presets that
scaffold a TypeScript project: express, next (+ standalone), vue, nuxt,
react (+ standalone), react-native, expo, node, ts and ts-standalone.
The pin lands in `package.json` before the first install, so npm
resolves tsquery's peer against it instead of against `latest`.

This adds nothing to the final workspace. `@nx/js:init` already installs
the same `~6.0.3`; the pin only changes *when* it is written — before
the install rather than after — which is what npm needs in order to
resolve the peer correctly.

`apps` and `npm` are split out of the case they shared with the TS
presets and deliberately left unpinned: neither runs a preset generator
(`preset.ts` returns immediately for `apps`, and `new.ts` skips
`generatePreset` for `npm`), so `@nx/js:init` never runs and
`typescript` would be net-new there. Neither pulls tsquery, so neither
is exposed.

`ts-standalone` is the only preset that forwards `js` to its generator
(`preset.ts:324`) and the only one that prompts for JS vs TS, so its pin
mirrors `@nx/js:init` and is skipped when `js` is set.

### Notes for reviewers

- This makes workspace creation deterministic; it does not add
TypeScript 7 support. A workspace that installs TypeScript 7
deliberately still hits the same `TypeError` from the project graph via
the bare `require('typescript')` in
`packages/nx/src/plugins/js/utils/typescript.ts`. That is nrwl#36306 and is
out of scope here.
- Tests: 3 existing assertions in `new.spec.ts` (react/vue/nuxt) updated
to include `typescript`, matching the existing angular assertion. 3 new
tests cover the split — `apps`/`npm` stay TypeScript-free, `ts` gets the
pin, and `ts-standalone --js` does not.

## Related Issue(s)

Related to nrwl#36306 (Nx does not yet support the TypeScript 7 API). That
issue is **not** fixed by this PR and should stay open.

Fixes N/A

<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Pin-typescript-in-create-nx-workspace-preset-dependencies-84db677c)
<!-- polygraph-session-end -->
FrozenPandaz added a commit that referenced this pull request Jul 29, 2026
…typescript 7 (#36497)

## Current Behavior

`create-nx-workspace` fails for most framework presets:

```
✔ Installing dependencies with npm
✖ Creating your workspace in test

 NX   Failed to create workspace

Failed to create a workspace:
 NX   ts.readConfigFile is not a function
```

TypeScript 7 is now `latest` on npm. Its main entry point exports only
`version` and `versionMajorMinor` — the compiler API moved to
`typescript/unstable/*` and is not a drop-in replacement (no
`readConfigFile`, `parseJsonConfigFileContent`, `resolveModuleName`,
`createProgram`, or `createCompilerHost` anywhere in those subpaths,
including today's `7.1.0-dev` nightly).

`@phenomnomnominal/tsquery` declares `typescript: >3.0.0` as a peer
dependency. npm auto-installs peers, so when the new workspace's
`package.json` has no `typescript` entry, npm resolves that peer to the
newest major — 7.x — and hoists it to the workspace root. The preset
generator then reaches `getNeededCompilerOptionOverrides` and calls
`ts.readConfigFile` on a module that no longer has it.

Whether this bites depends on npm hoisting order, which is why only some
presets break:

```
next:   @nx/next → tsquery → typescript@7.0.2      ← depth 1, wins the root slot
        @nx/next → @nx/eslint → typescript@6.0.3   ← depth 2, gets nested

node:   @nx/node → @nx/eslint → typescript@6.0.3   ← wins the root slot
        @nx/node → @nx/jest → tsquery              ← deduped to 6.0.3
```

`angular`, `nest` and `web-components` already pinned `typescript` in
their preset dependencies and were unaffected. Verified on
`create-nx-workspace@23.1.0` with npm:

| preset | result |
| --- | --- |
| `angular-monorepo` (pinned) | exit 0, root `typescript` 6.0.3 |
| `next` (unpinned) | exit 1, `ts.readConfigFile is not a function` |

## Expected Behavior

`getPresetDependencies` pins `typescript` for the remaining presets that
scaffold a TypeScript project: express, next (+ standalone), vue, nuxt,
react (+ standalone), react-native, expo, node, ts and ts-standalone.
The pin lands in `package.json` before the first install, so npm
resolves tsquery's peer against it instead of against `latest`.

This adds nothing to the final workspace. `@nx/js:init` already installs
the same `~6.0.3`; the pin only changes *when* it is written — before
the install rather than after — which is what npm needs in order to
resolve the peer correctly.

`apps` and `npm` are split out of the case they shared with the TS
presets and deliberately left unpinned: neither runs a preset generator
(`preset.ts` returns immediately for `apps`, and `new.ts` skips
`generatePreset` for `npm`), so `@nx/js:init` never runs and
`typescript` would be net-new there. Neither pulls tsquery, so neither
is exposed.

`ts-standalone` is the only preset that forwards `js` to its generator
(`preset.ts:324`) and the only one that prompts for JS vs TS, so its pin
mirrors `@nx/js:init` and is skipped when `js` is set.

### Notes for reviewers

- This makes workspace creation deterministic; it does not add
TypeScript 7 support. A workspace that installs TypeScript 7
deliberately still hits the same `TypeError` from the project graph via
the bare `require('typescript')` in
`packages/nx/src/plugins/js/utils/typescript.ts`. That is #36306 and is
out of scope here.
- Tests: 3 existing assertions in `new.spec.ts` (react/vue/nuxt) updated
to include `typescript`, matching the existing angular assertion. 3 new
tests cover the split — `apps`/`npm` stay TypeScript-free, `ts` gets the
pin, and `ts-standalone --js` does not.

## Related Issue(s)

Related to #36306 (Nx does not yet support the TypeScript 7 API). That
issue is **not** fixed by this PR and should stay open.

Fixes N/A

<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Pin-typescript-in-create-nx-workspace-preset-dependencies-84db677c)
<!-- polygraph-session-end -->

(cherry picked from commit 0abf7a4)
polygraph-snapshot-app Bot pushed a commit that referenced this pull request Aug 11, 2026
…typescript 7 (#36497)

## Current Behavior

`create-nx-workspace` fails for most framework presets:

```
✔ Installing dependencies with npm
✖ Creating your workspace in test

 NX   Failed to create workspace

Failed to create a workspace:
 NX   ts.readConfigFile is not a function
```

TypeScript 7 is now `latest` on npm. Its main entry point exports only
`version` and `versionMajorMinor` — the compiler API moved to
`typescript/unstable/*` and is not a drop-in replacement (no
`readConfigFile`, `parseJsonConfigFileContent`, `resolveModuleName`,
`createProgram`, or `createCompilerHost` anywhere in those subpaths,
including today's `7.1.0-dev` nightly).

`@phenomnomnominal/tsquery` declares `typescript: >3.0.0` as a peer
dependency. npm auto-installs peers, so when the new workspace's
`package.json` has no `typescript` entry, npm resolves that peer to the
newest major — 7.x — and hoists it to the workspace root. The preset
generator then reaches `getNeededCompilerOptionOverrides` and calls
`ts.readConfigFile` on a module that no longer has it.

Whether this bites depends on npm hoisting order, which is why only some
presets break:

```
next:   @nx/next → tsquery → typescript@7.0.2      ← depth 1, wins the root slot
        @nx/next → @nx/eslint → typescript@6.0.3   ← depth 2, gets nested

node:   @nx/node → @nx/eslint → typescript@6.0.3   ← wins the root slot
        @nx/node → @nx/jest → tsquery              ← deduped to 6.0.3
```

`angular`, `nest` and `web-components` already pinned `typescript` in
their preset dependencies and were unaffected. Verified on
`create-nx-workspace@23.1.0` with npm:

| preset | result |
| --- | --- |
| `angular-monorepo` (pinned) | exit 0, root `typescript` 6.0.3 |
| `next` (unpinned) | exit 1, `ts.readConfigFile is not a function` |

## Expected Behavior

`getPresetDependencies` pins `typescript` for the remaining presets that
scaffold a TypeScript project: express, next (+ standalone), vue, nuxt,
react (+ standalone), react-native, expo, node, ts and ts-standalone.
The pin lands in `package.json` before the first install, so npm
resolves tsquery's peer against it instead of against `latest`.

This adds nothing to the final workspace. `@nx/js:init` already installs
the same `~6.0.3`; the pin only changes *when* it is written — before
the install rather than after — which is what npm needs in order to
resolve the peer correctly.

`apps` and `npm` are split out of the case they shared with the TS
presets and deliberately left unpinned: neither runs a preset generator
(`preset.ts` returns immediately for `apps`, and `new.ts` skips
`generatePreset` for `npm`), so `@nx/js:init` never runs and
`typescript` would be net-new there. Neither pulls tsquery, so neither
is exposed.

`ts-standalone` is the only preset that forwards `js` to its generator
(`preset.ts:324`) and the only one that prompts for JS vs TS, so its pin
mirrors `@nx/js:init` and is skipped when `js` is set.

### Notes for reviewers

- This makes workspace creation deterministic; it does not add
TypeScript 7 support. A workspace that installs TypeScript 7
deliberately still hits the same `TypeError` from the project graph via
the bare `require('typescript')` in
`packages/nx/src/plugins/js/utils/typescript.ts`. That is #36306 and is
out of scope here.
- Tests: 3 existing assertions in `new.spec.ts` (react/vue/nuxt) updated
to include `typescript`, matching the existing angular assertion. 3 new
tests cover the split — `apps`/`npm` stay TypeScript-free, `ts` gets the
pin, and `ts-standalone --js` does not.

## Related Issue(s)

Related to #36306 (Nx does not yet support the TypeScript 7 API). That
issue is **not** fixed by this PR and should stay open.

Fixes N/A

<!-- polygraph-session-start -->
---
[View session information
↗](https://app.trypolygraph.com/orgs/6a061dcb561c062131116eca/sessions/Pin-typescript-in-create-nx-workspace-preset-dependencies-84db677c)
<!-- polygraph-session-end -->
polygraph-snapshot-app Bot pushed a commit that referenced this pull request Aug 11, 2026
## Current Behavior

`apps`/`npm` preset workspaces pin no root typescript, so npm resolves
tsquery's open `typescript: >3.0.0` peer to 7.x. TS 7's CJS entry drops the
compiler API - tsquery crashes at load and `readTsConfig` sees `ts.sys`
undefined. Breaks e2e-vite, e2e-web, e2e-rspack on npm.

e2e-expo: Jest's NODE_ENV=test leaves @expo/cli's fusebox debugger shell on,
and dev-middleware 0.85+ throws "DefaultToolLauncher must be mocked or
overridden in tests". Behind it, `@nx/expo:serve` waits for a `Bundled` line
that only arrives once something requests the page, deadlocking @nx/cypress.

## Expected Behavior

`apps`/`npm` presets pin typescript, like every other preset (#36497).

Expo e2e sets EXPO_UNSTABLE_HEADLESS=1 through a shared `setupExpoEnv` helper.

`@nx/expo:serve` resolves once the packager accepts connections.

## Related Issue(s)

Fixes NXC-4612
jaysoo added a commit that referenced this pull request Aug 11, 2026
## Current Behavior

`apps`/`npm` preset workspaces pin no root typescript, so npm resolves
tsquery's open `typescript: >3.0.0` peer to 7.x. TS 7's CJS entry drops the
compiler API - tsquery crashes at load and `readTsConfig` sees `ts.sys`
undefined. Breaks e2e-vite, e2e-web, e2e-rspack on npm.

e2e-expo: Jest's NODE_ENV=test leaves @expo/cli's fusebox debugger shell on,
and dev-middleware 0.85+ throws "DefaultToolLauncher must be mocked or
overridden in tests". Behind it, `@nx/expo:serve` waits for a `Bundled` line
that only arrives once something requests the page, deadlocking @nx/cypress.

## Expected Behavior

`apps`/`npm` presets pin typescript, like every other preset (#36497).

Expo e2e sets EXPO_UNSTABLE_HEADLESS=1 through a shared `setupExpoEnv` helper.

`@nx/expo:serve` resolves once the packager accepts connections.

## Related Issue(s)

Fixes NXC-4612
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants