Skip to content

fix(portal): keep the preview up and report a broken specification while serving - #434

Open
saeedjamshaid wants to merge 7 commits into
devfrom
saeedjamshaid/serve-spec-errors
Open

saeedjamshaid wants to merge 7 commits into
devfrom
saeedjamshaid/serve-spec-errors

Conversation

@saeedjamshaid

@saeedjamshaid saeedjamshaid commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator

The problem

Under portal serve, a specification saved half written, a broken or missing file of a split specification, or a deleted specification made every address of the preview answer 500, Markdown pages included. The terminal said nothing, and the open tab needed F5 once the file was fixed.

Before the preview starts, and in portal generate, only the documents at the top of spec/ were checked. A file that a $ref named, missing or half written, passed the check and failed the build later, and a document that did not parse was reported with no reason.

What changes

  • The preview survives a broken specification. Under vite dev, openApiSource() loads each document on its own and leaves out the reference pages of one that fails, so the rest of the portal is served. A build still fails on it.
  • Every file is read as the bundler reads it. A document, and each file a $ref names, is read by what it holds, as @scalar/json-magic reads it: JSON, else YAML when the text holds a key and does not open as JSON does within its first 50 characters. One the bundler fails on, or reads as nothing (an empty file, one holding only comments), is refused as unreadableSpec, with the reason: the parser's own words for a syntax error, and the CLI's for the rest, such as "it is empty". The parser used to be picked by the file's extension.
    • Compared with dev: a .json file holding YAML is accepted, as the portal builds it; an empty, comment-only or key-less file at the top of spec/ is refused, where it was ignored (an empty .json already was).
  • The check follows every $ref. The files each document refers to are followed as the bundler follows them, each read once per document, a level of references at a time and in parallel within a level. One that is missing is refused as missingSpecReference, and one that cannot be read as unreadableSpec, each naming the file that refers to it. A missing file that is on disk in another case, as a file system that matches case misses it, is named as it is spelt.
    • The rules were read from the bundler's source: any string $ref on any object or list, resolved from the folder of the file it is written in, but not past a $ref's siblings or x-ext/x-ext-urls. A $ref that names an $id of the root document names a schema, not a file, in every file the document is bundled from.
    • A table of cases runs the real bundleSpec and the check on the same files, so the two cannot drift apart. Each row says whether the bundler builds, fails, or reads a file as nothing, which leaves its reference empty; the check accepts only what builds.
    • SpecBundle records each file read and lists the ones still to read, and answers the document's operations; the source context does the reading.
  • A parse error says where. OpenApiDocument.parse returns a Result carrying the parser's message on one line, with its line and column.
  • Each save in spec/ is checked while serving, against the same rules:
    • a mistake is reported as portal generate reports it, and the next save that fixes it says so once;
    • a document added to spec/ or taken out of it is warned of once, since the preview serves only the documents it started with;
    • a document caught mid-save, gone for a moment, is reported as "no longer there", and the save that follows fixes it.
  • One "refused, then fixed once" state. PreviewRefusal replaces the flag PreviewContent and PreviewConfig each kept. A message the watches of apimatic.json, content/ and spec/ share is one prompt, keyed by what is watched, and what each preview says after an accepted save is decided in its prompt. Two sentences change for apimatic.json: a fault while checking a save now says its changes "could not be checked" (it said "could not be applied to the preview"), and a lost watch no longer says "further edits".
  • The Live preview note, the command description and the README mention specification edits.
  • A static file under a symlinked path is served. The static directory is named by its real path in the preview's server.fs.allow, as the dependency directories already are: Vite resolves a symlink before comparing, so under macOS's /tmp or /var a static file asked for before its page, as Safari does in apimatic-io issue 2287, was refused with 403. The serve e2e test, now run on a copy of the fixture in the temp directory, found it on macOS.
  • The merge with dev reformatted src/actions/action-result.ts and src/prompts/api/transform.ts: lint-staged ran Prettier over them. Nothing else in them changed.

What it prints

■  'spec/components/schemas/Pet.yaml', which 'spec/paths/pets.yaml' refers to, could not be read: Flow sequence in block collection must be sufficiently indented and end with a ] at line 3, column 1.
│  The preview leaves what it cannot read out of the API reference until 'src/spec' is fixed; a build would stop here.

■  'spec/components/schemas/Owner.json', which 'spec/paths/pets.yaml' refers to, could not be read: it is empty.

■  'spec/paths/pets.yaml' refers to 'spec/components/schemas/Pet.yaml', but there is no such file.
│  A '$ref' names a file from the folder of the file it is written in.

■  'spec/paths/pets.yaml' refers to 'spec/components/schemas/pet.yaml', but the file is spelt 'spec/components/schemas/Pet.yaml' on disk.

■  'spec/petstore.json' could not be read: it is no longer there.

◆  'src/spec' is fixed; a build would accept it again.

