fix(runner): stop misdiagnosing a broken package.json as an unpublished version (DEV-2872) - #332
Merged
Merged
Conversation
…ed version (DEV-2872)
DEV-2855 guarded buildSetup so an unparseable /package.json no longer
aborts mount() -- that fix is correct and stays, it fixed a worse bug
(the preview used to die and silently swallow every later keystroke
until "Restart preview"). But it changed which message reaches
describeRuntimeError: the mount now survives, the bundler fails trying
to resolve dependencies out of a manifest it can't parse, and that
failure carries the exact same "could not fetch dependencies" wording
Sandpack uses for an unpublished/unresolvable Handsontable version.
Both populations landed on one rewrite branch that used to have only
one cause, so a visitor who broke their own JSON was told to go check
npm for a version that was never the problem.
describeDependencyFailure (apps/authoring/src/dependencyFailure.ts,
import-free like tier1Report.ts/fetchFailure.ts/sessionDiagnostics.ts)
tells the two apart by parsing our own authored /package.json, not by
matching the bundler's undocumented internal wording ("Cannot read
properties of null (reading 'match')") -- that text is an
implementation detail of Sandpack's bundler choking on a manifest with
no resolvable `dependencies`, and keying on it would stop
discriminating the moment the bundler is bumped. The regex entry gate
still keys on Sandpack's own "could not fetch dependencies" wording,
which is fine: it only gates onto our decision and fails open (falls
through to the bundler's message verbatim) rather than answering
wrong.
The npm sentence is preserved byte-for-byte for its real case (an
unresolvable/unpublished version with a manifest that parses fine, or
no manifest read at all) -- this is a discrimination fix, not a
rewrite of the existing behavior.
Fixture correction (load-bearing): the ClickUp ticket's Sentry
error.value quotes a trailing ", Tier-1 compile failed" clause that is
NOT part of e.message. App.tsx reports the Tier-1 case as
`new Error(report.synthesizeAs.message, { cause: e })`, so Sentry
renders the cause chain; that tail is COMPILE_TITLE, the synthesized
error's own message, not the runtime error's. The test fixture uses
the verbatim, untailed bundler string to prove the module doesn't
accidentally depend on text that never reaches it in production.
Both App.tsx callsites (runtime.onError and the mount().catch) now
pass filesRef.current["/package.json"] as the 4th argument to
describeRuntimeError -- both run inside the mount effect where
filesRef is in scope and holds current editor content at error time.
noUncheckedIndexedAccess makes this string | undefined, which
typecheck (the only coverage of apps/authoring's wiring) proves is
threaded correctly. The container engine branch (DEV-2538/DEV-2553/
DEMOS-9, pinned by pipeline/session-start-failure.test.mjs) is
untouched.
Verified: emitReady() in packages/runtime/src/sandpack.ts fires on any
bundler "done" with no compilatonError, and the editor's writeFile path
streams edits to the runtime unconditionally, not gated on preview
status -- so a fixed manifest does reach a live runtime and a clean
compile does clear the error card. Not independently verified: whether
Sandpack's upstream bundler re-resolves dependencies from a changed
manifest mid-session versus only at initial mount, which is
third-party behavior outside this repo's source.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The manifest sentence claimed "the preview rebuilds itself on the next clean compile". Our half of that is verified — `sandpack.ts`'s `emitReady()` fires on any bundler `done` with no compilation error, and the editor's `writeFile` is not gated on preview status, so a corrected manifest does reach the live runtime. But whether Sandpack re-resolves dependencies from a changed `/package.json` mid-session, or only at mount, is third-party behaviour this repo cannot settle. This module exists because we were telling visitors something false. Shipping an unverified promise in the replacement would swap one false statement for another, so the sentence now stops at "Fix the file to continue." — true under either bundler behaviour. The reasoning is recorded at the return site so that if someone confirms mid-session re-resolution against a live bundler, they can extend the sentence rather than rediscover the question. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes a regression introduced by my own #320 (DEV-2855), caught by verifying that fix in production rather than by a report.
What a visitor sees today
Type an invalid
/package.jsonin the live editor — trivially easy, since every intermediate keystroke is invalid JSON and the file is freely editable — and the error card says:The version is fine. The manifest is unparseable. It sends people to check npm for a problem that isn't there.
Why it started
DEV-2855 guarded
buildSetupso an unparseable/package.jsonno longer abortsmount(). That fix stays — before it, the preview died and every subsequent keystroke was silently swallowed until "Restart preview", which is strictly worse than a misleading sentence.But it changed which message reaches
describeRuntimeError:applyHandsontableVersion: /package.json is not valid JSON: …→ fell through toreturn msg. Internal-looking, but truthful.Could not fetch dependencies, please try again in a couple seconds: Cannot read properties of null (reading 'match')— which the/could not fetch dependencies/ibranch rewrites into the npm sentence.Verified at the deploy boundary, which is also the proof DEV-2855 took effect:
error.valueeec8afab(pre-deploy)applyHandsontableVersion: /package.json is not valid JSON: Expected double-quoted property name at position 455be2676d6d(post-deploy)Could not fetch dependencies, please try again in a couple seconds: Cannot read properties of null (reading 'match')DEMOS-85 is the same fault from the runtime side — same second (
10:13:09), same trace,tier: 1, one visitor in Gurugram.The fix
New import-free
apps/authoring/src/dependencyFailure.ts, following thetier1Report.ts/fetchFailure.ts/sessionDiagnostics.tspattern.describeRuntimeErrorgains apackageJsonparameter and delegates.The npm sentence is preserved byte-for-byte for its real case. Its existing rationale is good and unchanged: Sandpack's "please try again in a couple seconds" reads like a transient hiccup when the actual cause is a pinned version that was never published, which no retry fixes.
The discriminator is our own file state, not the bundler's text. The branch entry still keys on
/could not fetch dependencies/i— fine, since it only gates onto our decision and fails open (returns the bundler's message verbatim) rather than wrong. But the manifest-vs-version decision keys on whether/package.jsonactually parses. Deliberately not onCannot read properties of null (reading 'match'): that is Sandpack choking on a manifest with no resolvabledependencies, it is undocumented, and it would silently stop discriminating on any bundler bump.The manifest is truthfully readable at card time because
pinHandsontableFilesandbuildSetup's guard both return the files unchanged when the manifest is unparseable, so the visitor's broken text is still infilesRef.current.A fixture correction worth recording
The ticket quoted Sentry's
error.valueas…(reading 'match'), Tier-1 compile failed. That trailing clause is not part ofe.message—App.tsxreportsnew Error(report.synthesizeAs.message, { cause: e }), so Sentry is rendering the cause chain. What actually reachesdescribeRuntimeErroris the boundedshow-errortext without it.The regex matches either way, which is precisely why the right string had to be used: a test built on the quoted version would have passed while proving nothing about the real input. The spec's header says so.
Product copy: what it does and does not promise
The manifest sentence stops at "Fix the file to continue." and deliberately does not promise the preview recovers on its own.
Half of that claim is verified:
sandpack.ts'semitReady()fires on any bundlerdonewith no compilation error, and the editor'swriteFileis not gated on preview status, so a corrected manifest does reach the live runtime. What this repo cannot settle is whether Sandpack re-resolves dependencies from a changed/package.jsonmid-session or only at mount.Since this module exists because we were telling visitors something false, shipping an unverified promise in the replacement would swap one false statement for another. The reasoning is recorded at the return site so anyone who confirms mid-session re-resolution against a live bundler can extend the sentence rather than rediscover the question.
Verification
pnpm test—1145 tests / 1143 pass / 0 fail / 2 todo(todos pre-existing)pnpm typecheck— clean across all four packages. This is what proves both callsites satisfy the new signature;noUncheckedIndexedAccess: truemakesfilesRef.current["/package.json"]astring | undefinedthat cannot be transposed intoversion: string.pipeline/dependency-failure.test.mjs— 6/6Revert-check, reproduced independently during review: removing the manifest branch turns tests 1-2 red and leaves 3-6 green (4/6). Guards are labelled as guards in-file, and the parse-detail test computes its expected detail with its own
JSON.parsetry/catch rather than hardcoding a V8 string — JSON error wording is engine-specific, so a literal would pass CI while differing in the product.The container-engine branch is untouched: no word entered its alternation, its sentence is unchanged, and it still runs first, so nothing rewrites a message before it sees it. That branch owns the DEV-2538/DEV-2553/DEMOS-9 contract pinned by
pipeline/session-start-failure.test.mjs.Known gap and residuals
The wiring in
App.tsxhas no unit coverage — it cannot be imported bynode --test(@sentry/react,import.meta.env), which is why the decision lives in a separate module. No text-grep assertion was added to fake it.Residuals documented in the module's docblock: a valid manifest with a bogus non-Handsontable dependency still gets the npm sentence (pre-existing; fixing it would mean keying on whether the bundler's text names
handsontable, i.e. exactly the third-party dependence this change rejects), and a parseable-but-non-object manifest is unchanged from today.Root cause, still open
/package.jsonis freely editable at all:readOnlyexists onCodeEditorand is never wired fromEditorShell, andFileTree.tsx'sPROTECTEDset blocks only rename/delete. Gating it would remove this whole class at the source rather than improving its error message. Open product question on DEV-2852.🤖 Generated with Claude Code
Note
Low Risk
User-facing Sandpack error copy only; logic is isolated, unit-tested, and does not alter container (Tier 2) error handling.
Overview
Fixes DEV-2872: after DEV-2855 let preview mount survive invalid
/package.json, Sandpack’s generic “could not fetch dependencies” message was still rewritten as an unpublished Handsontable on npm error. The authoring shell now routes that Sandpack wording through a new import-freedependencyFailure.ts, which checks whether the live/package.jsontext actuallyJSON.parses and, when it does not, shows a manifest JSON error (with the engine’s parse detail) instead of the npm sentence.describeRuntimeErrortakes the currentpackage.jsonfromfilesRefat error time; container connectivity messaging is unchanged. The real unpublished-version case keeps the same npm copy as before.pipeline/dependency-failure.test.mjspins the discriminator and guards;sandpack-reload.test.mjsonly updates a comment to point at the new module.Reviewed by Cursor Bugbot for commit 1b3f917. Bugbot is set up for automated code reviews on this repo. Configure here.