Skip to Content
Diagnose a Stale TypeScript Project-Reference Build
Advanced
0 likes
Tutorials

Diagnose a Stale TypeScript Project-Reference Build

Reproduce a project-only build that consumes stale shared declarations, rebuild the graph, and verify that an API change reaches its consumer.

A successful app-only TypeScript build can still be checking yesterday's shared API. With package imports and project references, the app can consume the last emitted declaration file while the shared source has already changed. A green exit code from that one project does not prove the whole build graph is current.

This guide reproduces that stale-declaration case, rebuilds the graph, then fixes the consumer error it reveals. For the architecture and first workspace setup, use the project-reference course lesson. Here the useful evidence is the compiler invocation, emitted declaration and changed consumer check.

Inspect the failed build before cleaning it

In an existing project, record which tsconfig the command selects, whether it uses -p or -b, and whether the shared package's types entry points at emitted declarations. Run build mode with --verbose to see project order and out-of-date reasons. Do this before deleting build outputs; those outputs can explain the apparent success.

The controlled fixture below uses Node.js with npm available, TypeScript 7.0.2, two npm workspace packages and NodeNext module handling. Run it in a new disposable directory, outside an existing project. Package installation needs registry access; nothing is installed globally.

Create the diagnostic fixture

Save this as setup-fixture.cjs in that new directory. It writes the listed project files and refuses to overwrite an existing target. The root references the app, and the app references shared, so build mode can traverse the dependency chain.

const fs = require("node:fs");
const path = require("node:path");

const options = {
  target: "ES2022", module: "NodeNext", strict: true,
  composite: true, noEmitOnError: true, rootDir: "src", outDir: "dist"
};
const files = {
  "package.json": {
    name: "reference-diagnostic", private: true,
    workspaces: ["packages/*"], devDependencies: { typescript: "7.0.2" }
  },
  "tsconfig.json": { files: [], references: [{ path: "packages/app" }] },
  "packages/shared/package.json": {
    name: "@ref-lab/shared", version: "1.0.0", private: true,
    type: "commonjs", main: "dist/index.js", types: "dist/index.d.ts"
  },
  "packages/shared/tsconfig.json": { compilerOptions: options, include: ["src"] },
  "packages/shared/src/index.ts":
    'export function makeLabel(id: number): string { return "Draft " + id; }\n',
  "packages/app/package.json": {
    name: "@ref-lab/app", version: "1.0.0", private: true,
    type: "commonjs", dependencies: { "@ref-lab/shared": "1.0.0" }
  },
  "packages/app/tsconfig.json": {
    compilerOptions: options, include: ["src"], references: [{ path: "../shared" }]
  },
  "packages/app/src/main.ts":
    'import { makeLabel } from "@ref-lab/shared";\nconsole.log(makeLabel(7));\n'
};
for (const [name, value] of Object.entries(files)) {
  fs.mkdirSync(path.dirname(name), { recursive: true });
  fs.writeFileSync(name, typeof value === "string" ? value : JSON.stringify(value, null, 2), { flag: "wx" });
}
console.log("Created the disposable reference diagnostic fixture.");
node setup-fixture.cjs
npm install --no-audit --no-fund --ignore-scripts
npx tsc --version

Confirm version 7.0.2 for a matching run. The shared package exposes dist/index.d.ts as its types entry and dist/index.js at runtime. Both project configs keep source and output separate and enable composite builds. The root's empty files list makes it a graph entry point.

Recognize missing declarations on a clean checkout

npx tsc -p packages/app

Before shared has been built, expect TS2307 for @ref-lab/shared: its declared output does not exist yet. In this fixture, a project-only build does not build that dependency. Repair the graph build, then run the emitted app:

npx tsc -b --verbose
if ($LASTEXITCODE -ne 0) { throw 'Graph build failed.' }
node packages/app/dist/main.js

Expect Draft 7. The verbose build lists shared before app. A missing module can also have other causes; this result identifies missing generated declarations in this specific workspace rather than assuming every TS2307 means a missing npm dependency.

Reproduce the stale API that still appears to pass

Replace packages/shared/src/index.ts with this changed API. Leave the app's numeric call unchanged:

export function makeLabel(id: string): string {
  return id.toUpperCase();
}
npx tsc -p packages/app
Get-Content packages/shared/dist/index.d.ts
node packages/app/dist/main.js

The app-only check still passes in the tested fixture. The declaration still accepts a number, and running the old emitted JavaScript still prints Draft 7. You have checked and executed old shared output, not validated the newly edited API.

Build the graph and repair the revealed consumer

npx tsc -b --verbose

Now shared is rebuilt before app. Expect TS2345 at the numeric call: a number is not assignable to the new string parameter. Inspect packages/shared/dist/index.d.ts again; it now declares id: string. That change connects the source edit to the consumer error.

Replace packages/app/src/main.ts with:

import { makeLabel } from "@ref-lab/shared";
console.log(makeLabel("draft-7"));
npx tsc -b --verbose
if ($LASTEXITCODE -ne 0) { throw 'Do not run old output after a failed build.' }
node packages/app/dist/main.js
npx tsc -b --verbose

Expect DRAFT-7, then an up-to-date report on the repeated build. Gate execution on compiler success. A failed build can leave earlier JavaScript files on disk, even with noEmitOnError, so the mere presence of an output file is not proof of a successful current build.

Check the cause before forcing a rebuild

SymptomEvidence to inspect
Missing shared moduleWorkspace link, types path and emitted declarations
App passes after shared API changesWhether the command built references and refreshed the declaration
Runtime output looks oldCompiler exit, emitted JavaScript and actual launched file
Unexpected rebuild orderRoot/app references and verbose build graph

Project-only mode is useful when dependencies have already been built deliberately. Build mode is the appropriate entry point for this graph. An editor can show source information or an in-memory declaration view that differs from the files consumed by a terminal build; inspect the actual emitted file before assuming those tools checked the same state.

Try a clean-output failure

In this disposable fixture only, run npx tsc -b --clean, then try the app-only command again. Should the fixed string call prevent a missing-module error?

Answer: no. The API call is fixed, but the generated shared declaration is absent again. Build the graph with npx tsc -b and run the app after success; DRAFT-7 returns. Fixing source types and building the necessary artifacts are separate checks.

Reference: TypeScript project references and build mode.

About

Reproduce a project-only build that consumes stale shared declarations, rebuild the graph, and verify that an API change reaches its consumer.

Responsible
Mary Q
Last Update
10/16/2026
Reading Time
6 min
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.