▲  The documents in 'src/spec' are not the ones the preview started with: 'pets.json' was added, and 'petstore.json' was removed.
│  Restart the preview to match them; until then it shows the ones it started with that are still there.

Testing

  • pnpm test: 1864 passing, with the source and the tests type-checked and ESLint clean. New tests cover each $ref rule, how a file is read, the bundler table, SpecBundle, the messages, the two preview state types, and the spec watch in portal serve on a copy of the fixture.
  • APIMATIC_E2E=1 serve and build: 64 passing. The new serve test saves a specification half written, checks that its reference is left out while a Markdown page is still served, and that the reference comes back once the file is fixed.
  • Live portal serve sessions, with a browser tab left open, on a single-file specification and on a split one in a folder named with a space and an accent: a half-written root document, a renamed, added or deleted document, a missing $ref'd file, half-written YAML and JSON in referenced files, and CTRL+C. All were reported as above, and the open tab recovered without F5. These ran before 865ac82 and ecd4b0e, which changed how a file is read and some of the wording.
  • A real split specification (1,336 files) was accepted as before, with the same 234 operations; the check took 0.27 s on it, against 0.17 s before. Not measured again since 865ac82. At ecd4b0e, on generated split specifications, the check takes a median 93 ms on 1,301 files that refer to each other, and 605 ms on a chain of 1,000 files.

Not in this PR

  • A mistake only the portal itself refuses, such as a duplicate operationId, is still not reported while serving. Its reference is left out rather than failing every page.
  • Files a $ref names outside src/spec are checked, but saving one is not watched.
  • A $ref below an $id is not checked: the bundler resolves it against the $id, which names no folder of the specification, so it is left to the build.

🤖 Generated with Claude Code

saeedjamshaid and others added 4 commits October 7, 2026 14:58
Under `vite dev`, one document saved half written failed the module every route imports, so every
page of the preview answered 500 until it was fixed. The preview now leaves that document's
reference pages out and serves the rest; a build still fails on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… document cannot be read

Startup read only the documents at the top of `spec/`, so a split specification whose `$ref` named
a missing or half-written file passed the check, then failed late in the build or rendered with
that reference empty. The files each document refers to are now followed as the portal's bundler
follows them, each read once and in parallel, and one that is missing or unreadable is refused,
naming the file that refers to it. A document that does not parse is reported with the parser's
reason, which carries its line and column. A table of cases holds the check to `bundleSpec`.

The preview's dev-only handling in `openApiSource` is inlined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…runs

A specification saved with a mistake left its reference out of the preview and said nothing. Each
save in `spec/` is now checked as `portal generate` checks it: a mistake is reported with the
reason, the next good save says it is fixed, and a document added or removed is warned of once,
since the preview serves only the documents it started with. The live preview note says so.

The "refused, then fixed once" state the content and config previews each kept is one type now,
and the sentences the watch messages shared live once in the prompts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ec-errors

#405 added the same `resolveSpecs` for quickstart, so one is kept for both callers, and the test
this branch had for it gives way to the ones #405 brought. `specRejected` drops the
`offerQuickstart` option #405 removed from `reportSourceProblem`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
saeedjamshaid and others added 3 commits October 9, 2026 15:33
…w's allow list

Vite resolves a symlink before comparing a requested file against `server.fs.allow`, and the
static directory was listed as written. A project reached through a symlink, as anything under
macOS's `/tmp` or `/var` is, had a static file asked for before its page refused with 403, which
is the request apimatic-io#2287 is about. The directory is now named by its real path, as the
dependency directories beside it already are.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the review

The check read a file by its extension, and took the `$id`s a `$ref` may name from the file the
`$ref` sat in, so a referenced `.json` holding YAML, or a `$ref` naming an `$id` of the root
document, was refused although the portal builds it. Every file is now read as the bundler reads
it, by what it holds, and one the bundler fails on or reads as nothing is refused with the reason.
The `$id`s a reference may name are the root document's, as in the bundler.

A document gone from `spec/` mid-save is reported as unreadable rather than left out, which had the
preview warn that it was removed. `specs()` is back within Sonar's cognitive complexity limit, the
watch messages say what the preview leaves out, and a test replaces the comment on the order in
which `PreviewConfig.show` accepts a refusal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the preview's messages

The `$ref` walk leaves the source context for `SpecBundle`, a plain type that records each file
read and lists the ones still to read, while the context reads them a level at a time. A spec
problem's reason is a kind the prompt words, so a document gone mid-save is "no longer there"
rather than an ENOENT message, and a missing referenced file is a `MissingFile`, which names its
spelling on disk in another case. The 50 characters the bundler looks at for JSON are a named
constant, held to the bundler by a row of its table.

The preview's watch messages are one prompt each, keyed by what is watched, and each preview
type's answer goes straight to its prompt. A save of `apimatic.json` that throws now says its
changes could not be checked, and a lost watch on it no longer says "further edits".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant