Skip to content

fix(tsconfig): stop four library tsconfigs from shadowing the workspace baseUrl - #1071

Merged
blove merged 1 commit into
mainfrom
blove/fix-library-baseurl-shadowing
Sep 8, 2026
Merged

fix(tsconfig): stop four library tsconfigs from shadowing the workspace baseUrl#1071
blove merged 1 commit into
mainfrom
blove/fix-library-baseurl-shadowing

Conversation

@blove

@blove blove commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

What changed

libs/cockpit-runtime-bridge, libs/growth, libs/example-layouts and libs/cockpit-registry each re-declared "baseUrl": "." in their own tsconfig.json while extending tsconfig.base.json and declaring no paths of their own. baseUrl outranks the implicit pathsBasePath, so every inherited substitution from the base paths map — which is workspace-root-relative, e.g. libs/design-tokens/src/index.ts — was probed under the library folder instead of the repo root. This PR removes those four baseUrl lines so the libraries inherit the base config's baseUrl (the repo root).

Why it matters (the evidence)

The build passes either way, so a trace is the only proof the change does something real. npx tsc --traceResolution on a probe file importing @threadplane/design-tokens from each library.

Before (identical shape in all four; example-layouts shown):

'baseUrl' option is set to '<root>/libs/example-layouts', using this value to resolve non-relative module name '@threadplane/design-tokens'.
'paths' option is specified, looking for a pattern to match module name '@threadplane/design-tokens'.
Module name '@threadplane/design-tokens', matched pattern '@threadplane/design-tokens'.
Trying substitution 'libs/design-tokens/src/index.ts', candidate module location: 'libs/design-tokens/src/index.ts'.
File '<root>/libs/example-layouts/libs/design-tokens/src/index.ts' does not exist.
Loading module '@threadplane/design-tokens' from 'node_modules' folder, target file types: TypeScript, JavaScript, Declaration, JSON.
File '<root>/node_modules/@threadplane/design-tokens/src/index.ts' exists - use it as a name resolution result.
Resolving real path for '<root>/node_modules/@threadplane/design-tokens/src/index.ts', result '<root>/libs/design-tokens/src/index.ts'.
======== Module name '@threadplane/design-tokens' was successfully resolved to '<root>/libs/design-tokens/src/index.ts' with Package ID '@threadplane/design-tokens/src/index.ts@0.0.35'. ========

The paths substitution missed. Resolution only landed on the right file because the npm workspace symlink under node_modules/@threadplane/ happens to point at the same source. The Package ID on the last line is the tell: TypeScript treated the library as an installed package, not a workspace path mapping.

After (again identical in all four):

'baseUrl' option is set to '<root>', using this value to resolve non-relative module name '@threadplane/design-tokens'.
Trying substitution 'libs/design-tokens/src/index.ts', candidate module location: 'libs/design-tokens/src/index.ts'.
File '<root>/libs/design-tokens/src/index.ts' exists - use it as a name resolution result.
======== Module name '@threadplane/design-tokens' was successfully resolved to '<root>/libs/design-tokens/src/index.ts'. ========

The substitution now hits on the first try, with no node_modules fallback and no Package ID.

What was deliberately left alone

  • tsconfig.base.json's own baseUrl — Nx's createTmpTsConfig (@nx/js/src/utils/buildable-libs-utils.js) writes generated build tsconfigs whose paths carry non-relative dist/libs/... entries. With no baseUrl anywhere in the chain TypeScript raises TS5090 and discards the entire paths map, breaking render:build:production and cockpit-shell:build. The existing comment there already explains this.
  • libs/growth-capture and apps/growth-research — both pair baseUrl with their own "paths": {}, so baseUrl is doing real directory-resolution work and shadows nothing.
  • apps/website — declares its own complete paths.
  • cockpit/** and examples/** e2e/python tsconfigs — each declares its own paths or does not extend the base.
  • apps/lifecycle/tsconfig.json — noted as out of scope, see below.

Guard spec

scripts/tsconfig-path-inheritance.spec.mjs (new, 5 tests) guards both halves of the fix:

  1. A structural check: walk every libs/*/tsconfig*.json, follow its extends chain, and fail any config that reaches tsconfig.base.json without declaring its own paths yet re-declares baseUrl.
  2. A behavioral check, one per library: parse the real tsconfig with ts.getParsedCommandLineOfConfigFile, call ts.resolveModuleName('@threadplane/design-tokens', …), and assert both that it lands on libs/design-tokens/src/index.ts and that resolvedModule.packageId is undefined — i.e. it came through the paths map, not the node_modules symlink.

Mutation-checked: restoring "baseUrl": "." in libs/growth/tsconfig.json fails both assertions.

