-
Notifications
You must be signed in to change notification settings - Fork 6
fix(blocks-react): let the build system own declaration freshness #669
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,71 +1,81 @@ | ||
| import { execFileSync } from "node:child_process"; | ||
| import { dirname, join } from "node:path"; | ||
| import { existsSync, readFileSync } from "node:fs"; | ||
| import { dirname, join, resolve } from "node:path"; | ||
| import { fileURLToPath } from "node:url"; | ||
|
|
||
| /** | ||
| * Build this package before any suite is COLLECTED. | ||
| * Refuse to start when this package's declarations are missing, rather than | ||
| * building them. | ||
| * | ||
| * Several suites here assert against the BUILT declarations rather than the | ||
| * source, because the questions they ask — what an entry exports, what a | ||
| * consumer can resolve — have answers only in the emitted artifact. A `dist` | ||
| * that is absent fails them at import time and a `dist` that merely predates | ||
| * the source certifies a surface nobody has. | ||
| * consumer can resolve — have answers only in the emitted artifact. So a `dist` | ||
| * that is absent has to stop the run. | ||
| * | ||
| * **Global setup, because collection is concurrent.** Sibling suites import | ||
| * this package while any one suite's own hook would still be building, so on a | ||
| * tree with no `dist` they fail to resolve before that build finishes. Only a | ||
| * stage that completes before collection starts covers them. | ||
| * **Stopping is all it does, and that is the whole design.** Freshness belongs | ||
| * to the build system: `turbo.json` makes `test` depend on this package's own | ||
| * `build` and names `dist/**` among its inputs, so an edit to anything the | ||
| * declarations are emitted from rebuilds and re-runs. `pnpm test`, `turbo run | ||
| * test` and CI all arrive through that edge with the artifact already current. | ||
| * What is checked here is the path that bypasses it — a direct `vitest`, or | ||
| * `pnpm --filter @nextlyhq/blocks-react test`, which runs the package script | ||
| * without turbo's graph. | ||
| * | ||
| * It runs ONCE per process, which is the whole of what it guarantees: the | ||
| * declarations are current when the run begins. Keeping them current as inputs | ||
| * change is Turbo's job — `test` depends on `build` and names `dist/**` among | ||
| * its inputs — and a `--watch` session is explicitly outside both, because | ||
| * Vitest selects suites from a module graph that cannot see a `.d.ts` read off | ||
| * disk. | ||
| * Building here as well looked like belt-and-braces and was not. `turbo run | ||
| * build --filter=@nextlyhq/blocks-react` carries the `^build` edge, so it | ||
| * rebuilds this package's whole dependency tree — the engine, the adapters, | ||
| * `nextly` — and each of those bundles with `clean: true`, deleting its `dist` | ||
| * before emitting. Run from global setup, that happens DURING collection, while | ||
| * sibling packages' suites are already importing from those same directories. | ||
| * A concurrent run then fails in whichever package was mid-import, with a | ||
| * missing chunk in code nobody changed. One package's convenience cannot be | ||
| * bought by deleting another package's build output underneath it. | ||
| * | ||
| * Unconditional, with no attempt to detect a build that already happened. The | ||
| * signals available say a Turbo task is running, never that it was one | ||
| * carrying this package's build edge, and a precondition derived from a nearby | ||
| * signal is wrong silently. Where the build did already run this is a cache | ||
| * hit costing milliseconds. | ||
| * **Absent is checked; STALE is not, and cannot usefully be.** A `dist` older | ||
| * than the source it describes would pass here and let the suites read an | ||
| * artifact that agrees with itself while the source has moved. Modification | ||
| * times cannot close that: Turbo restores a cached `dist` with the timestamp of | ||
| * the restore, so any later touch of a source file — a checkout, a branch | ||
| * switch, a formatter — makes the source newer than an artifact whose content is | ||
| * current. Measured on this package: a cache-restored `dist/index.d.ts` at | ||
| * `…57.173` against an unchanged `src/index.ts` at `…57.691`. A timestamp guard | ||
| * would refuse to run on a correct tree, which is a worse failure than the rare | ||
| * one it prevents, and deciding freshness exactly means re-implementing Turbo's | ||
| * hashing. Turbo stays the authority, through the `test` -> `build` edge. | ||
| * | ||
| * Through Turbo rather than `tsup` directly: the declaration build resolves | ||
| * `@nextlyhq/blocks-engine`, so invoking the bundler on a tree where that | ||
| * dependency has not been built fails before any declarations exist. `turbo | ||
| * run build` carries the `^build` edge, so the dependency is built first. | ||
| * The entries are read from `package.json` rather than listed here, so this | ||
| * checks exactly what the suites resolve and cannot drift from it as entries | ||
| * are added. | ||
| */ | ||
| export default function setup(): void { | ||
| const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); | ||
| const packageRoot = dirname(fileURLToPath(import.meta.url)); | ||
| const manifest = JSON.parse( | ||
| readFileSync(join(packageRoot, "package.json"), "utf8") | ||
| ) as { name?: string; exports?: Record<string, { types?: string }> }; | ||
| const name = manifest.name ?? "@nextlyhq/blocks-react"; | ||
| // Scoped through `turbo run` directly rather than the root `build` script, | ||
| // which already carries `--filter=./packages/*`. Turbo's filters are | ||
| // ADDITIVE, so `pnpm build --filter <name>` selects 23 package builds rather | ||
| // than this one's subtree — advice that would clean unrelated packages' `dist` | ||
| // is the behaviour this file exists to stop recommending. | ||
| const recovery = `pnpm exec turbo run build --filter=${name}`; | ||
|
|
||
| try { | ||
| execFileSync( | ||
| "pnpm", | ||
| ["exec", "turbo", "run", "build", "--filter=@nextlyhq/blocks-react"], | ||
| { | ||
| cwd: repoRoot, | ||
| stdio: "pipe", | ||
| // Windows resolves package-manager shims via the shell: Corepack | ||
| // installs `pnpm.cmd`, which cannot be launched directly. Matches how | ||
| // the CLI spawns a package manager in `cli/commands/add.ts`. | ||
| shell: process.platform === "win32", | ||
| } | ||
| ); | ||
| } catch (error) { | ||
| // Compiler diagnostics are the only thing that makes a failure here | ||
| // actionable, and a discarded stream reduces every cause to the same | ||
| // opaque non-zero exit. | ||
| const output = `${streamText(error, "stdout")}${streamText(error, "stderr")}`; | ||
| const declarations = Object.entries(manifest.exports ?? {}) | ||
| .map(([subpath, entry]) => ({ subpath, types: entry?.types })) | ||
| .filter( | ||
| (entry): entry is { subpath: string; types: string } => | ||
| typeof entry.types === "string" | ||
| ) | ||
| .map(entry => ({ ...entry, file: resolve(packageRoot, entry.types) })); | ||
|
|
||
| const missing = declarations | ||
| .filter(entry => !existsSync(entry.file)) | ||
|
mobeenabdullah marked this conversation as resolved.
|
||
| .map(entry => `${entry.subpath} (${entry.types})`); | ||
| if (missing.length > 0) { | ||
| throw new Error( | ||
| `building @nextlyhq/blocks-react failed:\n${output.trim()}` | ||
| `${name} has no built declarations for ${missing.join(", ")}.\n` + | ||
| `Several suites here assert against the emitted artifact, so there is ` + | ||
| `nothing for them to read.\n` + | ||
| `Run \`${recovery}\`, or use \`pnpm test\` / \`turbo run test\`, which build first.` | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| /** `stdout`/`stderr` off a failed `execFileSync`, without assuming its shape. */ | ||
| function streamText(error: unknown, key: "stdout" | "stderr"): string { | ||
| if (typeof error !== "object" || error === null || !(key in error)) return ""; | ||
| const value: unknown = Reflect.get(error, key); | ||
| if (typeof value === "string") return value; | ||
| if (value instanceof Uint8Array) return Buffer.from(value).toString("utf8"); | ||
| return ""; | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.