Skip to content

Commit 390bd2a

Browse files
build(turbo): hash the Service Worker generator into the Console's build and type-check only (#29504)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbS1Z33XiLfwgAvkxHrZ2Diq)_ Fixes #29361 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:Service Worker 生成器 `ui/packages/mobile/src/serviceWorkerSource.ts` 不再挂在根 `turbo.json` 通用 `build` / `type-check` 的输入上,改挂到 Console 自己的 `ui/apps/console/turbo.json`(带 `$TURBO_EXTENDS$`,根输入照常继承)。通用 `test` 输入保留。ui 的两个 turbo 输入守卫改为按「turbo 对该包实际解析出的输入」判定,能读懂包级 `turbo.json`。 - **为什么改**:改一行生成器会让所有 `@object-ui/*` 包的 build/type-check 缓存失效,实际只有 Console 读它。总监在 PR #29326 合同复审里提的 should-fix。 - **风险与回滚**:只动缓存键,不动构建产物;不发布任何包,无 changeset;`ci.yml` 未动,`check:ci-filter-parity` 绿。回滚即 revert。 - **先看**:`ui/apps/console/turbo.json`(两条 inputs 必须带 `$TURBO_EXTENDS$`,否则 Console 的根输入全丢);`ui/scripts/__tests__/helpers/turbo-inputs.ts` 的 `packageTaskInputs`;`turbo-build-inputs.test.ts` 新增的逐包 `$TURBO_DEFAULT$` 断言。 - **两分钟验证**:`pnpm exec turbo run build type-check --filter=@object-ui/console --filter=@object-ui/core --dry=json`,给生成器末尾加一行注释再跑一次:Console 两个 hash 变,core 两个不变。 - **需要你做的**:无,席位落地。 ## What changes - Root `turbo.json`: the generator leaves the generic `build` and `type-check` inputs. The generic `test` input stays, because the root ui vitest config loads the Console's `vite.config.ts`. - `ui/apps/console/turbo.json`: `build` and `type-check` declare `["$TURBO_EXTENDS$", "$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts"]`, so the Console keeps every root input and adds the generator. - `ui/scripts/__tests__/helpers/turbo-inputs.ts` (ruling A, claim amendment 6581882409): `packageTaskInputs` resolves a package's task inputs the way turbo does. No `inputs` on the package task inherits the root list; a list holding `$TURBO_EXTENDS$` is the root list plus its entries; a list without it replaces the root list. A root `PACKAGE#TASK` override for the package throws, because the reader does not implement that shape. `packageTurboConfig` narrows the package config like the root one. - `turbo-build-inputs.test.ts` and `turbo-type-check-inputs.test.ts`: each package row is judged against its resolved list. The phantom-input and earned-input assertions also cover each package's own `$TURBO_ROOT$` entries, earned only by that package's program. The build suite gains a per-package assertion that the resolved list still carries `$TURBO_DEFAULT$`. No rule is loosened, no row exempted, no assertion deleted. - New `turbo-inputs-package-config.test.ts`: fixtures for each resolution, the throw cases, and live-tree pins (Console build/type-check hash the generator plus the root list; `@object-ui/core` build/type-check do not; every package's `test` still does). No changeset: nothing here ships. `@object-ui/console` publishes `dist`, `plugin.*` and `README.md` only; the rest is repo tooling. `skip-changeset` is set on this PR. ## Tests Hash proofs, `turbo run build type-check test --filter=@object-ui/console --filter=@object-ui/core --filter=@object-ui/app-shell --dry=json` (turbo 2.11.10), before and after appending a comment to the generator: <details><summary>main (6af3d29): every package's build and type-check move</summary> | task | before | after edit | |:--|:--|:--| | `@object-ui/core#build` | 341838eef8262e54 | 078a70f287b05963 | | `@object-ui/core#type-check` | b0e7109c12bb4885 | b968c1ef54b40528 | | `@object-ui/console#build` | 7af14fd740b2d419 | a23ad7dc8c5a851e | | `@object-ui/console#type-check` | 86aaf22d49bf1e02 | 3d185b303fac6359 | `@object-ui/core` does not depend on `@object-ui/mobile` (its build closure has 4 tasks, none of them mobile's), so its move came from the input alone. </details> <details><summary>this branch: only the Console's build and type-check move; every test still moves</summary> | task | before | after edit | |:--|:--|:--| | `@object-ui/core#build` | 6c35a1e42b0e1130 | 6c35a1e42b0e1130 (unchanged) | | `@object-ui/core#type-check` | 9cb5b96291a6bf06 | 9cb5b96291a6bf06 (unchanged) | | `@object-ui/core#test` | 14e60fabf5d09a61 | ce70e0cf375c9560 | | `@object-ui/console#build` | 94196d2f1d9d4d72 | 1c05c0f359ea0ac5 | | `@object-ui/console#type-check` | 514067ed9666bd87 | 1d9746d5003f1d35 | | `@object-ui/console#test` | ec433433ddf5f4d2 | 2b676d5bdf2fa74e | | `@object-ui/app-shell#test` | f52cfbe7090d54f6 | b669ea12779cae1f | `@object-ui/console#build` hashes 457 files, the same count as on main, with all 9 `ui/scripts/vite-*.ts` root inputs present. `@object-ui/app-shell#build` still moves because it depends on `@object-ui/mobile#build`, not through the input. </details> <details><summary>$TURBO_EXTENDS$ is load-bearing</summary> With the marker removed from `ui/apps/console/turbo.json`, `@object-ui/console#build` hashes 3 files (`$TURBO_DEFAULT$` and the root list are gone). With the marker written last instead of first it hashes the same 457. Restored from HEAD after each probe, `git diff HEAD` empty. </details> <details><summary>Reverse checks on the suites</summary> - Old reader (helper and both suites restored from 6af3d29) with the new turbo configs: exit 1, 2 failed / 82 passed, the `@object-ui/console` rows of both suites, each reading `serviceWorkerSource.ts` from outside `apps/console`. This is the red the ruling asked to teach away. - New reader, marker dropped from the Console config: exit 1, 5 failed (both Console rows, the Console `$TURBO_DEFAULT$` case, two live-tree pins). - New reader, Console inputs removed (generator hashed nowhere): exit 1, 4 failed (both Console rows, two live-tree pins). - Each leg restored with `git checkout HEAD --`; `git status --porcelain` empty after. </details> ## Acceptance notes - The other turbo-input suites (`turbo-test-inputs`, `turbo-lint-inputs`) still read the root list only. No package config declares `test` or `lint` today, so their verdicts are unchanged; they can take `packageTaskInputs` when one does. - `check:ci-filter-parity` reads root `turbo.json` only (its header names a package-level `turbo.json` as a known bound). The `core:` row for the generator stays, held by the generic `test` input. `ci.yml` is untouched. - An open PR, #29488, also edits root `turbo.json`, in a `@objectstack/*#test` task about 600 lines away from these hunks. No overlap. ## Verification On head 84365b1 (main merged at 6af3d29): - `pnpm check:ci-filter-parity`: exit 0 (self-test 138 assertions; 29 build inputs and 9 test inputs covered, no leftover). - `pnpm --dir ui exec vitest run scripts/` under the verify lock: exit 0, 170 files passed / 2 skipped, 4972 tests passed / 2 skipped. - `pnpm --dir ui run type-check:scripts`: exit 0. `eslint` on the four changed ui files: exit 0. `pnpm --dir ui run lint:root`: exit 0, no warning in these files. - `dispatch-gates --commands`: 31 derived. 27 exit 0. `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:sourcemap-no-sources-content` exit 3 (PREREQUISITE NOT MET, no `dist/` without a full `pnpm build`): NOT MEASURED locally, Build Core runs them on this PR. `--ran` reconciliation: 31 accounted, 27 run, 4 NOT-MEASURED, 0 UNRUN. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (not touched) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. (eslint on the changed ui files and `lint:root` locally; the repo-wide run is CI's) - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (publishes nothing; `skip-changeset`) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (not touched) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01QsR2jTkrepTAZhPsbF4oG6 --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jTkrepTAZhPsbF4oG6)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent c9aaee9 commit 390bd2a

