@bobfrankston/npmglobalize 1.0.217 → 1.0.218

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 +12 -7
  2. package/lib.d.ts +21 -13
  3. package/lib.js +110 -34
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -736,7 +736,7 @@ Browser projects that use [`importgen`](https://www.npmjs.com/package/@bobfranks
736
736
 
737
737
  Before building each project, it checks whether the project is an importgen project, in this order:
738
738
 
739
- 1. `.vscode/tasks.json` has a task whose `command` is `importgen` (the HTML file, if the task names one, is reused).
739
+ 1. `.vscode/tasks.json` has a task whose `command` is `importgen` (the HTML file, if the task names one, is reused). The task's `"options": { "cwd": ... }` is honored too: a plain sub-directory (`www/`) yields `importgen www/index.html`, while a sub-directory that is a package of its own (`client/package.json`) is not a root importgen project at all — its own build script owns the import map and the root build delegates to it (see Sub-projects below).
740
740
  2. A root-level `.htm`/`.html` file already contains a `<script type="importmap">` block (`index.html`, `default.html`, `default.htm` are checked first, so a stray `temp.htm` doesn't win).
741
741
  3. `importgen` is in `dependencies`/`devDependencies` **and** the project has one of those HTML files — the HTML requirement keeps packages that merely *use* importgen as a library from matching.
742
742
 
@@ -744,20 +744,25 @@ If a signal matches and the `build` script doesn't already run `importgen`, you'
744
744
 
745
745
  Once wired, the freshness check gains a second condition: a project whose build runs importgen is only considered up to date if the generated HTML is at least as new as `package.json`. Adding a dependency changes the import map without touching any `.ts` file, which the source-vs-output comparison alone would miss.
746
746
 
747
- #### Sub-projects (a second `tsconfig.json` in a sub-directory)
747
+ #### Sub-projects (a sub-directory with its own build script or `tsconfig.json`)
748
748
 
749
- A package can hold more than one TypeScript project. The common shape is a service worker in `Sw/` with its own `tsconfig.json` (`lib: ["WebWorker"]`, its own `outDir`) that the root `tsconfig.json` lists under `exclude` — so a bare `"build": "tsc"` compiles everything *except* the service worker, and the stale `sw2.js` ships. As with import maps, these have historically been built only by a second `.vscode/tasks.json` watcher, which runs on folder open and nowhere else.
749
+ A package can hold more than one project. Two shapes, treated differently:
750
+
751
+ - **A sub-package** — `client/` with its own `package.json` whose `build` script (`importgen index.html && tsc`) already says how the client builds. That script is the single source of truth, so the root build **delegates** to it: `npm run build --prefix client`. The root never re-implements the client's recipe (no `importgen client/index.html && tsc -p client` in the root script), and the sub-package's own build script gets the same importgen check the root does.
752
+ - **A plain tsconfig sub-directory** — a service worker in `Sw/` with its own `tsconfig.json` (`lib: ["WebWorker"]`, its own `outDir`) that the root `tsconfig.json` lists under `exclude`, so a bare `"build": "tsc"` compiles everything *except* the service worker and the stale `sw2.js` ships. It has no build of its own, so the root runs `tsc -p Sw`.
753
+
754
+ As with import maps, these have historically been built only by a second `.vscode/tasks.json` watcher, which runs on folder open and nowhere else.
750
755
 
751
756
  Before building, `npmglobalize` looks for sub-projects, in this order:
752
757
 
753
- 1. `.vscode/tasks.json` has a task that runs `tsc` with `"options": { "cwd": "${workspaceFolder}/Sw" }` or 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).
754
- 2. An immediate sub-directory containing its own `tsconfig.json`. Build output and vendored trees (`node_modules`, `prev`, `dist`, `built`, `out`, `wwwroot`, `coverage`, `temp`, `preflight`, dot-directories) are never scanned.
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.
755
760
 
756
761
  Nothing is detected when the root `tsconfig.json` uses project `references` — that build graph belongs to `tsc -b` and isn't second-guessed.
757
762
 
758
- When the `build` script is `tsc`-driven and doesn't already compile a detected sub-project, you're prompted to append it — `"build": "tsc"` → `"build": "tsc && tsc -p Sw"`, or for an importgen project `"build": "importgen default.htm && tsc && tsc -p Sw"`. Already-wired scripts are recognized in any of their spellings (`tsc -p Sw`, `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`), and the directory is emitted with its real on-disk casing so the script still works on WSL and CI. Decline and `"subProjects": false` is written to `.globalize.json5`, suppressing the prompt for good; a `"subProjects": ["Sw"]` array pins the list instead of detecting it.
763
+ When the `build` script is `tsc`-driven and doesn't already build a detected sub-project, you're prompted to append it — `"build": "tsc"` → `"build": "tsc && npm run build --prefix client"` or `"build": "tsc && tsc -p Sw"`, or for an importgen project `"build": "importgen default.htm && tsc && tsc -p Sw"`. Already-wired scripts are recognized in any of their spellings (`tsc -p Sw`, `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`, `npm run build --prefix client`, `npm -C client run build`), and the directory is emitted with its real on-disk casing so the script still works on WSL and CI. Decline and `"subProjects": false` is written to `.globalize.json5`, suppressing the prompt for good; a `"subProjects": ["Sw"]` array pins the list instead of detecting it.
759
764
 
760
- Sub-projects the build actually compiles are also folded into the freshness check — the package rebuilds when a sub-project's sources are newer than its output. A sub-project that emits *outside* its own directory (`"outDir": ".."`, the usual service-worker case) always reports stale: comparing its sources against the whole package would prove nothing.
765
+ Sub-projects the build actually runs are also folded into the freshness check — the package rebuilds when a sub-project's sources are newer than its output (for a sub-package, also when its import map is older than its `package.json`). A sub-project that emits *outside* its own directory (`"outDir": ".."`, the usual service-worker case) always reports stale: comparing its sources against the whole package would prove nothing.
761
766
 
762
767
  #### TypeScript 6 `types` auto-fix
763
768
 
package/lib.d.ts CHANGED
@@ -427,22 +427,30 @@ export declare function detectImportgen(cwd: string): {
427
427
  * start its watcher on folder open. Declining records `importgen: false` in
428
428
  * .globalize.json5 so it stops asking. */
429
429
  export declare function ensureImportgenInBuild(cwd: string): Promise<void>;
430
- /** Sub-directories that need their own `tsc -p <dir>` pass. A package can hold
431
- * more than one tsconfig — a service worker compiled with `lib: WebWorker`, say,
432
- * which the root tsconfig `exclude`s and the root `tsc` silently skips them.
430
+ /** A sub-directory the root build must build, and how. */
431
+ interface SubProject {
432
+ dir: string; /** Relative directory with its on-disk casing */
433
+ kind: 'package' | 'tsconfig'; /** package: delegate to its build script; tsconfig: `tsc -p` */
434
+ reason: string; /** The signal that found it, quoted in prompts */
435
+ }
436
+ /** Sub-directories the root build must build itself. A package can hold more
437
+ * than one project — a service worker compiled with `lib: WebWorker` that the
438
+ * root tsconfig `exclude`s, or a browser client that is a package of its own
439
+ * with its own build script — and the root `tsc` silently skips them.
433
440
  * Signals, in order:
434
- * 1. `.vscode/tasks.json` runs tsc with `options.cwd` or `-p` pointing at a
435
- * sub-dir (authoritative: it's how the developer actually builds today),
436
- * 2. an immediate sub-directory holding its own tsconfig.json.
441
+ * 1. `.vscode/tasks.json` runs tsc or importgen with `options.cwd` or `-p`
442
+ * pointing at a sub-dir (authoritative: it's how the developer actually
443
+ * builds today),
444
+ * 2. an immediate sub-directory holding its own package.json build script
445
+ * or tsconfig.json.
437
446
  * Returns nothing when the root tsconfig uses project `references` — `tsc -b`
438
447
  * owns that build graph and shouldn't be second-guessed. */
439
- export declare function detectSubProjects(cwd: string): Array<{
440
- dir: string;
441
- reason: string;
442
- }>;
443
- /** Append `tsc -p <dir>` for every sub-project the existing build script misses.
444
- * Only touches tsc-driven builds — a bundler-driven `build` has its own idea of
445
- * what to compile. Declining records `subProjects: false` in .globalize.json5. */
448
+ export declare function detectSubProjects(cwd: string): SubProject[];
449
+ /** Append a build step for every sub-project the existing build script misses:
450
+ * `npm run build --prefix <dir>` for a sub-package, `tsc -p <dir>` for a plain
451
+ * tsconfig. Only touches tsc-driven builds — a bundler-driven `build` has its
452
+ * own idea of what to compile. Declining records `subProjects: false` in
453
+ * .globalize.json5. */
446
454
  export declare function ensureSubProjectsInBuild(cwd: string): Promise<void>;
447
455
  /** Build a single project: detect tsconfig, prompt to add `build: tsc` if a
448
456
  * TypeScript project lacks a build script, run `npm run build`, record
package/lib.js CHANGED
@@ -344,7 +344,7 @@ const VALUE_COMMENTS = {
344
344
  gitVisibility: 'private (default) or public',
345
345
  npmVisibility: 'private (default) or public',
346
346
  bump: 'patch (default), minor, or major',
347
- subProjects: 'Sub-dirs with their own tsconfig that build must compile (false = never ask)'
347
+ subProjects: 'Sub-dirs with their own build script or tsconfig that build must run (false = never ask)'
348
348
  };
349
349
  /** Write .globalize.json5 config file */
350
350
  export function writeConfig(dir, config, explicitKeys) {
@@ -450,7 +450,7 @@ export function writeConfig(dir, config, explicitKeys) {
450
450
  ['"usePaths": true', 'true = resolve file: deps from siblings; false = use latest npm version (standalone)'],
451
451
  ['"allowTs": false', 'true = include .ts source in npm tarball (auto-true for noEmit projects)'],
452
452
  ['"importgen": auto', 'Detected from .vscode/tasks.json / import map in HTML; false = never ask'],
453
- ['"subProjects": auto', 'Sub-dirs with their own tsconfig (e.g. ["Sw"]); detected from .vscode/tasks.json; false = never ask']
453
+ ['"subProjects": auto', 'Sub-dirs with their own build script or tsconfig (e.g. ["client", "Sw"]); detected from .vscode/tasks.json; false = never ask']
454
454
  ];
455
455
  const refWidth = Math.max(...reference.map(([decl]) => decl.length));
456
456
  for (const [decl, note] of reference) {
@@ -3019,7 +3019,27 @@ export function detectImportgen(cwd) {
3019
3019
  if (!scriptRunsImportgen(command))
3020
3020
  continue;
3021
3021
  const named = args.find(a => /\.html?$/i.test(a));
3022
- return { htmlFile: findImportgenHtml(cwd, named), reason: '.vscode/tasks.json runs importgen' };
3022
+ // 2026-09-06 Claude Code (Fable 5.1), at Bob's direction.
3023
+ // Honor the task's `options.cwd`. itemw runs importgen with cwd
3024
+ // `${workspaceFolder}/client` because its HTML is client/index.html;
3025
+ // ignoring that made us search the root, find nothing, and wire a bare
3026
+ // `importgen` that failed with "No HTML file found".
3027
+ // - cwd is a sub-PACKAGE (has its own package.json): its own build
3028
+ // script owns the import map, and the root build delegates to it
3029
+ // (see detectSubProjects). Not a root importgen project.
3030
+ // - cwd is a plain sub-directory (www/ with just the HTML): run
3031
+ // `importgen www/index.html` from the root — importgen resolves
3032
+ // positional paths against cwd and finds package.json by walking up.
3033
+ const taskCwd = typeof task?.options?.cwd === 'string' ? task.options.cwd : '';
3034
+ let subdir = normalizeProjectPath(taskCwd);
3035
+ if (path.isAbsolute(subdir))
3036
+ subdir = ''; // some other machine's absolute path — nothing we can build from
3037
+ if (subdir && fs.existsSync(path.join(cwd, subdir, 'package.json')))
3038
+ return null;
3039
+ const searchDir = subdir ? path.join(cwd, subdir) : cwd;
3040
+ const found = findImportgenHtml(searchDir, named);
3041
+ const htmlFile = found && subdir ? `${subdir}/${found}` : found;
3042
+ return { htmlFile, reason: '.vscode/tasks.json runs importgen' };
3023
3043
  }
3024
3044
  }
3025
3045
  catch {
@@ -3178,13 +3198,34 @@ function resolveOnDiskCase(base, rel) {
3178
3198
  }
3179
3199
  return out.length ? out.join('/') : null;
3180
3200
  }
3181
- /** True when `script` already compiles the tsconfig in `dir` `tsc -p Sw`,
3182
- * `tsc --project ./Sw/tsconfig.json`, or `cd Sw && tsc`. */
3201
+ /** Classify `rel` under `cwd`: a package with its own build script, a plain
3202
+ * tsconfig sub-project, or null when it's neither. */
3203
+ 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
+ }
3212
+ return fs.existsSync(path.join(cwd, rel, 'tsconfig.json')) ? 'tsconfig' : null;
3213
+ }
3214
+ /** The command the root build script runs for a sub-project. */
3215
+ function subProjectBuildCommand(s) {
3216
+ return s.kind === 'package'
3217
+ ? `npm run build --prefix ${quoteScriptPath(s.dir)}`
3218
+ : `tsc -p ${quoteScriptPath(s.dir)}`;
3219
+ }
3220
+ /** True when `script` already builds `dir` — `tsc -p Sw`,
3221
+ * `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`, or for a package
3222
+ * `npm run build --prefix client` / `npm -C client run build`. */
3183
3223
  function scriptBuildsSubProject(script, dir) {
3184
3224
  const target = normalizeProjectPath(dir).toLowerCase();
3185
3225
  if (!target)
3186
3226
  return false;
3187
3227
  for (const re of [/(?:^|\s)(?:-p|--project)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
3228
+ /(?:^|\s)(?:-C|--prefix)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
3188
3229
  /(?:^|[\s&|(])cd\s+("[^"]+"|'[^']+'|\S+)/g]) {
3189
3230
  for (const m of script.matchAll(re)) {
3190
3231
  if (normalizeProjectPath(m[1]).toLowerCase() === target)
@@ -3193,13 +3234,16 @@ function scriptBuildsSubProject(script, dir) {
3193
3234
  }
3194
3235
  return false;
3195
3236
  }
3196
- /** Sub-directories that need their own `tsc -p <dir>` pass. A package can hold
3197
- * more than one tsconfig — a service worker compiled with `lib: WebWorker`, say,
3198
- * which the root tsconfig `exclude`s and the root `tsc` silently skips them.
3237
+ /** Sub-directories the root build must build itself. A package can hold more
3238
+ * than one project — a service worker compiled with `lib: WebWorker` that the
3239
+ * root tsconfig `exclude`s, or a browser client that is a package of its own
3240
+ * with its own build script — and the root `tsc` silently skips them.
3199
3241
  * Signals, in order:
3200
- * 1. `.vscode/tasks.json` runs tsc with `options.cwd` or `-p` pointing at a
3201
- * sub-dir (authoritative: it's how the developer actually builds today),
3202
- * 2. an immediate sub-directory holding its own tsconfig.json.
3242
+ * 1. `.vscode/tasks.json` runs tsc or importgen with `options.cwd` or `-p`
3243
+ * pointing at a sub-dir (authoritative: it's how the developer actually
3244
+ * builds today),
3245
+ * 2. an immediate sub-directory holding its own package.json build script
3246
+ * or tsconfig.json.
3203
3247
  * Returns nothing when the root tsconfig uses project `references` — `tsc -b`
3204
3248
  * owns that build graph and shouldn't be second-guessed. */
3205
3249
  export function detectSubProjects(cwd) {
@@ -3211,7 +3255,7 @@ export function detectSubProjects(cwd) {
3211
3255
  catch {
3212
3256
  // No root tsconfig (or unparsable) — nested ones still count
3213
3257
  }
3214
- const found = new Map(); // on-disk relative dir → reason
3258
+ const found = new Map(); // on-disk relative dir → entry
3215
3259
  const add = (raw, reason) => {
3216
3260
  const rel = normalizeProjectPath(raw);
3217
3261
  if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
@@ -3221,10 +3265,11 @@ export function detectSubProjects(cwd) {
3221
3265
  const onDisk = resolveOnDiskCase(cwd, rel);
3222
3266
  if (!onDisk)
3223
3267
  return;
3224
- if (!fs.existsSync(path.join(cwd, onDisk, 'tsconfig.json')))
3268
+ const kind = subProjectKind(cwd, onDisk);
3269
+ if (!kind)
3225
3270
  return;
3226
3271
  if (!found.has(onDisk))
3227
- found.set(onDisk, reason);
3272
+ found.set(onDisk, { dir: onDisk, kind, reason });
3228
3273
  };
3229
3274
  // 1. .vscode/tasks.json — JSON5 because VS Code allows comments/trailing commas
3230
3275
  try {
@@ -3232,37 +3277,39 @@ export function detectSubProjects(cwd) {
3232
3277
  for (const task of Array.isArray(tasks?.tasks) ? tasks.tasks : []) {
3233
3278
  const command = typeof task?.command === 'string' ? task.command : '';
3234
3279
  const args = Array.isArray(task?.args) ? task.args.filter((a) => typeof a === 'string') : [];
3235
- if (!scriptRunsTsc([command, ...args].join(' ')))
3280
+ const line = [command, ...args].join(' ');
3281
+ const tool = scriptRunsTsc(line) ? 'tsc' : scriptRunsImportgen(line) ? 'importgen' : null;
3282
+ if (!tool)
3236
3283
  continue;
3237
- const label = typeof task?.label === 'string' ? task.label : 'a tsc task';
3284
+ const label = typeof task?.label === 'string' ? task.label : `a ${tool} task`;
3238
3285
  const projIdx = args.findIndex(a => a === '-p' || a === '--project');
3239
- if (projIdx !== -1 && args[projIdx + 1]) {
3286
+ if (tool === 'tsc' && projIdx !== -1 && args[projIdx + 1]) {
3240
3287
  add(args[projIdx + 1], `.vscode/tasks.json "${label}" builds it`);
3241
3288
  }
3242
3289
  else if (typeof task?.options?.cwd === 'string') {
3243
- add(task.options.cwd, `.vscode/tasks.json "${label}" runs tsc there`);
3290
+ add(task.options.cwd, `.vscode/tasks.json "${label}" runs ${tool} there`);
3244
3291
  }
3245
3292
  }
3246
3293
  }
3247
3294
  catch {
3248
3295
  // No tasks.json, or unparsable — fall through to the filesystem scan
3249
3296
  }
3250
- // 2. Immediate sub-directories with their own tsconfig.json
3297
+ // 2. Immediate sub-directories with their own build script or tsconfig.json
3251
3298
  try {
3252
3299
  for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
3253
3300
  if (!e.isDirectory() || e.isSymbolicLink())
3254
3301
  continue;
3255
3302
  if (e.name.startsWith('.') || SUBPROJECT_SKIP_DIRS.has(e.name.toLowerCase()))
3256
3303
  continue;
3257
- if (fs.existsSync(path.join(cwd, e.name, 'tsconfig.json'))) {
3258
- add(e.name, `${e.name}/tsconfig.json`);
3259
- }
3304
+ const kind = subProjectKind(cwd, e.name);
3305
+ if (kind)
3306
+ add(e.name, kind === 'package' ? `${e.name}/package.json has a build script` : `${e.name}/tsconfig.json`);
3260
3307
  }
3261
3308
  }
3262
3309
  catch {
3263
3310
  // Unreadable directory — nothing more to detect
3264
3311
  }
3265
- return [...found].map(([dir, reason]) => ({ dir, reason }));
3312
+ return [...found.values()];
3266
3313
  }
3267
3314
  /** The sub-projects to wire, honoring `.globalize.json5`: `false` disables the
3268
3315
  * whole check, an array pins the list instead of detecting it. */
@@ -3270,9 +3317,14 @@ function configuredSubProjects(cwd, config) {
3270
3317
  if (config.subProjects === false)
3271
3318
  return [];
3272
3319
  if (Array.isArray(config.subProjects)) {
3273
- return config.subProjects
3274
- .map(d => ({ dir: resolveOnDiskCase(cwd, normalizeProjectPath(d)) || d, reason: 'listed in .globalize.json5' }))
3275
- .filter(s => fs.existsSync(path.join(cwd, s.dir, 'tsconfig.json')));
3320
+ const listed = [];
3321
+ for (const d of config.subProjects) {
3322
+ const dir = resolveOnDiskCase(cwd, normalizeProjectPath(d)) || d;
3323
+ const kind = subProjectKind(cwd, dir);
3324
+ if (kind)
3325
+ listed.push({ dir, kind, reason: 'listed in .globalize.json5' });
3326
+ }
3327
+ return listed;
3276
3328
  }
3277
3329
  return detectSubProjects(cwd);
3278
3330
  }
@@ -3280,9 +3332,11 @@ function configuredSubProjects(cwd, config) {
3280
3332
  function quoteScriptPath(p) {
3281
3333
  return /[\s&|<>]/.test(p) ? `"${p}"` : p;
3282
3334
  }
3283
- /** Append `tsc -p <dir>` for every sub-project the existing build script misses.
3284
- * Only touches tsc-driven builds a bundler-driven `build` has its own idea of
3285
- * what to compile. Declining records `subProjects: false` in .globalize.json5. */
3335
+ /** Append a build step for every sub-project the existing build script misses:
3336
+ * `npm run build --prefix <dir>` for a sub-package, `tsc -p <dir>` for a plain
3337
+ * tsconfig. Only touches tsc-driven builds a bundler-driven `build` has its
3338
+ * own idea of what to compile. Declining records `subProjects: false` in
3339
+ * .globalize.json5. */
3286
3340
  export async function ensureSubProjectsInBuild(cwd) {
3287
3341
  const config = readConfig(cwd);
3288
3342
  if (config.subProjects === false)
@@ -3297,12 +3351,18 @@ export async function ensureSubProjectsInBuild(cwd) {
3297
3351
  const buildScript = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
3298
3352
  if (!buildScript || !scriptRunsTsc(buildScript))
3299
3353
  return;
3300
- const missing = configuredSubProjects(cwd, config)
3301
- .filter(s => !scriptBuildsSubProject(buildScript, s.dir));
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));
3302
3362
  if (missing.length === 0)
3303
3363
  return;
3304
3364
  const name = pkg.name || path.basename(cwd);
3305
- const newScript = [buildScript, ...missing.map(s => `tsc -p ${quoteScriptPath(s.dir)}`)].join(' && ');
3365
+ const newScript = [buildScript, ...missing.map(subProjectBuildCommand)].join(' && ');
3306
3366
  // Default to yes only when tasks.json says the developer already builds these
3307
3367
  // — a bare nested tsconfig (tests, examples) is a weaker signal, and this
3308
3368
  // prompt rewrites a build script that currently works.
@@ -3318,7 +3378,7 @@ export async function ensureSubProjectsInBuild(cwd) {
3318
3378
  }
3319
3379
  pkg.scripts.build = newScript;
3320
3380
  writePackageJson(cwd, pkg);
3321
- console.log(colors.green(`✓ Added ${missing.map(s => `tsc -p ${s.dir}`).join(' && ')} to ${name}'s build script`));
3381
+ console.log(colors.green(`✓ Added ${missing.map(subProjectBuildCommand).join(' && ')} to ${name}'s build script`));
3322
3382
  }
3323
3383
  /** Freshness for the sub-projects the build script actually compiles. A
3324
3384
  * sub-project that emits outside its own directory (a service worker with
@@ -3327,6 +3387,22 @@ export async function ensureSubProjectsInBuild(cwd) {
3327
3387
  function areSubProjectsUpToDate(cwd, subs) {
3328
3388
  for (const s of subs) {
3329
3389
  const dir = path.join(cwd, s.dir);
3390
+ if (s.kind === 'package') {
3391
+ // Its own build script decides what it emits; judge it the way the
3392
+ // root is judged — sources vs output, plus the import map if it has one.
3393
+ if (!isBuildUpToDate(dir))
3394
+ return false;
3395
+ let subBuild = '';
3396
+ try {
3397
+ subBuild = readPackageJson(dir).scripts?.build ?? '';
3398
+ }
3399
+ catch {
3400
+ return false;
3401
+ }
3402
+ if (scriptRunsImportgen(subBuild) && !isImportMapUpToDate(dir))
3403
+ return false;
3404
+ continue;
3405
+ }
3330
3406
  let co;
3331
3407
  try {
3332
3408
  co = JSON5.parse(fs.readFileSync(path.join(dir, 'tsconfig.json'), 'utf-8')).compilerOptions || {};
@@ -3395,7 +3471,7 @@ export async function buildProject(cwd, opts = {}) {
3395
3471
  // Compile the sub-projects (service worker, …) in the same breath: the
3396
3472
  // root tsc excludes them, so a bare "tsc" would leave them unbuilt.
3397
3473
  const subs = configuredSubProjects(cwd, readConfig(cwd));
3398
- const tscParts = ['tsc', ...subs.map(s => `tsc -p ${quoteScriptPath(s.dir)}`)];
3474
+ const tscParts = ['tsc', ...subs.map(subProjectBuildCommand)];
3399
3475
  const newScript = [...(existingBuild ? [existingBuild] : []), ...tscParts].join(' && ');
3400
3476
  console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
3401
3477
  for (const s of subs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobfrankston/npmglobalize",
3
- "version": "1.0.217",
3
+ "version": "1.0.218",
4
4
  "description": "Transform file: dependencies to npm versions for publishing",
5
5
  "main": "index.js",
6
6
  "type": "module",