|
288 | 288 | * accept set, deliberately not taken here. |
289 | 289 | */ |
290 | 290 |
|
291 | | -import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; |
| 291 | +import { readFileSync, readdirSync, statSync, existsSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; |
| 292 | +import { tmpdir } from 'node:os'; |
292 | 293 | // [#15487] The SHARED half of the regex-literal recogniser: the 14-word |
293 | 294 | // `REGEX_AFTER_KEYWORD` set, `IDENT_CHAR`, and the literal WALK (character |
294 | 295 | // class, backslash sequences, line-terminator refusal, flags) live ONCE, in |
@@ -493,12 +494,46 @@ function expandWorkspaceMembers(root, glob) { |
493 | 494 | * passes {@link expandWorkspaceMembers} — production and `--self-test` reading |
494 | 495 | * two different populations is the defect #17145 carded, not a spare knob. |
495 | 496 | */ |
496 | | -export function emptyWorkspaceGlobs({ readGlobs, expand }) { |
| 497 | +export function emptyWorkspaceGlobs({ readGlobs, expand, awaiting = new Set() }) { |
497 | 498 | return readGlobs() |
498 | 499 | .filter((glob) => !isExclusionGlob(glob)) |
| 500 | + .filter((glob) => !awaiting.has(glob)) |
499 | 501 | .filter((glob) => expand(glob).length === 0); |
500 | 502 | } |
501 | 503 |
|
| 504 | +/** |
| 505 | + * Workspace globs `pnpm-workspace.yaml` declares AHEAD of the tree they match: |
| 506 | + * the objectui import under `ui/` (objectstack#25677, wired by #25697). Each |
| 507 | + * is excused from the empty-glob refusal above ONLY while |
| 508 | + * {@link AWAITING_IMPORT_MARKER} is absent; the moment the import lands, every |
| 509 | + * one of them must expand to members or it is the finding it always was. The |
| 510 | + * self-test holds the map against the live workspace (every key is a declared |
| 511 | + * glob) and against both states of the marker. Retire the map with the import. |
| 512 | + */ |
| 513 | +export const WORKSPACE_GLOBS_AWAITING_IMPORT = Object.freeze({ |
| 514 | + ui: 'objectui import (objectstack#25677): the `ui` root manifest itself (`object-ui-monorepo`)', |
| 515 | + 'ui/packages/*': 'objectui import (objectstack#25677): the @object-ui/* packages', |
| 516 | + 'ui/apps/*': 'objectui import (objectstack#25677): the Console and the docs site', |
| 517 | + 'ui/examples/*': 'objectui import (objectstack#25677): objectui example apps', |
| 518 | +}); |
| 519 | + |
| 520 | +/** The file whose presence ends the excuse: the imported tree's root manifest. */ |
| 521 | +export const AWAITING_IMPORT_MARKER = 'ui/package.json'; |
| 522 | + |
| 523 | +/** |
| 524 | + * The globs excused from the empty-glob refusal on the tree at `root`: the |
| 525 | + * whole {@link WORKSPACE_GLOBS_AWAITING_IMPORT} key set while the marker is |
| 526 | + * absent, nothing once it exists. |
| 527 | + * |
| 528 | + * @param {string} root |
| 529 | + * @returns {Set<string>} |
| 530 | + */ |
| 531 | +export function awaitingImportGlobs(root) { |
| 532 | + return existsSync(join(root, AWAITING_IMPORT_MARKER)) |
| 533 | + ? new Set() |
| 534 | + : new Set(Object.keys(WORKSPACE_GLOBS_AWAITING_IMPORT)); |
| 535 | +} |
| 536 | + |
502 | 537 | /** |
503 | 538 | * [#16649] Every published workspace member's `src/`, as repo-relative prefixes. |
504 | 539 | * |
@@ -6145,8 +6180,50 @@ function selfTest() { |
6145 | 6180 | ok(emptyWorkspaceGlobs({ |
6146 | 6181 | readGlobs: () => readWorkspaceGlobs(ROOT), |
6147 | 6182 | expand: (g) => expandWorkspaceMembers(ROOT, g), |
| 6183 | + awaiting: awaitingImportGlobs(ROOT), |
6148 | 6184 | }).length === 0, |
6149 | 6185 | 'the LIVE workspace has a glob that expands to nothing — the production run refuses on this too'); |
| 6186 | + // [#25697] The awaiting-import excuse, narrow in every direction: only the |
| 6187 | + // globs it names, only while the marker is absent, and only globs the live |
| 6188 | + // workspace really declares (so the map cannot outlive the import unnoticed). |
| 6189 | + ok(emptyWorkspaceGlobs({ |
| 6190 | + readGlobs: () => ['packages/*', 'ui/packages/*', 'zz-gone/*'], |
| 6191 | + expand: (g) => (g === 'packages/*' ? ['a'] : []), |
| 6192 | + awaiting: new Set(['ui/packages/*']), |
| 6193 | + }).join(',') === 'zz-gone/*', |
| 6194 | + 'an empty glob the awaiting set names is excused; an empty glob it does not name is still reported by name'); |
| 6195 | + ok(emptyWorkspaceGlobs({ |
| 6196 | + readGlobs: () => ['ui/packages/*'], |
| 6197 | + expand: () => [], |
| 6198 | + }).join(',') === 'ui/packages/*', |
| 6199 | + 'with no awaiting set the same empty glob is a finding — the excuse is opt-in at the call site, never a default'); |
| 6200 | + { |
| 6201 | + const liveGlobs = new Set(readWorkspaceGlobs(ROOT)); |
| 6202 | + const keys = Object.keys(WORKSPACE_GLOBS_AWAITING_IMPORT); |
| 6203 | + ok(keys.every((g) => liveGlobs.has(g)), |
| 6204 | + `every awaiting-import glob is one pnpm-workspace.yaml declares — stale keys: ` + |
| 6205 | + `${keys.filter((g) => !liveGlobs.has(g)).join(', ') || 'none'}`); |
| 6206 | + ok(keys.every((g) => typeof WORKSPACE_GLOBS_AWAITING_IMPORT[g] === 'string' && WORKSPACE_GLOBS_AWAITING_IMPORT[g].length > 0), |
| 6207 | + 'every awaiting-import entry carries a reason'); |
| 6208 | + ok(keys.sort().join() === 'ui,ui/apps/*,ui/examples/*,ui/packages/*', |
| 6209 | + 'the awaiting-import map holds exactly the four ui roots and nothing else'); |
| 6210 | + // Both states of the marker, on a scratch root: absent → the whole key |
| 6211 | + // set is excused; present → nothing is, and the import commit meets the |
| 6212 | + // refusal with no further edit here. |
| 6213 | + const scratch = mkdtempSync(join(tmpdir(), 'dispatcher-awaiting-')); |
| 6214 | + try { |
| 6215 | + ok(awaitingImportGlobs(scratch).size === keys.length, |
| 6216 | + 'with the marker absent, every awaiting-import glob is excused'); |
| 6217 | + mkdirSync(join(scratch, 'ui'), { recursive: true }); |
| 6218 | + writeFileSync(join(scratch, 'ui', 'package.json'), '{"name":"object-ui-monorepo","private":true}\n'); |
| 6219 | + ok(awaitingImportGlobs(scratch).size === 0, |
| 6220 | + 'with the marker present, nothing is excused — the excuse voids itself at the import'); |
| 6221 | + } finally { |
| 6222 | + rmSync(scratch, { recursive: true, force: true }); |
| 6223 | + } |
| 6224 | + ok(existsSync(join(ROOT, AWAITING_IMPORT_MARKER)) === (awaitingImportGlobs(ROOT).size === 0), |
| 6225 | + 'the live reading agrees with the live marker'); |
| 6226 | + } |
6150 | 6227 | // [#17145] The same expansion, shown to be the MANIFEST-filtered one and not |
6151 | 6228 | // the raw directory listing. Without this the two could drift apart again |
6152 | 6229 | // silently, which is the whole defect: a glob resolving only to category |
@@ -6516,6 +6593,7 @@ function main() { |
6516 | 6593 | const emptyGlobs = emptyWorkspaceGlobs({ |
6517 | 6594 | readGlobs: () => readWorkspaceGlobs(ROOT), |
6518 | 6595 | expand: (glob) => expandWorkspaceMembers(ROOT, glob), |
| 6596 | + awaiting: awaitingImportGlobs(ROOT), |
6519 | 6597 | }); |
6520 | 6598 | if (emptyGlobs.length > 0) { |
6521 | 6599 | throw new Error( |
|
0 commit comments