@ultimat3/cli 27.16.1 → 29.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,16 @@
1
1
  // The workspace dependency graph, in the two forms that must agree: the edges every manifest
2
2
  // DECLARES, the edges every shipped source file actually IMPORTS, and the gate rule that the
3
- // second is a subset of the first. An edge that lives only in a tsconfig `paths` entry resolves
4
- // for `tsc` and is invisible to bun, to `--filter` ordering and to any change-detection tool.
3
+ // second is covered by the first, with no unused framework sibling declarations. A tsconfig-only
4
+ // edge resolves for `tsc` but is invisible to bun, `--filter` ordering and change-detection tools.
5
5
 
6
6
  import { join } from 'node:path';
7
7
  import { ERROR_DOCS_URL, maskLiterals } from '@ultimat3/core';
8
+ import { CATALOG_PACKAGES } from './error-catalog';
8
9
  import { scanAllImports } from './import-scan';
9
10
  import type { Finding } from './output';
10
- import { eachSourceFile, isGenerated, isTest, isVendored } from './source-files';
11
+ import { FIXTURE_SUFFIX } from './publish-closure';
12
+ import { quoteArg } from './shell-quote';
13
+ import { eachSourceFile, isGenerated, isTest, isVendored, SOURCE_GLOBS } from './source-files';
11
14
 
12
15
  export interface WorkspaceNode {
13
16
  /** package.json "name". */
@@ -176,7 +179,7 @@ export const undeclaredWorkspaceDepFinding = (
176
179
  cause: `${at} imports ${to.name}, which ${from.dir}/package.json does not declare`,
177
180
  // The exact line to paste, at the version the target really carries: a `workspace:*` range would
178
181
  // resolve and then fail `checkLockstep`, which compares a sibling pin against the version.
179
- fix: `add "${to.name}": "${to.version ?? '0.0.0'}" to "dependencies" in ${from.dir}/package.json`,
182
+ fix: `add "${to.name}": "${to.version ?? '0.0.0'}" to "${at.endsWith(FIXTURE_SUFFIX) ? 'devDependencies' : 'dependencies'}" in ${from.dir}/package.json`,
180
183
  docs: ERROR_DOCS_URL,
181
184
  at: `${from.dir}/package.json`,
182
185
  });
@@ -186,10 +189,9 @@ export const undeclaredWorkspaceDepFinding = (
186
189
  * declared none of it — the imports resolved through the root tsconfig's `paths`, so the edges
187
190
  * existed only inside `tsc` and every tool that asks what a change affects answered short.
188
191
  *
189
- * Shipped source only. A test file is excluded from the tarball by every package here
190
- * (`TEST_EXCLUSION`) and resolves its imports through the ROOT manifest's hoisted devDependencies,
191
- * which is why no `packages/*` in this repo carries a `devDependencies` block of its own —
192
- * judging test imports would demand 29 of them to describe a resolution that already works.
192
+ * Tests resolve through the root's hoisted devDependencies and are excluded here. Fixtures still
193
+ * need forward declarations, but their imports justify only development dependencies: they are
194
+ * excluded from tarballs and must not keep unused shipped dependency fields alive.
193
195
  *
194
196
  * One finding per (workspace, dependency) pair, never one per import site: the fix is one line in
195
197
  * one manifest, and repeating it per file is noise an author has to deduplicate by hand.
@@ -202,6 +204,8 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
202
204
  // Deepest directory first: a nested workspace's files are its own, never its parent's.
203
205
  const owners = [...graph].sort((left, right) => right.dir.length - left.dir.length);
204
206
  const reported = new Set<string>();
207
+ const used = new Set<string>();
208
+ const fixtureUsed = new Set<string>();
205
209
  for await (const path of eachSourceFile(root)) {
206
210
  if (isTest(path) || isGenerated(path)) continue;
207
211
  const owner = owners.find((node) => path.startsWith(`${node.dir}/`));
@@ -209,12 +213,55 @@ export async function checkWorkspaceDependencies(root: string): Promise<readonly
209
213
  for (const name of importedPackages(await Bun.file(join(root, path)).text(), path)) {
210
214
  const target = byName.get(name);
211
215
  if (target === undefined || target.name === owner.name) continue;
212
- if (owner.dependencies.includes(target.name)) continue;
213
216
  const pair = `${owner.dir} ${target.name}`;
217
+ // Fixtures still owe forward declarations, but never justify a shipped dependency.
218
+ (path.endsWith(FIXTURE_SUFFIX) ? fixtureUsed : used).add(pair);
219
+ if (owner.dependencies.includes(target.name)) continue;
214
220
  if (reported.has(pair)) continue;
215
221
  reported.add(pair);
216
222
  findings.push(undeclaredWorkspaceDepFinding(owner, target, path));
217
223
  }
218
224
  }
225
+ for (const owner of graph) {
226
+ // Only infer absence where the source contract scans. Match directory prefixes rather than
227
+ // observed files so a covered workspace with no source still owes removal of unused siblings.
228
+ const covered = SOURCE_GLOBS.some((pattern) => {
229
+ const directories = pattern.split('/').slice(0, -1);
230
+ return directories.some((_, index) =>
231
+ new Bun.Glob(directories.slice(0, index + 1).join('/')).match(owner.dir),
232
+ );
233
+ });
234
+ if (!covered || isVendored(owner.dir)) continue;
235
+ const unused = owner.dependencies.filter(
236
+ (name) =>
237
+ name.startsWith('@ultimat3/') &&
238
+ !used.has(`${owner.dir} ${name}`) &&
239
+ !(owner.name === '@ultimat3/cli' && CATALOG_PACKAGES.some((root) => root === name)),
240
+ );
241
+ if (unused.length === 0) continue;
242
+ const path = `${owner.dir}/package.json`;
243
+ const manifest = await readManifest(join(root, path));
244
+ for (const name of unused) {
245
+ const fields = DEPENDENCY_FIELDS.filter(
246
+ (field) =>
247
+ Object.hasOwn(asRecord(manifest?.[field]) ?? {}, name) &&
248
+ !(field === 'devDependencies' && fixtureUsed.has(`${owner.dir} ${name}`)),
249
+ );
250
+ if (fields.length === 0) continue;
251
+ // A package-wide remove would also delete a valid fixture-only development declaration.
252
+ const script =
253
+ 'const [path, name, fields] = Bun.argv.slice(-3); ' +
254
+ 'const manifest = await Bun.file(path).json(); ' +
255
+ 'for (const field of JSON.parse(fields)) delete manifest[field]?.[name]; ' +
256
+ 'await Bun.write(path, JSON.stringify(manifest, null, 2) + "\\n");';
257
+ findings.push({
258
+ code: 'X_PACKAGE_SHAPE',
259
+ cause: `${path} declares ${name} in ${fields.join(', ')}, but no eligible source in ${owner.dir} imports it`,
260
+ fix: `bun -e ${quoteArg(script)} -- ${quoteArg(path)} ${quoteArg(name)} ${quoteArg(JSON.stringify(fields))}`,
261
+ docs: ERROR_DOCS_URL,
262
+ at: path,
263
+ });
264
+ }
265
+ }
219
266
  return findings;
220
267
  }