Skip to content

feat(build)!: compile with the TypeScript 7 native compiler - #11793

Open
raymondfeng wants to merge 9 commits into
masterfrom
upgrade-typescript-7
Open

raymondfeng wants to merge 9 commits into
masterfrom
upgrade-typescript-7

Conversation

@raymondfeng

@raymondfeng raymondfeng commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Upgrades the monorepo to TypeScript 7, using the side-by-side install recommended for TypeScript 7:

"@typescript/native": "npm:typescript@~7.0.2",
"typescript": "npm:@typescript/typescript6@~6.0.2"

Stacked PR. Per review, this PR was split. It builds on #11811 (typescript-eslint v8) and #11810 (mock-oauth2-provider CodeQL fix), whose commits appear here until they merge. The .yo-rc.json bug is fixed separately in #11812. Merge order: #11810 → #11811 → this PR. Only the last three commits belong to this PR.

TypeScript 7 no longer ships the JS compiler API: its exports["."] resolves to lib/version.cjs. So anything that introspects the compiler (@typescript-eslint, editor language services, tsserver) needs the API that @typescript/typescript6 still provides. The two packages declare different bin names (tsc vs tsc6), so they don't collide.

packages/build/package.json is the source of truth. bin/sync-dev-deps propagates the spec to every workspace package, and bin/update-template-deps carries it into the lb4 scaffolding templates.

A cold build of the monorepo drops from ~10.7s to ~2.1s.

Commits

  1. feat(build)!: the toolchain, tsconfig.common.json, and the README. This is the only commit with BREAKING CHANGE:, so lerna's conventional-commit bump makes a major release of @loopback/build only.
    • @typescript/native is a hard dependency of @loopback/build, and lb-tsc always compiles with TypeScript 7. It prefers the project's own @typescript/native over the one @loopback/build depends on. The fallback to typescript/lib/tsc was unreachable and has been removed.
    • bin/sync-dev-deps.js had been broken since the Lerna → npm workspaces migration (d5c4994 removed loadLernaRepo). It's repaired using the @npmcli/map-workspaces pattern already used by update-template-deps.js.
  2. feat(cli): generated projects target TypeScript 7 and drop moduleResolution: node. The template's mocha moves to ^12; update-template-deps syncs it with the monorepo, which already uses mocha 12.
  3. fix:: the source changes TypeScript 6/7 requires, and the typescript alias in every package.

