Skip to content

Commit 9b402db

Browse files
objectstack-fleet[bot]hotlongclaude
authored
fix(cli)!: the JSX page gate's console manifest fallback resolves through @objectstack/console/package.json, so a project without its own manifest gets full component checking (#20589)
Fixes #19922 Clause-②: no ## What this changes `resolveSduiManifest()` (`packages/cli/src/utils/sdui-manifest.ts`) is the one resolver `os validate`, `os compile` / `os build` and `os lint` use to arm the JSX page gate. `os dev` and `os start` run `compile` before they boot when `dist/objectstack.json` is missing or `--compile` is passed, and `dev`'s default watch mode reruns it when a watched file changes. The resolver's second place to look, the copy `@objectstack/console` ships as `dist/sdui.manifest.json`, asked Node for that file by its own subpath. The console's `exports` map publishes `./package.json` alone, so the resolve threw `ERR_PACKAGE_PATH_NOT_EXPORTED`, a `catch` swallowed it, and every project with no `sdui.manifest.json` of its own had its `kind: 'html'` pages checked at parse level only. The fallback now resolves `@objectstack/console/package.json` from the CLI's own location (`import.meta.url`, the CLI's declared dependency in the same fixed release group) and joins `dist/sdui.manifest.json` to its directory, through a new `consoleSduiManifestPath(origin)`. The console's `exports` stays closed. `resolveSduiManifest(cwd, consoleOrigin)` gains an optional origin, used only by the pins. The old module header handed one decision to whoever made this leg reachable: what a broken shipped copy should do. It now gets the project leg's rule. A shipped copy that is present but cannot be read or parsed is `unusable` (new `source: 'console'`), and the command is refused with exit 1, naming the file, with the remedy "reinstall @objectstack/console". It is never read as "not found". With no page to check it is read by nothing and not refused, the same as the project leg. ## This round (the seat's unlock record `5890591366` on #19922) The ledger entry `ui-html-page-div-refused` landed on `main` (#20592, PR #20610), and Version Packages #17076 consumed `.changeset/sdui-manifest-one-producer.md`. This round: 1. **Merged `origin/main` at `f1e921ab8e`** (a merge, not a rebase; merge commit `f9cb969f44`). One conflict: `.changeset/sdui-manifest-one-producer.md`, modify/delete, resolved in favour of `main`'s deletion. `build-json-failure-conversions.e2e.test.ts` and `validate-json-failure-conversions.e2e.test.ts` auto-merged: `main` changed other regions of both, and the `box` fixture line and its docblock sentence survived. The branch's delta against `main` is exactly the 7 intended files. `main` has since gained one commit (`cd901d7a5f`), which touches none of them. 2. **The correction moved into this PR's changeset.** The released note is `@objectstack/console` 17.5.0, patch entry `28ce612`. A new paragraph goes through its closing paragraph one sentence at a time: - two sentences stop being true with this release: the file is no longer "only present in the tarball", and the CLI fallback no longer "keeps parse-level validation"; - two still hold: `exports` is unchanged. No `CHANGELOG.md`, no `content/docs/releases/` and nothing under `packages/spec/` is edited. 3. **The ADR-0087 marker** now reads `not-required (already-registered ui-html-page-div-refused)` with its reason. The gate's verdict: "check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … not-required (already-registered)", exit 0. 4. **Two sentences re-measured and corrected:** - **"the html-tier renderer still renders `div`" was false.** At the pinned objectui `dd3f7e1be3`, read with `git show` from the sibling checkout (nothing checked out, nothing edited): - `packages/components/src/renderers/layout/page.tsx:487-488` builds the html compile's whitelist from `getKnownTypes()` minus `deprecationFor(t, 'html')`; - `packages/components/src/renderers/basic/div.tsx` registers `div` with `deprecated.surfaces: ['json', 'html']`; - `nameHtmlTierReplacement` turns the resulting `forbidden-tag` into a refusal naming the replacement. That pin shipped in `@objectstack/console` 17.5.0: its CHANGELOG entry `3cf6449` says a `kind:'html'` page that authors a `div` "is refused at compile time, and the error names `box`". The changeset now says the console has refused `div` since 17.5.0, and that what is new is every other tag the manifest does not declare. The console's html compile accepts every non-deprecated registered component, while the manifest declares the public contract plus the html intrinsics. Measured below with `avatar`. - **"`objectstack compile` (which `dev` and `start` run first)" was inexact.** It now says exactly when they run it: `dev.ts:319` compiles on `flags.compile` or a missing artifact, and `dev.ts:383` re-runs it in watch mode; `start.ts:228-232` has the same condition. ## Premise and hypotheses, measured Round-1 readings (on `f11b5f20a2`) are kept where they still hold. Round-2 readings are on `77338a7186`: Node v26.7.0, macOS. - **H0 (premise holds).** On unmodified `f11b5f20a2`, a real `os init` project with a `kind: 'html'` page rooted in `div` passes `os validate`, `os compile` and `os lint` at exit 0. Each prints only the parse-level notice, and it does so even with a `cmp`-identical copy of the tracked manifest at `packages/console/dist/sdui.manifest.json`. From `packages/cli/dist`, the old subpath throws `ERR_PACKAGE_PATH_NOT_EXPORTED`. - **H1 (the route finds the file in both layouts).** - Workspace: `consoleSduiManifestPath()` answers `packages/console/dist/sdui.manifest.json`. - Installed package: `npm pack` of `packages/console` with a stand-in dist lists `dist/sdui.manifest.json`. Extracted under a scratch `node_modules`, the old subpath throws `ERR_PACKAGE_PATH_NOT_EXPORTED` from a sibling CLI origin, while `resolveSduiManifest` answers `resolved`. - **H2 (Clause-② arm: narrowing).** Round 2, merged tree, with the console copy present (`cmp`-identical stand-in): - a `div` page gives exit 1 (`jsx-forbidden-tag`, `jsx-unknown-component`); - a `box` page gives exit 0, with no findings; - an `avatar` page gives exit 1 (`jsx-forbidden-tag`, `jsx-unknown-component`). With no copy, all three exit 0 with the notice only. `avatar` is registered at the pin (`renderers/data-display/avatar.tsx:17`) and not deprecated, so the console's html compile renders it and nothing refused it before this change. That is the narrowing the `(narrowing)` arm and BREAKING rest on. - ⚠️ **Round 1 misread this half.** It took "the renderer still renders `div`" from ruling A's reading, which predates objectui#10757, instead of reading the pin. The pin had landed on `main` (#20436) before round 1 ran. For `div`, this change moves a refusal the 17.5.0 console already gives at render time to author time. The arm still holds because of the undeclared tags. - **CLI fixtures the live fallback newly refuses** (round 1, with the console copy present): 23 tests went red across `build-json-failure-conversions.e2e` (5), `validate-json-failure-conversions.e2e` (4) and `jsx-gate-manifest-notice.e2e` (14). `lint-conversion-notices.e2e` stayed green, but its page is refused too. The three conversion fixtures moved from `div` to `box`. The notice file's 14 manifest-less cases are skipped by name where the CLI's own console copy exists. They run in the CI job, which builds no console, and their rules are pinned hermetically in `src/utils/sdui-manifest.test.ts`. - **Examples:** only `examples/app-showcase` carries html pages (three). - **H3 (shipped pages stay clean)**, round 2, merged tree. `main` brought a regenerated `sdui.manifest.json` carrying `tier: 'html'` marks (#20582). - `validate-jsx-pages.production-witness.test.ts`: 5/5 pass. - `examples/app-showcase` with the console copy present: exit 0 on `os validate` / `os compile` / `os lint`, with zero `jsx-*` / `sdui/*` findings. - **H4 (ablation, round 1)**, through `node scripts/ablation-replace.mjs` in WRAP mode: - the anchor `resolve(CONSOLE_PACKAGE_JSON)` went 1 → 0, and `resolve(CONSOLE_SDUI_MANIFEST)` (the old subpath) 0 → 1; - 4 console-leg pins went red ("expected undefined to be defined"); - restore: the blob is back at the HEAD blob `be1f8d4ad33e`, and `git diff HEAD` is empty. The pins import the subject from `src/`, so no `dist/` sits on that path. This round changed no source or test file. ## Tests, at `77338a7186` - The whole CLI `unit` project (`--project unit --maxWorkers=2`) with a real-path `TMPDIR`: 234/234 files, 3347/3347 tests. With the default macOS `TMPDIR`: 232/234. The 2 files are `published-subpath-console.pin` and `published-subpath-hook-body.pin`, 5 cases comparing `/var` against `/private/var`. They are host-only and untouched here. - `pnpm --filter @objectstack/cli typecheck` (`tsc --noEmit` plus `check:test-typecheck`): exit 0. - The four touched nightly `*.e2e` files (`OS_TEST_TIERS=nightly --project integration`): - with the console copy present: 53 passed, 14 skipped; - without it (the CI state): 67/67 passed. The rest of the integration layer is declared to CI. ## Gates, at `77338a7186` - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`: 63 derived, the same 63 as round 1. All 63 exit 0, and `--ran` reconciled "63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3)". `check:dual-build-cjs-loads` first answered PREREQUISITE NOT MET, then exit 0 after building its eight missing packages. - `check-adr-0087-registration --base origin/main`, `check-empty-changeset --base origin/main` (it now reads "No changeset from the merge base modified or deleted by this diff") and `check-changeset-no-major --base origin/main`: all exit 0. - Roster rows that could apply, all exit 0: `check-changeset-fixed`, `check-sdui-manifest` (plus `--self-test`), `check:authz-resolver`, `check:error-code-casing`, `check:filter-alias-parity`, `check:cli-examples-parity`, `check:published-readme-exports`, `check:scaffold-emission-policy`, `check:console-injection`. - `pnpm lint` (full repo, not narrowed): exit 0, no output. - `node scripts/check-issue-citations.mjs --base origin/main`: exit 0. - `check:nul-bytes`: exit 0, plus a control-byte scan of the 7 changed files: 0. **Declared narrowing — verification ran UNLOCKED.** `scripts/pm/os-verify-lock.sh` could not take the shared verify lock on this host: no usable `flock`. The shared verify lock is declared Linux-only (`flock` is util-linux, and a stock macOS does not ship it), so the command below was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for this run, nor for any sibling agent in this container while it ran. every build, test, typecheck, ablation and `pnpm lint` command named above ## Acceptance notes - **Where the `div` → `box` prescription reaches an upgrader.** The CLI's refusal text does not carry it: the gate answers "is not an allowed component" / "is not a known component", from `@objectstack/sdui-parser` (`parse.ts`). The upgrade guide does not carry it either: `packages/spec/scripts/build-upgrade-guide.ts:78` loops majors up to `PROTOCOL_MAJOR`, `PROTOCOL_VERSION` is `17.0.0`, and `docs/protocol-upgrade-guide.md` does not name `ui-html-page-div-refused`. What does carry it: - this changeset's FROM → TO table; - the console's own render-time refusal, which names `box`; - `objectstack migrate meta --from 17`. Measured on a stack with a `div` page, it lists the entry as one of 242 "manual change(s) require your judgment", headed "⚠ [protocol 18] kind:'html' page source …", with `box` as the replacement, and exits 0. - The ledger entry's own `why` text (`packages/spec/src/migrations/entries/semantic/18.ui-html-page-div-refused.ts`, the spec seat's file) still says "`objectstack compile` (which `dev` and `start` run first)", the phrasing corrected here. Noted, not edited. - Release order, flagged by the seat in `5890591366`: Version Packages PR #20639 carries the ledger entry's changeset. If it merges before this PR, the ledger row ships one release ahead of the CLI refusal it describes. - `packages/cli/src/utils/scaffold-validate.ts` (the note at :128-:133) was re-read. It is true now, so it is not edited. A pre-existing imprecision stays as it was: `os init` reads the invoker's directory, which may carry its own `sdui.manifest.json` (this repository's root does). - Comment drift outside this claim, noted only: - `.github/workflows/lint.yml` (:899) and `scripts/check-sdui-manifest.mjs` (:28-30, :240) still say `resolveSduiManifest()` degrades to parse-only silently; - the header of `packages/lint/src/validate-jsx-pages.ts` still calls manifest validation "not wired"; - `docs/qa/platform-checklist/areas/studio-authoring.json` describes the showcase tree as flex/div/a. - The five macOS-only `published-subpath-*` failures come from a `tmpdir()` path compared with the real path that module resolution returns. They are host-specific. - Measurement scaffolding was all in scratch, or in this worktree's gitignored `packages/console/dist/`, with each stand-in trap-removed. `git status --porcelain` printed 0 lines after every run. --- _Generated by [Claude Code](https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289)_ --------- Co-authored-by: Jack Zhuang <50353452+hotlong@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 73213a5 commit 9b402db

