@bobfrankston/npmglobalize 1.0.218 → 1.0.219

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.
Files changed (4) hide show
  1. package/README.md +1 -1
  2. package/lib.d.ts +15 -6
  3. package/lib.js +75 -63
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -756,7 +756,7 @@ As with import maps, these have historically been built only by a second `.vscod
756
756
  Before building, `npmglobalize` looks for sub-projects, in this order:
757
757
 
758
758
  1. `.vscode/tasks.json` has a task that runs `tsc` or `importgen` with `"options": { "cwd": "${workspaceFolder}/client" }`, or runs `tsc` with `-p`/`--project` naming a sub-directory (authoritative — it's how the project is actually built today; the task's `label` is quoted back in the prompt).
759
- 2. An immediate sub-directory containing its own `package.json` build script or `tsconfig.json`. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
759
+ 2. An immediate sub-directory containing its own `package.json` or `tsconfig.json`. A sub-package without a build script gets one set up the same way the root does (importgen, `tsc`, its own sub-projects) before the root delegates to it. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
760
760
 
761
761
  Nothing is detected when the root `tsconfig.json` uses project `references` — that build graph belongs to `tsc -b` and isn't second-guessed.
762
762
 
package/lib.d.ts CHANGED
@@ -430,7 +430,7 @@ export declare function ensureImportgenInBuild(cwd: string): Promise<void>;
430
430
  /** A sub-directory the root build must build, and how. */
431
431
  interface SubProject {
432
432
  dir: string; /** Relative directory with its on-disk casing */
433
- kind: 'package' | 'tsconfig'; /** package: delegate to its build script; tsconfig: `tsc -p` */
433
+ kind: 'package' | 'tsconfig'; /** package: has its own package.json, delegate to its build script; tsconfig: `tsc -p` */
434
434
  reason: string; /** The signal that found it, quoted in prompts */
435
435
  }
436
436
  /** Sub-directories the root build must build itself. A package can hold more
@@ -452,11 +452,20 @@ export declare function detectSubProjects(cwd: string): SubProject[];
452
452
  * own idea of what to compile. Declining records `subProjects: false` in
453
453
  * .globalize.json5. */
454
454
  export declare function ensureSubProjectsInBuild(cwd: string): Promise<void>;
455
- /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
456
- * TypeScript project lacks a build script, run `npm run build`, record
457
- * failures. Returns true if build succeeded (or was skipped because no
458
- * tsconfig / noEmit / no build script after declining the prompt / output
459
- * already up to date). Pass `forceBuild` to skip the freshness check. */
455
+ /** Make sure `cwd`'s package.json has the build script it needs: importgen
456
+ * for an importgen project, `tsc` for a TypeScript project (prompted), and a
457
+ * step per sub-project. Returns the package plus whether the script runs
458
+ * importgen, or null when there is nothing to build here (no package.json, no
459
+ * tsconfig or `noEmit` and no importgen, or the user declined the prompt). */
460
+ export declare function ensureBuildScript(cwd: string): Promise<{
461
+ pkg: any;
462
+ runsImportgen: boolean;
463
+ } | null>;
464
+ /** Build a single project: set up its build script (see `ensureBuildScript`),
465
+ * run `npm run build`, record failures. Returns true if build succeeded (or
466
+ * was skipped because there was nothing to build / the user declined the
467
+ * build-script prompt / output already up to date). Pass `forceBuild` to
468
+ * skip the freshness check. */
460
469
  export declare function buildProject(cwd: string, opts?: {
461
470
  verbose?: boolean;
462
471
  force?: boolean;
package/lib.js CHANGED
@@ -3198,17 +3198,12 @@ function resolveOnDiskCase(base, rel) {
3198
3198
  }
3199
3199
  return out.length ? out.join('/') : null;
3200
3200
  }
3201
- /** Classify `rel` under `cwd`: a package with its own build script, a plain
3202
- * tsconfig sub-project, or null when it's neither. */
3201
+ /** Classify `rel` under `cwd`: a package (its own package.json its build
3202
+ * script is set up by `ensureBuildScript` if it lacks one), a plain tsconfig
3203
+ * sub-project, or null when it's neither. */
3203
3204
  function subProjectKind(cwd, rel) {
3204
- try {
3205
- const pkg = readPackageJson(path.join(cwd, rel));
3206
- if (typeof pkg?.scripts?.build === 'string' && pkg.scripts.build.trim())
3207
- return 'package';
3208
- }
3209
- catch {
3210
- // No package.json there — fall through to the tsconfig test
3211
- }
3205
+ if (fs.existsSync(path.join(cwd, rel, 'package.json')))
3206
+ return 'package';
3212
3207
  return fs.existsSync(path.join(cwd, rel, 'tsconfig.json')) ? 'tsconfig' : null;
3213
3208
  }
3214
3209
  /** The command the root build script runs for a sub-project. */
@@ -3303,7 +3298,7 @@ export function detectSubProjects(cwd) {
3303
3298
  continue;
3304
3299
  const kind = subProjectKind(cwd, e.name);
3305
3300
  if (kind)
3306
- add(e.name, kind === 'package' ? `${e.name}/package.json has a build script` : `${e.name}/tsconfig.json`);
3301
+ add(e.name, kind === 'package' ? `${e.name}/package.json` : `${e.name}/tsconfig.json`);
3307
3302
  }
3308
3303
  }
3309
3304
  catch {
@@ -3328,6 +3323,20 @@ function configuredSubProjects(cwd, config) {
3328
3323
  }
3329
3324
  return detectSubProjects(cwd);
3330
3325
  }
3326
+ /** The sub-projects the root build should run, each ready to be run: a
3327
+ * sub-package gets its own build script set up first (importgen, tsc, its own
3328
+ * sub-projects — the same setup the root gets), and one that still has
3329
+ * nothing to build afterwards is left out, since delegating to it would only
3330
+ * fail with "missing script: build". */
3331
+ async function readySubProjects(cwd, config) {
3332
+ const ready = [];
3333
+ for (const s of configuredSubProjects(cwd, config)) {
3334
+ if (s.kind === 'package' && !(await ensureBuildScript(path.join(cwd, s.dir))))
3335
+ continue;
3336
+ ready.push(s);
3337
+ }
3338
+ return ready;
3339
+ }
3331
3340
  /** Quote a path for a package.json script only when it needs it. */
3332
3341
  function quoteScriptPath(p) {
3333
3342
  return /[\s&|<>]/.test(p) ? `"${p}"` : p;
@@ -3351,14 +3360,8 @@ export async function ensureSubProjectsInBuild(cwd) {
3351
3360
  const buildScript = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
3352
3361
  if (!buildScript || !scriptRunsTsc(buildScript))
3353
3362
  return;
3354
- const subs = configuredSubProjects(cwd, config);
3355
- // A sub-package owns its own import map: give its build script the same
3356
- // importgen check the root gets, so delegating to it is enough.
3357
- for (const s of subs) {
3358
- if (s.kind === 'package')
3359
- await ensureImportgenInBuild(path.join(cwd, s.dir));
3360
- }
3361
- const missing = subs.filter(s => !scriptBuildsSubProject(buildScript, s.dir));
3363
+ const missing = (await readySubProjects(cwd, config))
3364
+ .filter(s => !scriptBuildsSubProject(buildScript, s.dir));
3362
3365
  if (missing.length === 0)
3363
3366
  return;
3364
3367
  const name = pkg.name || path.basename(cwd);
@@ -3367,7 +3370,7 @@ export async function ensureSubProjectsInBuild(cwd) {
3367
3370
  // — a bare nested tsconfig (tests, examples) is a weaker signal, and this
3368
3371
  // prompt rewrites a build script that currently works.
3369
3372
  const strong = missing.every(s => s.reason.startsWith('.vscode/tasks.json') || s.reason.startsWith('listed in'));
3370
- console.log(colors.yellow(`${name}'s build script doesn't compile ${missing.length === 1 ? 'a sub-project' : 'its sub-projects'}:`));
3373
+ console.log(colors.yellow(`${name}'s build script doesn't build ${missing.length === 1 ? 'a sub-project' : 'its sub-projects'}:`));
3371
3374
  for (const s of missing)
3372
3375
  console.log(colors.yellow(` ${s.dir}/ — ${s.reason}`));
3373
3376
  const addIt = await confirm(`Set "build": "${newScript}" in ${name}'s package.json?`, strong);