6 files changed

Lines changed: 333 additions & 17 deletions

File tree

‎turbo.json‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@
2121
"$TURBO_ROOT$/ui/scripts/build-plugin-stylesheet.mjs",
2222
"$TURBO_ROOT$/ui/scripts/check-dist-completeness.mjs",
2323
"$TURBO_ROOT$/ui/scripts/invoked-as.mjs",
24-
"$TURBO_ROOT$/ui/scripts/vitest-invocation-guard.mjs",
25-
"$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts"
24+
"$TURBO_ROOT$/ui/scripts/vitest-invocation-guard.mjs"
2625
]
2726
},
2827
"test": {
@@ -71,8 +70,7 @@
7170
"$TURBO_ROOT$/ui/packages/cli/src/utils/issue-path.ts",
7271
"$TURBO_ROOT$/ui/packages/cli/src/utils/union-arm-diagnostics.ts",
7372
"$TURBO_ROOT$/ui/scripts/check-doc-component-types.mjs",
74-
"$TURBO_ROOT$/ui/scripts/markdown-fence-scan.mjs",
75-
"$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts"
73+
"$TURBO_ROOT$/ui/scripts/markdown-fence-scan.mjs"
7674
]
7775
},
7876
"lint": {

‎ui/apps/console/turbo.json‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,11 @@
1313
"!**/node_modules/**",
1414
"plugin.js",
1515
"plugin.d.ts"
16-
]
16+
],
17+
"inputs": ["$TURBO_EXTENDS$", "$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts"]
18+
},
19+
"type-check": {
20+
"inputs": ["$TURBO_EXTENDS$", "$TURBO_ROOT$/ui/packages/mobile/src/serviceWorkerSource.ts"]
1721
}
1822
}
1923
}