7 files changed

Lines changed: 320 additions & 53 deletions
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
---
2+
'@objectstack/cli': minor
3+
---
4+
5+
fix(cli)!: a project with no `sdui.manifest.json` of its own has its `kind: 'html'` pages checked against the manifest `@objectstack/console` ships, so `div` and every other undeclared tag or prop is refused (#19922)
6+
7+
Clause-②: no (narrowing)
8+
9+
<!-- adr-0087: not-required (already-registered ui-html-page-div-refused) The semantic ledger entry for this narrowing landed on main before this change, in its own pull request, so this diff adds none. `objectstack migrate meta --from 17` lists it among the manual changes, with `box` as the replacement for `div`. -->
10+
11+
**BREAKING for `kind: 'html'` pages in projects without their own manifest.**
12+
13+
**What changed.** `objectstack validate`, `objectstack compile` / `build` and
14+
`objectstack lint` check the `source` of a `kind: 'html'` page against an SDUI
15+
component manifest. (`dev` and `start` run `compile` before they boot when
16+
`dist/objectstack.json` is missing or `--compile` is passed, and `dev`'s default
17+
watch mode reruns it when a watched file changes.) They look first for the
18+
`sdui.manifest.json` in the directory the command runs in, then for the copy
19+
`@objectstack/console` ships as `dist/sdui.manifest.json`. The second lookup asked for that file by a subpath
20+
the console package does not export, so it always failed, and a project with no
21+
manifest of its own had its html pages checked at parse level only: syntax and
22+
structure, never which components and props they use. The lookup now reaches the
23+
shipped copy, and those pages get full component and prop validation. A tag or
24+
prop the manifest does not declare is refused (`jsx-forbidden-tag`,
25+
`jsx-unknown-component`, `jsx-unknown-prop`), naming the page and the tag, and
26+
the command exits 1.
27+
28+
**What was refused before, and what is new.** The console has refused a `div` on
29+
a `kind: 'html'` page since `@objectstack/console` 17.5.0: when the page renders,
30+
its in-browser html compile answers `forbidden-tag`, naming `box`. These commands
31+
now give that answer while you author. What they refuse that nothing refused
32+
before is every other tag the manifest does not declare. The console's html
33+
compile accepts every component its registry knows that is not deprecated there,
34+
while the published manifest declares only the public component contract and the
35+
html tier's intrinsic tags. So a page using, for example, `avatar` or `checkbox`
36+
renders in the console and is refused here.
37+
38+
## FROM → TO
39+
40+
| you wrote | write instead |
41+
|:--|:--|
42+
| `<div>` … `</div>` in a `kind: 'html'` page | `<box>` … `</box>`, which takes the same `className` and children |
43+
| any other tag or prop the command names | a component and prop the manifest declares |
44+
45+
**What is not affected.** A project that keeps its own `sdui.manifest.json` is
46+
checked against that file, as before. `kind: 'react'` pages and pages authored
47+
as regions are not read by this gate. With no manifest reachable at all, the
48+
pages are still checked at parse level, and the notice that says so is
49+
unchanged.
50+
51+
**A damaged install is refused, not skipped.** A shipped copy that is present
52+
but cannot be read or parsed stops the command with exit 1, naming the file,
53+
with the remedy: reinstall `@objectstack/console`.
54+
55+
**A correction to the 17.5.0 note on this manifest.** The `@objectstack/console`
56+
17.5.0 patch entry `28ce612`, the one that says the prebuilt Console dist now
57+
ships `dist/sdui.manifest.json`, ends with a paragraph that this release changes,
58+
sentence by sentence:
59+
60+
- "For now the file is only present in the tarball." No longer true: the CLI
61+
reads it, as described above.
62+
- "This package's `exports` map exposes `./package.json` and nothing else, so
63+
resolving `@objectstack/console/dist/sdui.manifest.json` through `exports`
64+
fails with `ERR_PACKAGE_PATH_NOT_EXPORTED`." Still true: the `exports` map is
65+
unchanged.
66+
- "Anything that resolves through `exports` cannot read the file yet." Still
67+
true. To read the file, resolve `@objectstack/console/package.json` and join
68+
`dist/sdui.manifest.json` to its directory.
69+
- "That includes the CLI's JSX-page manifest fallback, which catches the error
70+
and keeps parse-level validation, as before." True of the 17.5.0 CLI, false
71+
from this release: the fallback now reads the file that way, so a project
72+
without its own manifest is checked against the shipped copy.

‎packages/cli/src/utils/sdui-manifest.test.ts‎

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,18 @@
1414
*/
1515

1616
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
17-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
17+
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
18+
import { createRequire } from 'node:module';
1819
import { tmpdir } from 'node:os';
19-
import { join } from 'node:path';
20+
import { dirname, join } from 'node:path';
21+
import { pathToFileURL } from 'node:url';
2022
import { validateJsxPages } from '@objectstack/lint';
2123
import {
22-
CONSOLE_SDUI_MANIFEST_SPECIFIER,
24+
CONSOLE_SDUI_MANIFEST,
2325
JSX_PARSE_LEVEL_ONLY_RULE,
2426
PROJECT_SDUI_MANIFEST_FILE,
2527
SduiManifestRefusalError,
28+
consoleSduiManifestPath,
2629
countJsxGatePages,
2730
jsxGateStacks,
2831
printJsxGateNotices,
@@ -59,10 +62,11 @@ const PACKAGE_CARRIED: Record<string, Record<string, unknown>> = {
5962

6063
const ABSENT: SduiManifestResolution = {
6164
status: 'absent',
62-
lookedAt: ['/proj/sdui.manifest.json', CONSOLE_SDUI_MANIFEST_SPECIFIER],
65+
lookedAt: ['/proj/sdui.manifest.json', CONSOLE_SDUI_MANIFEST],
6366
};
6467
const UNUSABLE: SduiManifestResolution = {
6568
status: 'unusable',
69+
source: 'project',
6670
path: '/proj/sdui.manifest.json',
6771
reason: 'it is not valid JSON (x)',
6872
};
@@ -82,13 +86,17 @@ describe('resolveSduiManifest — says WHY it has no manifest', () => {
8286
expect(r).toEqual({ status: 'resolved', manifest: MANIFEST, path: join(dir, PROJECT_SDUI_MANIFEST_FILE) });
8387
});
8488

85-
// Holds in any checkout: `packages/console/dist/` is gitignored and absent
86-
// unless the console is built, and today the specifier is not in the
87-
// console's `exports` either. Both legs are named, in order.
88-
it('absent: names the project path, then the console specifier', () => {
89-
expect(resolveSduiManifest(dir)).toEqual({
89+
// Hermetic: the console is located from an origin nothing resolves from
90+
// (`createRequire` refuses a relative one), so the answer does not depend on
91+
// whether this checkout has built the console. ⚠️ An absolute origin in an
92+
// empty directory is NOT that: a runner started through pnpm's `.bin` shim
93+
// inherits a NODE_PATH carrying the virtual store's hoisted packages, and
94+
// `@objectstack/console` resolves from anywhere through it. Both legs are
95+
// named, in order; the console-leg block below pins the absolute spelling.
96+
it('absent: names the project path, then the console copy', () => {
97+
expect(resolveSduiManifest(dir, 'not-an-absolute-origin.mjs')).toEqual({
9098
status: 'absent',
91-
lookedAt: [join(dir, PROJECT_SDUI_MANIFEST_FILE), CONSOLE_SDUI_MANIFEST_SPECIFIER],
99+
lookedAt: [join(dir, PROJECT_SDUI_MANIFEST_FILE), CONSOLE_SDUI_MANIFEST],
92100
});
93101
});
94102

@@ -103,6 +111,7 @@ describe('resolveSduiManifest — says WHY it has no manifest', () => {
103111
const r = resolveSduiManifest(dir);
104112
expect(r.status).toBe('unusable');
105113
if (r.status !== 'unusable') return;
114+
expect(r.source).toBe('project');
106115
expect(r.path).toBe(join(dir, PROJECT_SDUI_MANIFEST_FILE));
107116
expect(r.reason).toMatch(reason);
108117
});
@@ -115,6 +124,113 @@ describe('resolveSduiManifest — says WHY it has no manifest', () => {
115124
});
116125
});
117126

127+
/**
128+
* The `package.json` of the REAL `@objectstack/console` this package depends
129+
* on, resolved the way any installed dependency is — through `node_modules` —
130+
* so the layouts below carry the console's actual `exports` map, which is what
131+
* decides whether a subpath resolves at all.
132+
*/
133+
const REAL_CONSOLE_PACKAGE_JSON = createRequire(import.meta.url).resolve('@objectstack/console/package.json');
134+
135+
/**
136+
* An installed-package layout under `root`: `node_modules/@objectstack/console`
137+
* carrying the real console `package.json`, plus a `dist/sdui.manifest.json`
138+
* with `body` (`null` for a console that ships none, as 17.0.0 to 17.4.0 did).
139+
* Returns the origin a CLI installed beside it resolves from.
140+
*/
141+
function installConsole(root: string, body: string | null): URL {
142+
const pkgDir = join(root, 'node_modules', '@objectstack', 'console');
143+
mkdirSync(join(pkgDir, 'dist'), { recursive: true });
144+
writeFileSync(join(pkgDir, 'package.json'), readFileSync(REAL_CONSOLE_PACKAGE_JSON, 'utf8'));
145+
if (body !== null) writeFileSync(join(pkgDir, 'dist', 'sdui.manifest.json'), body);
146+
return pathToFileURL(join(root, 'node_modules', '@objectstack', 'cli', 'dist', 'index.js'));
147+
}
148+
149+
describe('the console leg — the copy @objectstack/console ships is reached, through its package.json', () => {
150+
let root = '';
151+
let project = '';
152+
beforeEach(() => {
153+
// Real path: module resolution answers with one (`/var` is `/private/var` on macOS).
154+
root = realpathSync(mkdtempSync(join(tmpdir(), 'os-sdui-console-')));
155+
project = join(root, 'project');
156+
mkdirSync(project);
157+
});
158+
afterEach(() => {
159+
rmSync(root, { recursive: true, force: true });
160+
});
161+
162+
// The production default: from the CLI's OWN location, where its declared
163+
// dependency lives. Asking for the file by its own subpath resolves nothing
164+
// (the console's `exports` publishes `./package.json` alone), so this reds
165+
// the moment the leg goes back to that spelling.
166+
it("locates the CLI's own @objectstack/console dependency, whether or not it is built", () => {
167+
const path = consoleSduiManifestPath();
168+
expect(path).toBeDefined();
169+
expect(path!.endsWith(join('dist', 'sdui.manifest.json'))).toBe(true);
170+
const owner = JSON.parse(readFileSync(join(dirname(dirname(path!)), 'package.json'), 'utf8'));
171+
expect(owner.name).toBe('@objectstack/console');
172+
});
173+
174+
it('resolved: a project with no manifest of its own is checked against the shipped copy', () => {
175+
const origin = installConsole(root, JSON.stringify(MANIFEST));
176+
expect(resolveSduiManifest(project, origin)).toEqual({
177+
status: 'resolved',
178+
manifest: MANIFEST,
179+
path: join(root, 'node_modules', '@objectstack', 'console', 'dist', 'sdui.manifest.json'),
180+
});
181+
});
182+
183+
it("resolved: the project's own manifest is read first", () => {
184+
const origin = installConsole(root, JSON.stringify({ components: {} }));
185+
writeFileSync(join(project, PROJECT_SDUI_MANIFEST_FILE), JSON.stringify(MANIFEST));
186+
expect(resolveSduiManifest(project, origin)).toEqual({
187+
status: 'resolved',
188+
manifest: MANIFEST,
189+
path: join(project, PROJECT_SDUI_MANIFEST_FILE),
190+
});
191+
});
192+
193+
it('absent: a console that ships no manifest is named by the absolute path looked at', () => {
194+
const origin = installConsole(root, null);
195+
expect(resolveSduiManifest(project, origin)).toEqual({
196+
status: 'absent',
197+
lookedAt: [
198+
join(project, PROJECT_SDUI_MANIFEST_FILE),
199+
join(root, 'node_modules', '@objectstack', 'console', 'dist', 'sdui.manifest.json'),
200+
],
201+
});
202+
});
203+
204+
it('unusable: a damaged shipped copy is refused with its own remedy, never read as "not found"', () => {
205+
const origin = installConsole(root, '{ "components": [ oops');
206+
const r = resolveSduiManifest(project, origin);
207+
expect(r).toMatchObject({
208+
status: 'unusable',
209+
source: 'console',
210+
path: join(root, 'node_modules', '@objectstack', 'console', 'dist', 'sdui.manifest.json'),
211+
});
212+
213+
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
214+
try {
215+
let thrown: unknown;
216+
try {
217+
resolveJsxGateManifest(HTML_STACK, r);
218+
} catch (e) {
219+
thrown = e;
220+
}
221+
expect(thrown).toBeInstanceOf(SduiManifestRefusalError);
222+
const e = thrown as SduiManifestRefusalError;
223+
expect(e.message).toContain(join('@objectstack', 'console', 'dist', 'sdui.manifest.json'));
224+
// The remedy names the package to reinstall, not the file to edit.
225+
const remedy = e.hints[e.hints.length - 1];
226+
expect(remedy).toContain('@objectstack/console');
227+
expect(remedy).not.toContain(PROJECT_SDUI_MANIFEST_FILE);
228+
} finally {
229+
errSpy.mockRestore();
230+
}
231+
});
232+
});
233+
118234
describe('countJsxGatePages — the pages the JSX gate checks against a manifest', () => {
119235
// The kind set is `validateJsxPages`'s own, read by DRIVING it: with a
120236
// manifest that declares no components, every page the gate compiles against

0 commit comments

Comments
 (0)