× never shadows the workspace baseUrl the inherited paths map resolves against
× resolves @threadplane/design-tokens through the paths map from libs/growth
AssertionError: expected [ 'libs/growth/tsconfig.json' ] to deeply equal []
AssertionError: resolved via node_modules symlink, not the paths map: expected { …(4) } to be undefined
Tests  2 failed | 3 passed (5)

Verification

Lane Result
nx run-many -t lint,test,build across cockpit-runtime-bridge, growth, example-layouts, cockpit-registry, chat, ag-ui, langgraph, render, a2ui, telemetry, cockpit-shell, scripts Successfully ran targets for 12 projects and 7 dependent tasks (lint: 0 errors, warnings only)
nx run chat:type-tests, ag-ui:type-tests, langgraph:type-tests all three green
nx run render:build:production green (TS5090 regression check)
nx run cockpit-shell:build green (TS5090 regression check)
vitest run --root apps/website 141 files, 1408 tests passed
GROWTH_FORM_POLICY=growth_v1 nx build website green
nx run-many -t build --projects=examples-chat-angular,cockpit-chat-debug-angular,cockpit-render-repeat-loops-angular green

Follow-up noted, not taken

apps/lifecycle/tsconfig.json has the same shape — it extends apps/lifecycle/tsconfig.runtime-base.json, which extends tsconfig.base.json, and it declares "baseUrl": "." with no paths of its own. It was outside this task's scope, and nothing under apps/lifecycle currently imports a mapped @threadplane/* specifier, so the shadowing is inert there today. The new guard spec is scoped to libs/** and therefore does not cover it. Worth cleaning up separately, alongside a decision about widening the guard to apps/**.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 8, 2026 10:15pm UTC

Request Review

@github-actions github-actions Bot 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.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

…ce baseUrl

`libs/cockpit-runtime-bridge`, `libs/growth`, `libs/example-layouts` and
`libs/cockpit-registry` each re-declared `"baseUrl": "."` while extending
`tsconfig.base.json` and declaring no `paths` of their own. `baseUrl`
outranks the implicit `pathsBasePath`, so every inherited substitution
from the base `paths` map — which is workspace-root-relative, e.g.
`libs/design-tokens/src/index.ts` — was probed under the library folder
instead of the repo root.

`npx tsc --traceResolution` on a probe file in each library, before:

    'baseUrl' option is set to '<root>/libs/example-layouts', using this
      value to resolve non-relative module name '@threadplane/design-tokens'.
    Trying substitution 'libs/design-tokens/src/index.ts', candidate module
      location: 'libs/design-tokens/src/index.ts'.
    File '<root>/libs/example-layouts/libs/design-tokens/src/index.ts' does
      not exist.
    Loading module '@threadplane/design-tokens' from 'node_modules' folder…
    Module name '@threadplane/design-tokens' was successfully resolved to
      '<root>/libs/design-tokens/src/index.ts' with Package ID
      '@threadplane/design-tokens/src/index.ts@0.0.35'.

and after:

    'baseUrl' option is set to '<root>', using this value to resolve
      non-relative module name '@threadplane/design-tokens'.
    Trying substitution 'libs/design-tokens/src/index.ts', candidate module
      location: 'libs/design-tokens/src/index.ts'.
    File '<root>/libs/design-tokens/src/index.ts' exists - use it as a name
      resolution result.
    Module name '@threadplane/design-tokens' was successfully resolved to
      '<root>/libs/design-tokens/src/index.ts'.

The builds were green either way only because the npm workspace symlink
under `node_modules/@threadplane/*` happens to land on the same source
file; the `Package ID` in the before-trace is the tell that resolution
went through `node_modules` rather than the paths map.

`baseUrl` in `tsconfig.base.json` itself stays: Nx's `createTmpTsConfig`
writes build tsconfigs whose `paths` carry non-relative `dist/libs/...`
entries, and with no `baseUrl` in the chain TypeScript raises TS5090 and
discards the whole map. `libs/growth-capture` and `apps/growth-research`
pair `baseUrl` with their own `"paths": {}`, where it does real
directory-resolution work, and `apps/website` declares a complete `paths`
of its own — all left alone.

`scripts/tsconfig-path-inheritance.spec.mjs` guards both halves: a
structural check that no library tsconfig inheriting the base `paths`
re-declares `baseUrl`, and a resolution check through the TypeScript API
asserting `@threadplane/design-tokens` resolves from each of the four
libraries with no `packageId` — that is, through the paths map and not
the node_modules symlink. Both assertions fail when the `baseUrl` line is
put back.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@blove
blove force-pushed the blove/fix-library-baseurl-shadowing branch from 3c39667 to 14482e2 Compare September 8, 2026 22:12
@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 41900ad into main Sep 8, 2026
72 checks passed
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