@@ -3417,76 +3420,61 @@ function areSubProjectsUpToDate(cwd, subs) {
3417
3420
  }
3418
3421
  return true;
3419
3422
  }
3420
- /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
3421
- * TypeScript project lacks a build script, run `npm run build`, record
3422
- * failures. Returns true if build succeeded (or was skipped because no
3423
- * tsconfig / noEmit / no build script after declining the prompt / output
3424
- * already up to date). Pass `forceBuild` to skip the freshness check. */
3425
- export async function buildProject(cwd, opts = {}) {
3423
+ // 2026-09-06 Claude Code (Fable 5.1), at Bob's direction ("when you add
3424
+ // importgen for a client dir you should set up the proper package.json").
3425
+ // Factored out of buildProject so a sub-package (itemw/client) gets the SAME
3426
+ // build-script setup as the root importgen wired in, `tsc` added when it has
3427
+ // a tsconfig, its own sub-projects appended instead of a second recipe.
3428
+ /** Make sure `cwd`'s package.json has the build script it needs: importgen
3429
+ * for an importgen project, `tsc` for a TypeScript project (prompted), and a
3430
+ * step per sub-project. Returns the package plus whether the script runs
3431
+ * importgen, or null when there is nothing to build here (no package.json, no
3432
+ * tsconfig or `noEmit` and no importgen, or the user declined the prompt). */
3433
+ export async function ensureBuildScript(cwd) {
3426
3434
  // Do this before the tsconfig check: an importgen project may be plain JS
3427
3435
  // with no tsconfig at all, and still need its import map regenerated.
3428
3436
  await ensureImportgenInBuild(cwd);
3429
3437
  let shouldBuild = false;
3430
- let hasTsconfig = false;
3431
3438
  try {
3432
- const tsconfigPath = path.join(cwd, 'tsconfig.json');
3433
- const content = fs.readFileSync(tsconfigPath, 'utf-8');
3434
- const tsconfig = JSON5.parse(content);
3435
- hasTsconfig = true;
3439
+ const tsconfig = JSON5.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8'));
3436
3440
  shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
3437
3441
  }
3438
3442
  catch {
3439
- // No tsconfig — skip
3443
+ // No tsconfig — nothing for tsc to do
3440
3444
  }
3441
- // Fix removed-in-TS7 tsconfig settings up front rather than waiting for a
3442
- // build to fail on them. A deprecated `moduleResolution` is only a *hard*
3443
- // error under the tsc that flags it (TS6+, and the editor's bundled
3444
- // compiler), so a package pinned to an older tsc — or one that never gets
3445
- // to the build at all (noEmit, no build script, output already fresh) —
3446
- // would otherwise keep the deprecation until TS7 removes it outright.
3447
- // Deliberately ahead of every early return below: the tsconfig is worth
3448
- // fixing whether or not this package builds. Migrating also touches
3449
- // tsconfig.json, which makes the freshness check correctly read as stale.
3450
- const tsconfigSnapshot = new Map();
3451
- const issueMark = markBuildIssues();
3452
- const migratedUpFront = hasTsconfig && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
3453
- let earlyPkg;
3445
+ let pkg;
3454
3446
  try {
3455
- earlyPkg = readPackageJson(cwd);
3447
+ pkg = readPackageJson(cwd);
3456
3448
  }
3457
3449
  catch {
3458
- return true;
3450
+ return null;
3459
3451
  }
3460
- const runsImportgen = scriptRunsImportgen(typeof earlyPkg.scripts?.build === 'string' ? earlyPkg.scripts.build : '');
3452
+ const runsImportgen = scriptRunsImportgen(typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '');
3461
3453
  // A build script that regenerates the import map is worth running even when
3462
3454
  // there's nothing for tsc to emit.
3463
3455
  if (!shouldBuild && !runsImportgen)
3464
- return true;
3465
- const pkg = earlyPkg;
3456
+ return null;
3466
3457
  const existingBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
3467
3458
  // A build script ensureImportgenInBuild just created for a package that had
3468
3459
  // none still needs its tsc pass — treat importgen-only as "no build yet".
3469
3460
  const importgenOnly = !!existingBuild && existingBuild.split(/\s*&&\s*/).every(scriptRunsImportgen);
3470
3461
  if (shouldBuild && (!existingBuild || importgenOnly)) {
3471
- // Compile the sub-projects (service worker, …) in the same breath: the
3472
- // root tsc excludes them, so a bare "tsc" would leave them unbuilt.
3473
- const subs = configuredSubProjects(cwd, readConfig(cwd));
3462
+ // Build the sub-projects (service worker, client package, …) in the same
3463
+ // breath: the root tsc excludes them, so a bare "tsc" would leave them unbuilt.
3464
+ const subs = await readySubProjects(cwd, readConfig(cwd));
3474
3465
  const tscParts = ['tsc', ...subs.map(subProjectBuildCommand)];
3475
3466
  const newScript = [...(existingBuild ? [existingBuild] : []), ...tscParts].join(' && ');
3476
3467
  console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
3477
3468
  for (const s of subs)
3478
3469
  console.log(colors.yellow(` sub-project ${s.dir}/ — ${s.reason}`));
3479
3470
  const addIt = await confirm(`Add "build": "${newScript}" to ${pkg.name || path.basename(cwd)}'s package.json?`, true);
3480
- if (addIt) {
3481
- if (!pkg.scripts)
3482
- pkg.scripts = {};
3483
- pkg.scripts.build = newScript;
3484
- writePackageJson(cwd, pkg);
3485
- console.log(colors.green(`✓ Added "build": "${newScript}" to ${pkg.name || path.basename(cwd)}`));
3486
- }
3487
- else {
3488
- return true;
3489
- }
3471
+ if (!addIt)
3472
+ return null;
3473
+ if (!pkg.scripts)
3474
+ pkg.scripts = {};
3475
+ pkg.scripts.build = newScript;
3476
+ writePackageJson(cwd, pkg);
3477
+ console.log(colors.green(`✓ Added "build": "${newScript}" to ${pkg.name || path.basename(cwd)}`));
3490
3478
  }
3491
3479
  else if (existingBuild) {
3492
3480
  await ensureSubProjectsInBuild(cwd);
@@ -3498,6 +3486,30 @@ export async function buildProject(cwd, opts = {}) {
3498
3486
  catch { /* keep the script we already have */ }
3499
3487
  }
3500
3488
  ensureTsconfigNodeTypes(cwd);
3489
+ return { pkg, runsImportgen };
3490
+ }
3491
+ /** Build a single project: set up its build script (see `ensureBuildScript`),
3492
+ * run `npm run build`, record failures. Returns true if build succeeded (or
3493
+ * was skipped because there was nothing to build / the user declined the
3494
+ * build-script prompt / output already up to date). Pass `forceBuild` to
3495
+ * skip the freshness check. */
3496
+ export async function buildProject(cwd, opts = {}) {
3497
+ // Fix removed-in-TS7 tsconfig settings up front rather than waiting for a
3498
+ // build to fail on them. A deprecated `moduleResolution` is only a *hard*
3499
+ // error under the tsc that flags it (TS6+, and the editor's bundled
3500
+ // compiler), so a package pinned to an older tsc — or one that never gets
3501
+ // to the build at all (noEmit, no build script, output already fresh) —
3502
+ // would otherwise keep the deprecation until TS7 removes it outright.
3503
+ // Deliberately ahead of every early return below: the tsconfig is worth
3504
+ // fixing whether or not this package builds. Migrating also touches
3505
+ // tsconfig.json, which makes the freshness check correctly read as stale.
3506
+ const tsconfigSnapshot = new Map();
3507
+ const issueMark = markBuildIssues();
3508
+ const migratedUpFront = fs.existsSync(path.join(cwd, 'tsconfig.json')) && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
3509
+ const setup = await ensureBuildScript(cwd);
3510
+ if (!setup)
3511
+ return true;
3512
+ const { pkg, runsImportgen } = setup;
3501
3513
  // Only the sub-projects the final build script really compiles count toward
3502
3514
  // freshness — one the user declined to wire in isn't this build's business.
3503
3515
  const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.218",
3
+ "version": "1.0.219",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",