‎ui/scripts/__tests__/helpers/turbo-inputs.ts‎

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ import fs from 'node:fs';
2121
import path from 'node:path';
2222

2323
import { uiRoot } from '../../ui-root.mjs';
24-
import { uiScopedTurboConfig, workspaceGlobs } from '../../workspace-root.mjs';
24+
import {
25+
narrowTurboConfig,
26+
turboConfigPath,
27+
uiScopedTurboConfig,
28+
workspaceGlobs,
29+
} from '../../workspace-root.mjs';
2530

2631
/** Repo root: the objectui tree, standalone or as the monorepo's `ui/`. */
2732
export const repoRoot = uiRoot();
@@ -129,11 +134,109 @@ export function taskInputs(task: string, turbo: TurboTasks = liveTurboConfig()):
129134
* `$TURBO_ROOT$` exists at all.
130135
*/
131136
export function rootAnchoredInputs(task: string, turbo: TurboTasks = liveTurboConfig()): string[] {
132-
return taskInputs(task, turbo)
137+
return anchoredOnly(taskInputs(task, turbo));
138+
}
139+
140+
function anchoredOnly(inputs: readonly string[]): string[] {
141+
return inputs
133142
.filter((entry) => entry.startsWith('$TURBO_ROOT$/'))
134143
.map((entry) => entry.slice('$TURBO_ROOT$/'.length));
135144
}
136145