TypeScript 6/7 changes that required source updates

  • moduleResolution: node (node10) was removed. tsconfig.common.json now sets "moduleResolution": "bundler" explicitly, which is the TypeScript 6 default. With module: commonjs it resolves exports maps using the require and types conditions. Because the resolver honors exports, @loopback/graphql no longer imports Middleware from a deep type-graphql path (that path isn't exported) and declares the type itself.
  • @types/* packages are no longer auto-included. This accounted for 5630 of the 5668 initial errors (describe, process, NodeJS all unresolved). tsconfig.common.json now lists types: ["node", "mocha"].
  • Reading a parent class field through super is an error (TS2855). cron declares fireOnTick as a field but installs it on the prototype, so @loopback/cron now calls it through BaseCronJob.prototype.
  • Declaration emit no longer adds /// <reference path> for ambient files. TypeScript 5.2 emitted one into testlab/dist/expect.d.ts, which is how consumers resolved the global Internal type. Without it, expect silently becomes any in every consuming package, and api-extractor fails with Unable to follow symbol for "Internal". So should-as-function.d.ts moves to src/should-as-function.ts and exports its types as a module.
  • Non-portable inferred types (TS2883). GRAPHQL_CONTEXT_RESOLVER gets an explicit type. Otherwise declaration emit has to name ContextFunction by its path inside @apollo/server.

With TypeScript 6 type information, typescript-eslint reports more findings:

  • Async Express handlers in @loopback/rest and the rpc-server example now pass rejections to next.
  • transactions.suite.ts awaits disconnect().
  • ?? replaces || on objects and arrays. || is kept wherever the operand may be '' (see feat(eslint-config)!: upgrade typescript-eslint to v8 #11811 for the ignorePrimitives rationale).

Defects found along the way

  • belongs-to-repository-factory.unit.ts never assigned companyRepo. Its stub helper assigned customerRepo twice, clobbering the previous beforeEach.
  • The todo-list repository integration tests used repositories they never created.

Verification

Check Result
Cold build (TS 7.0.2) clean
npm run lint (eslint + prettier) clean
@loopback/build tests 21 passing
npm run mocha 4492 passing; the 220 failures are identical to #11811 (TypeScript 5.2)

The 220 failures don't come from this change:

  • Most are packages/cli generator tests. They refuse to prompt when stdin isn't a TTY, so they fail in a non-interactive shell.
  • Two are the todo geocoder tests, whose fixtures don't match the live geocoder's current coordinates.

Checklist

  • DCO (Developer Certificate of Origin) signed in all commits
  • npm test passes on your machine: see Verification
  • New tests added or existing tests modified to cover all changes
  • Code conforms with the style guide
  • API Documentation in code was updated
  • Documentation in /docs/site was updated
  • Affected artifact templates in packages/cli were updated
  • Affected example projects in examples/* were updated

🤖 Generated with Claude Code

Comment thread fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts Fixed
Comment thread fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts Fixed

@samarpanB samarpanB left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for this, the TS 6/7 investigation and the write-up are really thorough, and the build speedup is great.

My main concern is scope and semver. The PR bundles four independent changes: the TS 7 toolchain, typescript-eslint v8, ~60 ||→?? / ??= rewrites in framework code, and an unrelated CodeQL fix in the mock OAuth2 fixture. Two of them are breaking for downstream users but land under a non-breaking feat(build).

Blocking

  1. @typescript/native is a hard dependency of @loopback/build, so the "fall back to typescript when TS 7 isn't installed" path is never reached for consumers. Any app pinned to TS 5.x gets compiled by TS 7 on a minor bump (see inline). This needs feat(build)! + BREAKING CHANGE:, or the native compiler should be opt-in.
  2. @loopback/eslint-config → typescript-eslint v8 is also breaking for consumers (different peer ranges, new/renamed rules). It needs a major bump too.
  3. packages/cli/.yo-rc.json is regenerated with every generator's arguments emptied (see inline).

Suggest splitting out

  • The ||→?? conversions. Most are equivalent, but several change behavior when a value is '' in core packages (inline examples in context, core, boot; also extension-point.ts, inject-config.ts, controller-route.ts, build-schema.ts, repository.mixin.ts, lb3app.booter.ts restApiRoot). They may all be improvements, but they deserve their own refactor/fix PR. Here, the prefer-nullish-coalescing options (ignorePrimitives, ignoreTernaryTests, ignoreIfStatements) could keep this PR behavior-neutral.
  • The fix(mock-oauth2-provider) commit. The fix looks right, but it's unrelated to the TS upgrade.

Minor

  • try-catch-finally.unit.ts: three eslint-disable-next-line comments were replaced by blank lines. The lines can just be removed.
  • graphql/keys.ts: a short comment on why the explicit BindingKey<...> annotation on GRAPHQL_CONTEXT_RESOLVER is needed would help.
  • lb-ttsc / --use-ttypescript is now effectively TS 6-only. Worth a README note or a deprecation.
  • The PR currently has merge conflicts with master.

Looks good: the duplicate route() overload removal, the should-as-function move (nice catch on expect silently becoming any), the cron fireOnTick fix, the sync-dev-deps.js repair, and the test fixes in belongs-to / todo-list / transactions.suite.

Comment thread packages/build/package.json
Comment thread packages/build/bin/compile-package.js Outdated
Comment thread packages/eslint-config/package.json
Comment thread packages/build/config/tsconfig.common.json
Comment thread packages/cli/.yo-rc.json Outdated
Comment thread examples/passport-login/src/authentication-strategies/basic.ts Outdated
Comment thread examples/passport-login/src/authentication-strategies/local.ts Outdated
Comment thread fixtures/mock-oauth2-provider/src/mock-oauth2-social-app.ts Outdated
Comment thread packages/cli/package.json
Comment thread packages/rest/src/rest.server.ts
raymondfeng and others added 7 commits October 1, 2026 21:01
typescript-eslint v7 does not support TypeScript 6 or later, so it blocks the
TypeScript upgrade.

Rules that v8 renamed or split are disabled to keep the rule set this
configuration already had: `no-require-imports` replaces `no-var-requires`, and
`ban-types` is split into `no-empty-object-type`, `no-unsafe-function-type` and
`no-wrapper-object-types`.

`prefer-nullish-coalescing` now ignores primitive operands. The rule runs
without `strictNullChecks`, so it cannot tell whether a `string`, `number` or
`boolean` operand may be nullish, and v8 reports `||` on such operands, where
switching to `??` changes the result for `''`, `0` and `false`.

BREAKING CHANGE: `@loopback/eslint-config` requires `@typescript-eslint/parser`
and `@typescript-eslint/eslint-plugin` v8. Projects using it can get new
findings, such as unused `catch` bindings (`no-unused-vars`),
`prefer-optional-chain`, and `prefer-nullish-coalescing` for
`if (x == null) x = y`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
- Drop unused `catch` bindings (`no-unused-vars`).
- Use optional chains where they are equivalent (`prefer-optional-chain`).
- Use `??=` for `if (x == null) x = y` and for `!x` guards on values that are
  objects or arrays, where it is equivalent (`prefer-nullish-coalescing`).
- Remove the `route()` overload that `RestApplication` declared twice
  (`unified-signatures`).
- Remove `eslint-disable` directives that no longer suppress anything.

`||` is kept wherever the left operand may be `''` or `0`. The passport-login
example keeps its explicit `!user.credentials` guard, since
`user.credentials?.password !== password` accepts a user without credentials
when `password` is `undefined`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
…n redirect

Registered apps and issued tokens were held in plain objects keyed by values
taken from the request, so a `__proto__` key reached `Object.prototype`. They
are `Map`s now, which also drops the `[key: string]: any` index signature from
the `App` interface (CodeQL js/prototype-polluting-assignment).

`redirect_uri` is validated before a token is issued and the callback url is
built from the parsed `URL` rather than by concatenating the request value. A
real authorization server matches `redirect_uri` against the callback urls
registered for the client; this provider only ever serves test applications
running on the same machine, so it accepts loopback hosts
(CodeQL js/server-side-unvalidated-url-redirection).

The async route handlers are wrapped so that rejections reach Express instead
of becoming unhandled promise rejections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
Install TypeScript side by side, as recommended for TypeScript 7: `typescript`
is aliased to `@typescript/typescript6` so that tools consuming the JS compiler
API keep working, and `@typescript/native` provides the native compiler that
`lb-tsc` runs. A cold build of the monorepo drops from ~10.7s to ~2.1s.

`lb-tsc` prefers the `@typescript/native` installed by the project being built
over the one `@loopback/build` depends on. `lb-ttsc` keeps using the JS
compiler, since `ttypescript` patches the compiler API.

`tsconfig.common.json` changes for TypeScript 6/7:

- `moduleResolution: node` (node10) was removed. The config sets
  `moduleResolution: bundler`, the TypeScript 6 default, which resolves
  `exports` maps with the `require` and `types` conditions for
  `module: commonjs`.
- `@types/*` packages are no longer included automatically, so the config lists
  `types: ["node", "mocha"]`.

BREAKING CHANGE: `@loopback/build` depends on TypeScript 7
(`@typescript/native`) and compiles with it, and its `typescript` dependency
is TypeScript 6.
Projects extending `@loopback/build/config/tsconfig.common.json` have to remove
`moduleResolution: node`, and list ambient type packages other than `node` and
`mocha` in `types`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
Generated projects depend on TypeScript 7 (`@typescript/native`) and on
TypeScript 6 as `typescript`, and no longer set `moduleResolution: node`, which
TypeScript 7 removed.

The template's `mocha` moves to `^12` as well. `update-template-deps` syncs it
with the monorepo, which already uses mocha 12.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
Packages depend on TypeScript 6 as `typescript`, matching `@loopback/build`.

Source changes required by TypeScript 6/7:

- The default resolver for `module: commonjs` honors `exports` maps, so
  `@loopback/graphql` no longer imports `Middleware` from a deep `type-graphql`
  path and declares it instead. `GRAPHQL_CONTEXT_RESOLVER` gets an explicit
  type, since its inferred type is not portable (TS2883).
- Reading a parent class field through `super` is an error (TS2855), so
  `@loopback/cron` calls `fireOnTick` through `BaseCronJob.prototype`.
- Declaration emit no longer adds `/// <reference path>` for ambient files, so
  `@loopback/testlab` exports the should.js types from a module rather than a
  global declaration file. Without it, `expect` resolves to `any` in every
  consuming package and api-extractor cannot follow the `Internal` symbol.

With TypeScript 6 type information, typescript-eslint reports more findings:

- Async Express handlers in `@loopback/rest` and the rpc-server example pass
  rejections to `next` (`no-misused-promises`).
- `??` / `??=` replace `||` and `!x` guards on objects and arrays
  (`prefer-nullish-coalescing`). `||` is kept where the operand may be `''`.
- `transactions.suite.ts` awaits `disconnect()` (`no-floating-promises`).

Test defects surfaced by the stricter compiler are fixed as well:
`belongs-to-repository-factory.unit.ts` never assigned `companyRepo` and
clobbered `customerRepo` instead, and the todo-list repository tests used
repositories they never created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
@raymondfeng
raymondfeng force-pushed the upgrade-typescript-7 branch from 8915bc4 to f25e352 Compare October 2, 2026 05:03
@raymondfeng raymondfeng changed the title feat(build): compile with the TypeScript 7 native compiler feat(build)!: compile with the TypeScript 7 native compiler Oct 2, 2026
@raymondfeng

Copy link
Copy Markdown
Contributor Author

@samarpanB this PR is now split per your review:

PR Scope Release
#11810 fix(mock-oauth2-provider): CodeQL fixes, redirect_uri validated before a token is issued patch
#11811 feat(eslint-config)!: typescript-eslint v8, plus the lint fixes in separate commits major, eslint-config only
#11793 feat(build)!: TypeScript 7 (stacked on the two above) major, build only
#11812 fix(cli): the if (!gen) metadata bug patch

|| → ??: lint runs without strictNullChecks, so prefer-nullish-coalescing couldn't tell a string operand from a nullable one. On master it even flagged boolean || boolean in relation.helpers.ts. #11811 sets ignorePrimitives for strings, numbers and booleans, so only == null-style rewrites remain. Every rewrite where '' or 0 could change the result was reverted. That includes all the sites you listed, plus rpc-server (port: 0), http-caching-proxy (statusCode: 0), buffer.ts (encoding: '' would have thrown ERR_UNKNOWN_ENCODING), and the CLI app template's process.env.HOST, where an empty HOST would have bound to all interfaces.

Minor items:

  • try-catch-finally.unit.ts: the blank lines are removed (feat(eslint-config)!: upgrade typescript-eslint to v8 #11811).
  • graphql/keys.ts: commented. The annotation avoids TS2883, which I confirmed by building without it.
  • lb-ttsc: the README notes that it compiles with the JS compiler from typescript, not TypeScript 7. The old sentence claiming it falls back to tsc when ttypescript is missing was wrong (it exits with an error) and is removed.
  • Merge conflicts: resolved; everything is rebased on current master.

raymondfeng and others added 2 commits October 1, 2026 22:15
The US Census geocoder now returns slightly different coordinates for the test
address, so `GeoLookupService` and `TodoApplication` tests fail on every
platform, on master as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
The hook that launches puppeteer and loads the page intermittently exceeds
15 seconds on the ubuntu-latest runners. It now gets 30 seconds, the same as
the hook that generates the bundle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Raymond Feng <enjoyjava@gmail.com>
@raymondfeng raymondfeng mentioned this pull request Oct 2, 2026
4 of 8 tasks

This branch has not been deployed

No deployments
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.

4 participants