@bobfrankston/npmglobalize 1.0.217 → 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.
- package/README.md +12 -7
- package/lib.d.ts +35 -18
- package/lib.js +165 -77
- 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
|
|
747
|
+
#### Sub-projects (a sub-directory with its own build script or `tsconfig.json`)
|
|
748
748
|
|
|
749
|
-
A package can hold more than one
|
|
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}/
|
|
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` 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.
|
|
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
|
|
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
|
|
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,28 +427,45 @@ 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
|
-
/**
|
|
431
|
-
|
|
432
|
-
|
|
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: has its own package.json, 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`
|
|
435
|
-
* sub-dir (authoritative: it's how the developer actually
|
|
436
|
-
*
|
|
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):
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
*
|
|
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
|
-
/**
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
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. */
|
|
452
469
|
export declare function buildProject(cwd: string, opts?: {
|
|
453
470
|
verbose?: boolean;
|
|
454
471
|
force?: boolean;
|
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
|
|
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
|
-
|
|
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,29 @@ function resolveOnDiskCase(base, rel) {
|
|
|
3178
3198
|
}
|
|
3179
3199
|
return out.length ? out.join('/') : null;
|
|
3180
3200
|
}
|
|
3181
|
-
/**
|
|
3182
|
-
*
|
|
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. */
|
|
3204
|
+
function subProjectKind(cwd, rel) {
|
|
3205
|
+
if (fs.existsSync(path.join(cwd, rel, 'package.json')))
|
|
3206
|
+
return 'package';
|
|
3207
|
+
return fs.existsSync(path.join(cwd, rel, 'tsconfig.json')) ? 'tsconfig' : null;
|
|
3208
|
+
}
|
|
3209
|
+
/** The command the root build script runs for a sub-project. */
|
|
3210
|
+
function subProjectBuildCommand(s) {
|
|
3211
|
+
return s.kind === 'package'
|
|
3212
|
+
? `npm run build --prefix ${quoteScriptPath(s.dir)}`
|
|
3213
|
+
: `tsc -p ${quoteScriptPath(s.dir)}`;
|
|
3214
|
+
}
|
|
3215
|
+
/** True when `script` already builds `dir` — `tsc -p Sw`,
|
|
3216
|
+
* `tsc --project ./Sw/tsconfig.json`, `cd Sw && tsc`, or for a package
|
|
3217
|
+
* `npm run build --prefix client` / `npm -C client run build`. */
|
|
3183
3218
|
function scriptBuildsSubProject(script, dir) {
|
|
3184
3219
|
const target = normalizeProjectPath(dir).toLowerCase();
|
|
3185
3220
|
if (!target)
|
|
3186
3221
|
return false;
|
|
3187
3222
|
for (const re of [/(?:^|\s)(?:-p|--project)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
|
|
3223
|
+
/(?:^|\s)(?:-C|--prefix)[\s=]+("[^"]+"|'[^']+'|\S+)/g,
|
|
3188
3224
|
/(?:^|[\s&|(])cd\s+("[^"]+"|'[^']+'|\S+)/g]) {
|
|
3189
3225
|
for (const m of script.matchAll(re)) {
|
|
3190
3226
|
if (normalizeProjectPath(m[1]).toLowerCase() === target)
|
|
@@ -3193,13 +3229,16 @@ function scriptBuildsSubProject(script, dir) {
|
|
|
3193
3229
|
}
|
|
3194
3230
|
return false;
|
|
3195
3231
|
}
|
|
3196
|
-
/** Sub-directories
|
|
3197
|
-
*
|
|
3198
|
-
*
|
|
3232
|
+
/** Sub-directories the root build must build itself. A package can hold more
|
|
3233
|
+
* than one project — a service worker compiled with `lib: WebWorker` that the
|
|
3234
|
+
* root tsconfig `exclude`s, or a browser client that is a package of its own
|
|
3235
|
+
* with its own build script — and the root `tsc` silently skips them.
|
|
3199
3236
|
* Signals, in order:
|
|
3200
|
-
* 1. `.vscode/tasks.json` runs tsc with `options.cwd` or `-p`
|
|
3201
|
-
* sub-dir (authoritative: it's how the developer actually
|
|
3202
|
-
*
|
|
3237
|
+
* 1. `.vscode/tasks.json` runs tsc or importgen with `options.cwd` or `-p`
|
|
3238
|
+
* pointing at a sub-dir (authoritative: it's how the developer actually
|
|
3239
|
+
* builds today),
|
|
3240
|
+
* 2. an immediate sub-directory holding its own package.json build script
|
|
3241
|
+
* or tsconfig.json.
|
|
3203
3242
|
* Returns nothing when the root tsconfig uses project `references` — `tsc -b`
|
|
3204
3243
|
* owns that build graph and shouldn't be second-guessed. */
|
|
3205
3244
|
export function detectSubProjects(cwd) {
|
|
@@ -3211,7 +3250,7 @@ export function detectSubProjects(cwd) {
|
|
|
3211
3250
|
catch {
|
|
3212
3251
|
// No root tsconfig (or unparsable) — nested ones still count
|
|
3213
3252
|
}
|
|
3214
|
-
const found = new Map(); // on-disk relative dir →
|
|
3253
|
+
const found = new Map(); // on-disk relative dir → entry
|
|
3215
3254
|
const add = (raw, reason) => {
|
|
3216
3255
|
const rel = normalizeProjectPath(raw);
|
|
3217
3256
|
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
@@ -3221,10 +3260,11 @@ export function detectSubProjects(cwd) {
|
|
|
3221
3260
|
const onDisk = resolveOnDiskCase(cwd, rel);
|
|
3222
3261
|
if (!onDisk)
|
|
3223
3262
|
return;
|
|
3224
|
-
|
|
3263
|
+
const kind = subProjectKind(cwd, onDisk);
|
|
3264
|
+
if (!kind)
|
|
3225
3265
|
return;
|
|
3226
3266
|
if (!found.has(onDisk))
|
|
3227
|
-
found.set(onDisk, reason);
|
|
3267
|
+
found.set(onDisk, { dir: onDisk, kind, reason });
|
|
3228
3268
|
};
|
|
3229
3269
|
// 1. .vscode/tasks.json — JSON5 because VS Code allows comments/trailing commas
|
|
3230
3270
|
try {
|
|
@@ -3232,37 +3272,39 @@ export function detectSubProjects(cwd) {
|
|
|
3232
3272
|
for (const task of Array.isArray(tasks?.tasks) ? tasks.tasks : []) {
|
|
3233
3273
|
const command = typeof task?.command === 'string' ? task.command : '';
|
|
3234
3274
|
const args = Array.isArray(task?.args) ? task.args.filter((a) => typeof a === 'string') : [];
|
|
3235
|
-
|
|
3275
|
+
const line = [command, ...args].join(' ');
|
|
3276
|
+
const tool = scriptRunsTsc(line) ? 'tsc' : scriptRunsImportgen(line) ? 'importgen' : null;
|
|
3277
|
+
if (!tool)
|
|
3236
3278
|
continue;
|
|
3237
|
-
const label = typeof task?.label === 'string' ? task.label :
|
|
3279
|
+
const label = typeof task?.label === 'string' ? task.label : `a ${tool} task`;
|
|
3238
3280
|
const projIdx = args.findIndex(a => a === '-p' || a === '--project');
|
|
3239
|
-
if (projIdx !== -1 && args[projIdx + 1]) {
|
|
3281
|
+
if (tool === 'tsc' && projIdx !== -1 && args[projIdx + 1]) {
|
|
3240
3282
|
add(args[projIdx + 1], `.vscode/tasks.json "${label}" builds it`);
|
|
3241
3283
|
}
|
|
3242
3284
|
else if (typeof task?.options?.cwd === 'string') {
|
|
3243
|
-
add(task.options.cwd, `.vscode/tasks.json "${label}" runs
|
|
3285
|
+
add(task.options.cwd, `.vscode/tasks.json "${label}" runs ${tool} there`);
|
|
3244
3286
|
}
|
|
3245
3287
|
}
|
|
3246
3288
|
}
|
|
3247
3289
|
catch {
|
|
3248
3290
|
// No tasks.json, or unparsable — fall through to the filesystem scan
|
|
3249
3291
|
}
|
|
3250
|
-
// 2. Immediate sub-directories with their own tsconfig.json
|
|
3292
|
+
// 2. Immediate sub-directories with their own build script or tsconfig.json
|
|
3251
3293
|
try {
|
|
3252
3294
|
for (const e of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
3253
3295
|
if (!e.isDirectory() || e.isSymbolicLink())
|
|
3254
3296
|
continue;
|
|
3255
3297
|
if (e.name.startsWith('.') || SUBPROJECT_SKIP_DIRS.has(e.name.toLowerCase()))
|
|
3256
3298
|
continue;
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3299
|
+
const kind = subProjectKind(cwd, e.name);
|
|
3300
|
+
if (kind)
|
|
3301
|
+
add(e.name, kind === 'package' ? `${e.name}/package.json` : `${e.name}/tsconfig.json`);
|
|
3260
3302
|
}
|
|
3261
3303
|
}
|
|
3262
3304
|
catch {
|
|
3263
3305
|
// Unreadable directory — nothing more to detect
|
|
3264
3306
|
}
|
|
3265
|
-
return [...found
|
|
3307
|
+
return [...found.values()];
|
|
3266
3308
|
}
|
|
3267
3309
|
/** The sub-projects to wire, honoring `.globalize.json5`: `false` disables the
|
|
3268
3310
|
* whole check, an array pins the list instead of detecting it. */
|
|
@@ -3270,19 +3312,40 @@ function configuredSubProjects(cwd, config) {
|
|
|
3270
3312
|
if (config.subProjects === false)
|
|
3271
3313
|
return [];
|
|
3272
3314
|
if (Array.isArray(config.subProjects)) {
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3315
|
+
const listed = [];
|
|
3316
|
+
for (const d of config.subProjects) {
|
|
3317
|
+
const dir = resolveOnDiskCase(cwd, normalizeProjectPath(d)) || d;
|
|
3318
|
+
const kind = subProjectKind(cwd, dir);
|
|
3319
|
+
if (kind)
|
|
3320
|
+
listed.push({ dir, kind, reason: 'listed in .globalize.json5' });
|
|
3321
|
+
}
|
|
3322
|
+
return listed;
|
|
3276
3323
|
}
|
|
3277
3324
|
return detectSubProjects(cwd);
|
|
3278
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
|
+
}
|
|
3279
3340
|
/** Quote a path for a package.json script only when it needs it. */
|
|
3280
3341
|
function quoteScriptPath(p) {
|
|
3281
3342
|
return /[\s&|<>]/.test(p) ? `"${p}"` : p;
|
|
3282
3343
|
}
|
|
3283
|
-
/** Append
|
|
3284
|
-
*
|
|
3285
|
-
*
|
|
3344
|
+
/** Append a build step for every sub-project the existing build script misses:
|
|
3345
|
+
* `npm run build --prefix <dir>` for a sub-package, `tsc -p <dir>` for a plain
|
|
3346
|
+
* tsconfig. Only touches tsc-driven builds — a bundler-driven `build` has its
|
|
3347
|
+
* own idea of what to compile. Declining records `subProjects: false` in
|
|
3348
|
+
* .globalize.json5. */
|
|
3286
3349
|
export async function ensureSubProjectsInBuild(cwd) {
|
|
3287
3350
|
const config = readConfig(cwd);
|
|
3288
3351
|
if (config.subProjects === false)
|
|
@@ -3297,17 +3360,17 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3297
3360
|
const buildScript = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
|
|
3298
3361
|
if (!buildScript || !scriptRunsTsc(buildScript))
|
|
3299
3362
|
return;
|
|
3300
|
-
const missing =
|
|
3363
|
+
const missing = (await readySubProjects(cwd, config))
|
|
3301
3364
|
.filter(s => !scriptBuildsSubProject(buildScript, s.dir));
|
|
3302
3365
|
if (missing.length === 0)
|
|
3303
3366
|
return;
|
|
3304
3367
|
const name = pkg.name || path.basename(cwd);
|
|
3305
|
-
const newScript = [buildScript, ...missing.map(
|
|
3368
|
+
const newScript = [buildScript, ...missing.map(subProjectBuildCommand)].join(' && ');
|
|
3306
3369
|
// Default to yes only when tasks.json says the developer already builds these
|
|
3307
3370
|
// — a bare nested tsconfig (tests, examples) is a weaker signal, and this
|
|
3308
3371
|
// prompt rewrites a build script that currently works.
|
|
3309
3372
|
const strong = missing.every(s => s.reason.startsWith('.vscode/tasks.json') || s.reason.startsWith('listed in'));
|
|
3310
|
-
console.log(colors.yellow(`${name}'s build script doesn't
|
|
3373
|
+
console.log(colors.yellow(`${name}'s build script doesn't build ${missing.length === 1 ? 'a sub-project' : 'its sub-projects'}:`));
|
|
3311
3374
|
for (const s of missing)
|
|
3312
3375
|
console.log(colors.yellow(` ${s.dir}/ — ${s.reason}`));
|
|
3313
3376
|
const addIt = await confirm(`Set "build": "${newScript}" in ${name}'s package.json?`, strong);
|
|
@@ -3318,7 +3381,7 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3318
3381
|
}
|
|
3319
3382
|
pkg.scripts.build = newScript;
|
|
3320
3383
|
writePackageJson(cwd, pkg);
|
|
3321
|
-
console.log(colors.green(`✓ Added ${missing.map(
|
|
3384
|
+
console.log(colors.green(`✓ Added ${missing.map(subProjectBuildCommand).join(' && ')} to ${name}'s build script`));
|
|
3322
3385
|
}
|
|
3323
3386
|
/** Freshness for the sub-projects the build script actually compiles. A
|
|
3324
3387
|
* sub-project that emits outside its own directory (a service worker with
|
|
@@ -3327,6 +3390,22 @@ export async function ensureSubProjectsInBuild(cwd) {
|
|
|
3327
3390
|
function areSubProjectsUpToDate(cwd, subs) {
|
|
3328
3391
|
for (const s of subs) {
|
|
3329
3392
|
const dir = path.join(cwd, s.dir);
|
|
3393
|
+
if (s.kind === 'package') {
|
|
3394
|
+
// Its own build script decides what it emits; judge it the way the
|
|
3395
|
+
// root is judged — sources vs output, plus the import map if it has one.
|
|
3396
|
+
if (!isBuildUpToDate(dir))
|
|
3397
|
+
return false;
|
|
3398
|
+
let subBuild = '';
|
|
3399
|
+
try {
|
|
3400
|
+
subBuild = readPackageJson(dir).scripts?.build ?? '';
|
|
3401
|
+
}
|
|
3402
|
+
catch {
|
|
3403
|
+
return false;
|
|
3404
|
+
}
|
|
3405
|
+
if (scriptRunsImportgen(subBuild) && !isImportMapUpToDate(dir))
|
|
3406
|
+
return false;
|
|
3407
|
+
continue;
|
|
3408
|
+
}
|
|
3330
3409
|
let co;
|
|
3331
3410
|
try {
|
|
3332
3411
|
co = JSON5.parse(fs.readFileSync(path.join(dir, 'tsconfig.json'), 'utf-8')).compilerOptions || {};
|
|
@@ -3341,76 +3420,61 @@ function areSubProjectsUpToDate(cwd, subs) {
|
|
|
3341
3420
|
}
|
|
3342
3421
|
return true;
|
|
3343
3422
|
}
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
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) {
|
|
3350
3434
|
// Do this before the tsconfig check: an importgen project may be plain JS
|
|
3351
3435
|
// with no tsconfig at all, and still need its import map regenerated.
|
|
3352
3436
|
await ensureImportgenInBuild(cwd);
|
|
3353
3437
|
let shouldBuild = false;
|
|
3354
|
-
let hasTsconfig = false;
|
|
3355
3438
|
try {
|
|
3356
|
-
const
|
|
3357
|
-
const content = fs.readFileSync(tsconfigPath, 'utf-8');
|
|
3358
|
-
const tsconfig = JSON5.parse(content);
|
|
3359
|
-
hasTsconfig = true;
|
|
3439
|
+
const tsconfig = JSON5.parse(fs.readFileSync(path.join(cwd, 'tsconfig.json'), 'utf-8'));
|
|
3360
3440
|
shouldBuild = tsconfig.compilerOptions?.noEmit !== true;
|
|
3361
3441
|
}
|
|
3362
3442
|
catch {
|
|
3363
|
-
// No tsconfig —
|
|
3443
|
+
// No tsconfig — nothing for tsc to do
|
|
3364
3444
|
}
|
|
3365
|
-
|
|
3366
|
-
// build to fail on them. A deprecated `moduleResolution` is only a *hard*
|
|
3367
|
-
// error under the tsc that flags it (TS6+, and the editor's bundled
|
|
3368
|
-
// compiler), so a package pinned to an older tsc — or one that never gets
|
|
3369
|
-
// to the build at all (noEmit, no build script, output already fresh) —
|
|
3370
|
-
// would otherwise keep the deprecation until TS7 removes it outright.
|
|
3371
|
-
// Deliberately ahead of every early return below: the tsconfig is worth
|
|
3372
|
-
// fixing whether or not this package builds. Migrating also touches
|
|
3373
|
-
// tsconfig.json, which makes the freshness check correctly read as stale.
|
|
3374
|
-
const tsconfigSnapshot = new Map();
|
|
3375
|
-
const issueMark = markBuildIssues();
|
|
3376
|
-
const migratedUpFront = hasTsconfig && migrateTsconfigDeprecations(cwd, tsconfigSnapshot);
|
|
3377
|
-
let earlyPkg;
|
|
3445
|
+
let pkg;
|
|
3378
3446
|
try {
|
|
3379
|
-
|
|
3447
|
+
pkg = readPackageJson(cwd);
|
|
3380
3448
|
}
|
|
3381
3449
|
catch {
|
|
3382
|
-
return
|
|
3450
|
+
return null;
|
|
3383
3451
|
}
|
|
3384
|
-
const runsImportgen = scriptRunsImportgen(typeof
|
|
3452
|
+
const runsImportgen = scriptRunsImportgen(typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '');
|
|
3385
3453
|
// A build script that regenerates the import map is worth running even when
|
|
3386
3454
|
// there's nothing for tsc to emit.
|
|
3387
3455
|
if (!shouldBuild && !runsImportgen)
|
|
3388
|
-
return
|
|
3389
|
-
const pkg = earlyPkg;
|
|
3456
|
+
return null;
|
|
3390
3457
|
const existingBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build.trim() : '';
|
|
3391
3458
|
// A build script ensureImportgenInBuild just created for a package that had
|
|
3392
3459
|
// none still needs its tsc pass — treat importgen-only as "no build yet".
|
|
3393
3460
|
const importgenOnly = !!existingBuild && existingBuild.split(/\s*&&\s*/).every(scriptRunsImportgen);
|
|
3394
3461
|
if (shouldBuild && (!existingBuild || importgenOnly)) {
|
|
3395
|
-
//
|
|
3396
|
-
// root tsc excludes them, so a bare "tsc" would leave them unbuilt.
|
|
3397
|
-
const subs =
|
|
3398
|
-
const tscParts = ['tsc', ...subs.map(
|
|
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));
|
|
3465
|
+
const tscParts = ['tsc', ...subs.map(subProjectBuildCommand)];
|
|
3399
3466
|
const newScript = [...(existingBuild ? [existingBuild] : []), ...tscParts].join(' && ');
|
|
3400
3467
|
console.log(colors.yellow(`TypeScript project has no "build" script in ${pkg.name || cwd}`));
|
|
3401
3468
|
for (const s of subs)
|
|
3402
3469
|
console.log(colors.yellow(` sub-project ${s.dir}/ — ${s.reason}`));
|
|
3403
3470
|
const addIt = await confirm(`Add "build": "${newScript}" to ${pkg.name || path.basename(cwd)}'s package.json?`, true);
|
|
3404
|
-
if (addIt)
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
pkg.scripts
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
}
|
|
3411
|
-
else {
|
|
3412
|
-
return true;
|
|
3413
|
-
}
|
|
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)}`));
|
|
3414
3478
|
}
|
|
3415
3479
|
else if (existingBuild) {
|
|
3416
3480
|
await ensureSubProjectsInBuild(cwd);
|
|
@@ -3422,6 +3486,30 @@ export async function buildProject(cwd, opts = {}) {
|
|
|
3422
3486
|
catch { /* keep the script we already have */ }
|
|
3423
3487
|
}
|
|
3424
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;
|
|
3425
3513
|
// Only the sub-projects the final build script really compiles count toward
|
|
3426
3514
|
// freshness — one the user declined to wire in isn't this build's business.
|
|
3427
3515
|
const finalBuild = typeof pkg.scripts?.build === 'string' ? pkg.scripts.build : '';
|