146+
// ── A package's own turbo.json (objectstack#29361) ───────────────────────────
147+
//
148+
// The root config is not the whole answer for a package that carries its own
149+
// `turbo.json` (a turbo Package Configuration, `extends: ["//"]`). For each
150+
// task it declares, turbo resolves that package's `inputs` like this:
151+
//
152+
// - no `inputs` key on the package's task: the root task's list, unchanged;
153+
// - an `inputs` list holding `$TURBO_EXTENDS$`: the root task's list plus the
154+
// package's other entries;
155+
// - an `inputs` list WITHOUT it: the package's entries REPLACE the root's,
156+
// `$TURBO_DEFAULT$` included.
157+
//
158+
// Measured with turbo 2.11.10 on `@object-ui/console#build` (`--dry=json`):
159+
// 457 hashed files with `["$TURBO_EXTENDS$", <one $TURBO_ROOT$ file>]`, the
160+
// same 457 with the marker written last instead of first, and 3 with the
161+
// marker dropped. A guard that read only the root list would wave a replacing
162+
// list through and would red an extending one, so the guards read the list
163+
// turbo resolves for the package they judge.
164+
165+
/** Turbo's marker: "the root task's list, plus these". */
166+
export const TURBO_EXTENDS = '$TURBO_EXTENDS$';
167+
168+
/**
169+
* A package's own `turbo.json`, narrowed to this tree like the root's
170+
* (`$TURBO_ROOT$/ui/x` read as `$TURBO_ROOT$/x`), or `null` when it has none.
171+
*/
172+
export function packageTurboConfig(pkgDir: string): TurboTasks | null {
173+
const file = path.join(pkgDir, 'turbo.json');
174+
if (!fs.existsSync(file)) return null;
175+
const config = JSON.parse(fs.readFileSync(file, 'utf8')) as TurboTasks;
176+
const prefix = path.relative(path.dirname(turboConfigPath(repoRoot)), repoRoot);
177+
return narrowTurboConfig(config, prefix ? `${prefix.split(path.sep).join('/')}/` : '');
178+
}
179+
180+
/**
181+
* The `inputs` turbo hashes ONE package's task from: the root task's list,
182+
* resolved through the package's own `turbo.json` as described above.
183+
*
184+
* A root `<package>#<task>` override for the package throws: turbo has a
185+
* resolution rule for it too, and this reader does not implement it. Reading
186+
* the generic task instead would judge the package against a list turbo does
187+
* not hash, so teach this function first.
188+
*/
189+
export function packageTaskInputs(
190+
task: string,
191+
pkgDir: string,
192+
turbo: TurboTasks = liveTurboConfig(),
193+
own: TurboTasks | null = packageTurboConfig(pkgDir),
194+
): string[] {
195+
const manifest = path.join(pkgDir, 'package.json');
196+
const name = fs.existsSync(manifest)
197+
? (JSON.parse(fs.readFileSync(manifest, 'utf8')) as { name?: string }).name
198+
: undefined;
199+
if (name && turbo.tasks?.[`${name}#${task}`]) {
200+
throw new Error(
201+
`turbo.json declares \`${name}#${task}\`, and packageTaskInputs() does not resolve a root ` +
202+
`<package>#<task> override. Teach it turbo's rule for that shape rather than reading the ` +
203+
`generic \`${task}\` task in its place.`,
204+
);
205+
}
206+
const declared = own?.tasks?.[task]?.inputs;
207+
if (declared === undefined) return taskInputs(task, turbo);
208+
if (declared.includes(TURBO_EXTENDS)) {
209+
return [...taskInputs(task, turbo), ...declared.filter((entry) => entry !== TURBO_EXTENDS)];
210+
}
211+
if (declared.length === 0) {
212+
throw new Error(`${rel(path.join(pkgDir, 'turbo.json'))} \`${task}\` declares an empty \`inputs\` list`);
213+
}
214+
return declared;
215+
}
216+
217+
/** `packageTaskInputs`, `$TURBO_ROOT$`-anchored entries only, as repo-relative globs. */
218+
export function packageRootAnchoredInputs(
219+
task: string,
220+
pkgDir: string,
221+
turbo: TurboTasks = liveTurboConfig(),
222+
own: TurboTasks | null = packageTurboConfig(pkgDir),
223+
): string[] {
224+
return anchoredOnly(packageTaskInputs(task, pkgDir, turbo, own));
225+
}
226+
227+
/**
228+
* The `$TURBO_ROOT$` entries a package's OWN `turbo.json` writes for a task,
229+
* as repo-relative globs: the population the phantom-input and earned-input
230+
* assertions hold to that package's program, beside the root task's list.
231+
*/
232+
export function packageOwnRootAnchoredInputs(
233+
task: string,
234+
pkgDir: string,
235+
own: TurboTasks | null = packageTurboConfig(pkgDir),
236+
): string[] {
237+
return anchoredOnly(own?.tasks?.[task]?.inputs ?? []);
238+
}
239+
137240
/**
138241
* A turbo input glob as a regular expression.
139242
*

‎ui/scripts/__tests__/turbo-build-inputs.test.ts‎

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ import {
55
globToRegExp,
66
packagesWithScript,
77
rel,
8+
packageOwnRootAnchoredInputs,
9+
packageRootAnchoredInputs,
10+
packageTaskInputs,
811
repoFilesOnDisk,
912
rootAnchoredInputs,
1013
taskInputs,
@@ -79,7 +82,14 @@ const INPUTS_NOT_DERIVABLE: ReadonlyMap<string, string> = new Map();
7982
// ── The derivation, computed once ───────────────────────────────────────────
8083

8184
const PACKAGES = packagesWithScript(TASK);
82-
const DERIVED = PACKAGES.map((pkg) => ({ ...pkg, program: buildProgramFor(pkg) }));
85+
const DERIVED = PACKAGES.map((pkg) => ({
86+
...pkg,
87+
program: buildProgramFor(pkg),
88+
// What turbo hashes THIS package's task from: the root task's list, resolved
89+
// through the package's own turbo.json when it has one (objectstack#29361).
90+
inputs: packageRootAnchoredInputs(TASK, pkg.dir),
91+
ownInputs: packageOwnRootAnchoredInputs(TASK, pkg.dir),
92+
}));
8393
const ROOT_INPUTS = rootAnchoredInputs(TASK);
8494
const MATCHERS = ROOT_INPUTS.map((glob) => ({ glob, re: globToRegExp(glob) }));
8595

@@ -105,17 +115,17 @@ describe('turbo `build` inputs cover every out-of-package file (objectui#4185)',
105115
(pkg) => [pkg.name, pkg] as const,
106116
),
107117
)('%s', (_name, pkg) => {
108-
const uncovered = pkg.program.outside.filter(
109-
(file) => !MATCHERS.some(({ re }) => re.test(file)),
110-
);
118+
const matchers = pkg.inputs.map((glob) => globToRegExp(glob));
119+
const uncovered = pkg.program.outside.filter((file) => !matchers.some((re) => re.test(file)));
111120
expect(
112121
uncovered,
113122
`${pkg.name}'s build program reads ${uncovered.join(', ')} from outside ${rel(pkg.dir)}, ` +
114-
`and turbo hashes ${TASK} from ${JSON.stringify(ROOT_INPUTS)} plus the package ` +
123+
`and turbo hashes its ${TASK} from ${JSON.stringify(pkg.inputs)} plus the package ` +
115124
`directory. Turbo will replay a stale verdict — and hand the stale dist/ to every task ` +
116125
`that depends on it — when ${uncovered.length === 1 ? 'that file changes' : 'those files change'}. ` +
117126
`Add ${uncovered.map((file) => `"$TURBO_ROOT$/${file}"`).join(', ')} to turbo.json's ` +
118-
`\`${TASK}\` inputs.`,
127+
`\`${TASK}\` inputs, or to the package's own turbo.json \`${TASK}\` inputs beside ` +
128+
`"$TURBO_EXTENDS$" when only this package reads it.`,
119129
).toEqual([]);
120130
});
121131

@@ -133,6 +143,16 @@ describe('turbo `build` inputs cover every out-of-package file (objectui#4185)',
133143
`hashing nothing while reading as coverage — fix the glob or delete the entry.`,
134144
).toBe(true);
135145
}
146+
for (const pkg of DERIVED) {
147+
for (const glob of pkg.ownInputs) {
148+
const re = globToRegExp(glob);
149+
expect(
150+
[...onDisk].some((file) => re.test(file)),
151+
`${rel(pkg.dir)}/turbo.json \`${TASK}\` input "$TURBO_ROOT$/${glob}" matches no file in ` +
152+
`the repo. It is hashing nothing while reading as coverage — fix the glob or delete the entry.`,
153+
).toBe(true);
154+
}
155+
}
136156
});
137157

138158
/**
@@ -151,6 +171,18 @@ describe('turbo `build` inputs cover every out-of-package file (objectui#4185)',
151171
`INPUTS_NOT_DERIVABLE.`,
152172
).not.toHaveLength(0);
153173
}
174+
// A package's own entry is earned by THAT package's program: it hashes
175+
// into no other package's task, so no other program can earn it.
176+
for (const pkg of DERIVED) {
177+
for (const glob of pkg.ownInputs) {
178+
const re = globToRegExp(glob);
179+
expect(
180+
pkg.program.outside.filter((file) => re.test(file)),
181+
`${rel(pkg.dir)}/turbo.json \`${TASK}\` input "$TURBO_ROOT$/${glob}" is not required by ` +
182+
`${pkg.name}'s build program any more. Delete it.`,
183+
).not.toHaveLength(0);
184+
}
185+
}
154186
});
155187

156188
/**
@@ -169,6 +201,24 @@ describe('turbo `build` inputs cover every out-of-package file (objectui#4185)',
169201
).toContain('$TURBO_DEFAULT$');
170202
});
171203

204+
/**
205+
* The same worry, one level down: a package's own turbo.json whose `build`
206+
* inputs leave out "$TURBO_EXTENDS$" REPLACES the root list, and with it
207+
* "$TURBO_DEFAULT$" (objectstack#29361 measured `@object-ui/console#build`
208+
* fall from 457 hashed files to 3).
209+
*/
210+
it.each(PACKAGES.map((pkg) => [pkg.name, pkg] as const))(
211+
"%s's resolved build inputs still carry $TURBO_DEFAULT$",
212+
(_name, pkg) => {
213+
expect(
214+
packageTaskInputs(TASK, pkg.dir),
215+
`${rel(pkg.dir)}/turbo.json replaces turbo.json's \`${TASK}\` inputs without ` +
216+
`"$TURBO_DEFAULT$", so the package's own source stops being hashed. Add ` +
217+
`"$TURBO_EXTENDS$" to its list.`,
218+
).toContain('$TURBO_DEFAULT$');
219+
},
220+
);
221+
172222
/**
173223
* A `pnpm --filter <pkg> build` segment inside a `prebuild` is a task-GRAPH
174224
* fact, not an inputs fact: the right answer to "this build needs that
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import { afterAll, describe, expect, it } from 'vitest';
2+
import fs from 'node:fs';
3+
import os from 'node:os';
4+
import path from 'node:path';
5+
import {
6+
TURBO_EXTENDS,
7+
packageOwnRootAnchoredInputs,
8+
packageRootAnchoredInputs,
9+
packageTaskInputs,
10+
repoRoot,
11+
type TurboTasks,
12+
} from './helpers/turbo-inputs';
13+
14+
/**
15+
* objectstack#29361 — the turbo-input guards read the inputs turbo resolves
16+
* for the package a row judges, not the root task's list alone. These pin the
17+
* reader against fixtures for each of turbo's three resolutions, and against
18+
* the live tree for the one package that carries its own turbo.json today.
19+
*/
20+
21+
const ROOT: TurboTasks = {
22+
tasks: {
23+
build: { inputs: ['$TURBO_DEFAULT$', '$TURBO_ROOT$/scripts/shared.ts'] },
24+
'type-check': { inputs: ['$TURBO_DEFAULT$', '$TURBO_ROOT$/tsconfig.json'] },
25+
},
26+
};
27+
const OWN_ONLY = '$TURBO_ROOT$/packages/other/src/only-here.ts';
28+
29+
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'turbo-inputs-package-config-'));
30+
afterAll(() => fs.rmSync(scratch, { recursive: true, force: true }));
31+
32+
/** A scratch package directory holding a `package.json` with this name. */
33+
function scratchPackage(name: string): string {
34+
const dir = fs.mkdtempSync(path.join(scratch, 'pkg-'));
35+
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name }));
36+
return dir;
37+
}
38+
39+
describe('packageTaskInputs: turbo resolves a package task through its own turbo.json', () => {
40+
it('a package with no turbo.json reads the root task list unchanged', () => {
41+
const dir = scratchPackage('@fixture/plain');
42+
expect(packageTaskInputs('build', dir, ROOT, null)).toEqual(ROOT.tasks!.build.inputs);
43+
});
44+
45+
it('a package task that declares no inputs inherits the root task list', () => {
46+
const dir = scratchPackage('@fixture/outputs-only');
47+
const own: TurboTasks = { tasks: { build: {} } };
48+
expect(packageTaskInputs('build', dir, ROOT, own)).toEqual(ROOT.tasks!.build.inputs);
49+
});
50+
51+
it('an input list holding $TURBO_EXTENDS$ is the root list plus the package entries', () => {
52+
const dir = scratchPackage('@fixture/extends');
53+
const own: TurboTasks = { tasks: { build: { inputs: [TURBO_EXTENDS, OWN_ONLY] } } };
54+
expect(packageTaskInputs('build', dir, ROOT, own)).toEqual([
55+
'$TURBO_DEFAULT$',
56+
'$TURBO_ROOT$/scripts/shared.ts',
57+
OWN_ONLY,
58+
]);
59+
expect(packageRootAnchoredInputs('build', dir, ROOT, own)).toEqual([
60+
'scripts/shared.ts',
61+
'packages/other/src/only-here.ts',
62+
]);
63+
expect(packageOwnRootAnchoredInputs('build', dir, own)).toEqual(['packages/other/src/only-here.ts']);
64+
});
65+
66+
it('the marker extends wherever it sits in the list, as turbo reads it', () => {
67+
const dir = scratchPackage('@fixture/extends-last');
68+
const own: TurboTasks = { tasks: { build: { inputs: [OWN_ONLY, TURBO_EXTENDS] } } };
69+
expect(packageRootAnchoredInputs('build', dir, ROOT, own)).toEqual([
70+
'scripts/shared.ts',
71+
'packages/other/src/only-here.ts',
72+
]);
73+
});
74+
75+
it('an input list without the marker replaces the root list: the root entries stop counting', () => {
76+
const dir = scratchPackage('@fixture/replaces');
77+
const own: TurboTasks = { tasks: { build: { inputs: [OWN_ONLY] } } };
78+
expect(packageTaskInputs('build', dir, ROOT, own)).toEqual([OWN_ONLY]);
79+
expect(packageRootAnchoredInputs('build', dir, ROOT, own)).not.toContain('scripts/shared.ts');
80+
});
81+
82+
it("one package's turbo.json leaves another package's row and its other tasks alone", () => {
83+
const own: TurboTasks = { tasks: { build: { inputs: [TURBO_EXTENDS, OWN_ONLY] } } };
84+
const withOwn = scratchPackage('@fixture/with-own');
85+
const other = scratchPackage('@fixture/other');
86+
expect(packageRootAnchoredInputs('build', other, ROOT, null)).toEqual(['scripts/shared.ts']);
87+
expect(packageRootAnchoredInputs('type-check', withOwn, ROOT, own)).toEqual(['tsconfig.json']);
88+
});
89+
90+
it('an empty replacing list throws instead of reading as a list that covers nothing', () => {
91+
const dir = scratchPackage('@fixture/empty');
92+
const own: TurboTasks = { tasks: { build: { inputs: [] } } };
93+
expect(() => packageTaskInputs('build', dir, ROOT, own)).toThrow(/empty `inputs` list/);
94+
});
95+
96+
it('a root <package>#<task> override throws: the reader does not resolve that shape', () => {
97+
const dir = scratchPackage('@fixture/overridden');
98+
const root: TurboTasks = {
99+
tasks: { ...ROOT.tasks, '@fixture/overridden#build': { inputs: ['$TURBO_DEFAULT$'] } },
100+
};
101+
expect(() => packageTaskInputs('build', dir, root, null)).toThrow(/does not resolve a root/);
102+
});
103+
});
104+
105+
describe('the live tree: the Service Worker generator hashes into the Console tasks only', () => {
106+
const GENERATOR = 'packages/mobile/src/serviceWorkerSource.ts';
107+
const consoleDir = path.join(repoRoot, 'apps/console');
108+
const coreDir = path.join(repoRoot, 'packages/core');
109+
110+
it.each(['build', 'type-check'])(
111+
"@object-ui/console's %s reads the root list plus the generator, narrowed to this tree",
112+
(task) => {
113+
const resolved = packageRootAnchoredInputs(task, consoleDir);
114+
expect(resolved).toContain(GENERATOR);
115+
expect(resolved).toContain('scripts/vite-*.ts');
116+
expect(packageTaskInputs(task, consoleDir)).toContain('$TURBO_DEFAULT$');
117+
},
118+
);
119+
120+
it.each(['build', 'type-check'])("another package's %s does not hash the generator", (task) => {
121+
expect(packageRootAnchoredInputs(task, coreDir)).not.toContain(GENERATOR);
122+
});
123+
124+
it("every package's test still hashes it: the root ui vitest config loads the Console's vite.config.ts", () => {
125+
expect(packageRootAnchoredInputs('test', coreDir)).toContain(GENERATOR);
126+
expect(packageRootAnchoredInputs('test', consoleDir)).toContain(GENERATOR);
127+
});
128+
});

0 commit comments

Comments
 (0)