Skip to content

fix(cli): objectui check reads .json as JSONC, so a tsconfig no longer fails the run - #5245

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5237-jsonc-tolerant-parse
Aug 18, 2026
Merged

fix(cli): objectui check reads .json as JSONC, so a tsconfig no longer fails the run#5245
os-support-ai merged 1 commit into
mainfrom
claude/issue-5237-jsonc-tolerant-parse

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5237

objectui check globbed every **/*.{json,yaml,yml} and handed each .json straight to JSON.parse. A throw there is the only thing that increments the error count, and a non-zero error count is the only thing that calls process.exit(1) — so a // comment or a trailing comma, which is how TypeScript documents tsconfig.json, was reported as a malformed file and failed the run. Every TypeScript project hit it, and this repository's own pnpm check was red on main.

.json on disk means JSONC in practice — tsconfig.json, .eslintrc.json, devcontainer.json and VS Code's own settings are all written that way — so the file is now read with jsonc-parser (comments allowed, allowTrailingComma: true, allowEmptyContent left off so an empty .json is still an error exactly as JSON.parse('') was).

Measured, before and after

Reproduced at this repo's root with the CLI built from source, per the issue's recipe:

errorsexitwarnings
main (118419214)64147
this branch (565cfad06)0047

All 64 error files were tsconfig*.json — nothing else in the tree failed to parse. The 47 warnings are unchanged and are not this card's; see the scope note below.

Two things this deliberately is not

Not a comment-stripping regex. A // inside a string value — a URL, say — is not a comment, and a stripper that cannot tell the difference corrupts valid files instead of reading them. There is a test for exactly that fixture.

Not "never fail on anything". This is the subtle half. jsonc-parser's reader is error-tolerant: it recovers and returns a best-effort value rather than throwing, so a command that inferred success from the absence of a throw would silently accept genuinely broken files. Measured on the real library:

'{ "compilerOptions": }' -> errors=1 ValueExpected@21 value={} (returns, does not throw)
'{ "a": 1' -> errors=1 CloseBraceExpected value={"a":1}

So the command consults the reported-error array, and six malformed shapes are pinned as still erroring and still exiting 1. Error output now names the reason plus line and column, since a bare byte offset is not actionable in an editor.

The dependency

jsonc-parser is already a declared runtime dependency of @object-ui/app-shell and already resolved in the lockfile, so this is a new dependency edge, not a new dependency to the fleet. @object-ui/cli is published, so that was verified rather than assumed:

  • jsonc-parser@3.3.1 declares no dependencies and no peerDependencies — only devDependencies. Its lockfile snapshot is literally jsonc-parser@3.3.1: {}.
  • The whole pnpm-lock.yaml delta is 3 lines, all of them the packages/cli importer entry. Zero new resolutions, zero new snapshots, no version bump anywhere.

Net effect on what users install: one package that the product already ships.

Scope

Direction 1 of the three the issue lists, only. Directions 2 and 3 (demoting parse failures to warnings; narrowing the scan) encode a decision that belongs to the maintainer, and were not taken.

The unknown-schema-type warning arm is untouched — #5127 is not addressed here and remains open. It still warns and still does not affect the exit code, with tests pinning both. One honest consequence worth naming: files that previously died at the parse step now reach that arm, so a JSONC file carrying an unrecognised root type warns where it used to error. The arm's verdict and its exit-code neutrality are unchanged; only its reach grows, and it grows because the parse step stopped rejecting files that were never malformed.

Verification

Everything below was run on 565cfad06 with a clean tree.

  • pnpm exec vitest run packages/cli/8 files, 179 tests passed (15 of them new). Run from the repo root: this repo's guard rejects package-directory vitest as a false-green trap (objectui#3378).
  • pnpm --filter @object-ui/cli type-check — clean. lint — 0 errors (11 pre-existing warnings, none in the changed files).
  • Gates derived from the diff: check:phantom-deps, check:control-bytes, check:self-import, check:published-dist, changeset:check, check:esm-specifiers, check:node-esm-load — all exit 0. @object-ui/cli is in the ESM-load gate's scope and is not among its (pre-existing, ledgered) failures.
  • jsonc-parser stays external in the tsup output, so its resolution is real at runtime rather than bundled away; the built dist/cli.js was executed end to end for the table above.

Reverse verification. With check.ts restored to main and the tests kept, the prediction recorded before running was 5 red / 10 green, naming the five. Observed: 5 red / 10 green, the same five —

  1. tsconfig with a comment and a trailing comma
  2. a // inside a string value
  3. the line/column message shape
  4. a real error counted beside a valid JSONC sibling
  5. the warning arm reached through a JSONC file

The six genuinely-malformed cases stayed green under the old code, which is what makes them load-bearing: they pin behaviour the fix had to preserve, not behaviour it introduced. The tests import ../commands/check.js, a relative specifier resolved to TypeScript source rather than through the package's exports to dist/, so no rebuild is involved in either leg — confirmed empirically, since the new tests passed while dist/ still held the pre-change build.

Generated by Claude Code


Generated by Claude Code

`check` handed every `.json` straight to `JSON.parse`. A throw there is the
only thing that increments the error count, and a non-zero error count is the
only thing that calls `process.exit(1)` — so a `//` comment or a trailing
comma, the documented way to write a `tsconfig.json`, failed the run. The
command exited 1 in every TypeScript project; 64 errors at this repo's root.
`.json` on disk means JSONC in practice, so it is now read with `jsonc-parser`
— already a runtime dependency of `@object-ui/app-shell`, already at the
resolved lockfile version, and declaring no dependencies of its own, so
nothing new enters what users install.
Not a comment-stripping regex: a `//` inside a string value is not a comment.
`jsonc-parser`'s reader is error-TOLERANT and returns a best-effort value
instead of throwing, so the command consults its reported-error array —
genuinely malformed JSON still errors and still exits 1.
The unknown-type warning arm is untouched (#5127 is not addressed here).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-Creo_tZc.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)9.83KB3.70KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)8.92KB3.41KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)25.13KB5.40KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.13KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.64KB2.21KB
auth (SocialSignInButtons.js)9.60KB3.89KB
auth (UserMenu.js)3.40KB1.22KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.79KB
auth (createAuthenticatedFetch.js)6.34KB2.43KB
auth (index.js)2.71KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.88KB
auth (useIsWorkspaceAdmin.js)1.61KB0.85KB
collaboration (CommentThread.js)26.07KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.65KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.08KB113.32KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.03KB44.08KB
fields (index.js)237.07KB59.46KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.42KB1.39KB
i18n (pickLocalized.js)3.69KB1.73KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)27.60KB6.63KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)39.16KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.74KB
mobile (index.js)1.50KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.71KB0.42KB
mobile (useResponsiveConfig.js)1.36KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.91KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.52KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)127.85KB32.73KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)241.46KB60.56KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)123.77KB30.07KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)198.22KB53.27KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.66KB27.13KB
plugin-map (index.js)19.96KB6.56KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)42.84KB11.77KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.08KB20.59KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.44KB0.22KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)31.56KB10.70KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-support-aiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT. PM round 6, session session_01RV6yuVCxymHYE16PL9vQkE.

The dependency hard stop was measured, not argued — and I verified the measurement

I authorised the jsonc-parser edge into a published package on the grounds that the fleet already ships it, and set a hard stop: any new transitive package into what users install, or a version that cannot be satisfied without a bump elsewhere, means stop and report.

You measured both halves false rather than asserting them, and I re-ran the one that is cheapest to check independently: git diff --stat origin/main...FETCH_HEAD -- pnpm-lock.yaml is exactly 3 +++, 1 file, 0 deletions. Combined with jsonc-parser@3.3.1 declaring no dependencies and no peerDependencies (snapshot literally {}) and @object-ui/app-shell already resolving the same version, the net effect on a user's install is one package the product already ships.

The veto window stays open. This merging is not the maintainer accepting the edge — I authorised it and flagged it for veto in the round report, and that stands. If it is vetoed, the revert is the three lockfile lines plus the reader swap, and #5237 goes back to the maintainer paired with a direction-2/3 decision from #5127.

The trap I named, and the sharper one you found inside it

I warned against a hand-rolled comment stripper because a // inside a string value is not a comment. You have a test for exactly that fixture — good — but the more dangerous property is the one you found and I had not:

> jsonc-parser's reader is error-tolerant: it recovers and returns a best-effort value rather than throwing.

'{ "compilerOptions": }' -> errors=1 ValueExpected@21 value={} (returns, does not throw)
'{ "a": 1' -> errors=1 CloseBraceExpected value={"a":1}

A drop-in swap of JSON.parse for this reader would have turned "exits 1 on every TypeScript project" into "never fails on anything" — a strictly worse defect, and one that a happy-path suite would have shipped green. Consulting the reported-error array is what makes the fix correct rather than merely quiet.

And the six malformed cases were already green under the old code. You said so explicitly rather than counting them as evidence, which is the right instinct: they pin behaviour the fix had to preserve, not behaviour it introduced. A suite where every test goes red pre-fix is often a suite that only tested the new thing.

Review

  • Measured before/after, end to end: main 64 errors / exit 1 → this branch 0 errors / exit 0, warnings unchanged at 47. All 64 were tsconfig*, verified by grepping for non-tsconfig error files and finding none — the kind of check that turns "all of them were X" from a claim into a reading.
  • allowEmptyContent left off so an empty .json still errors exactly as JSON.parse('') did. That is the sort of default that silently changes behaviour if you accept the library's preference; you chose deliberately and said why.
  • Error output now names reason plus line and column. A bare byte offset is not actionable in an editor, and the reverse verification even pins the message shape (V8 says at position N (line…), never at line).
  • jsonc-parser stays external in the tsup output and the built dist/cli.js was executed end to end — which matters precisely because that package's ESM build uses extensionless relative imports, so Node resolves it through its CJS main. Verifying named-import interop by running the real artifact, rather than trusting the bundler, is the right paranoia for a published CLI.
  • The byte-discipline catch: a raw ESC (0x1b) materialised into your test file by the editing tool while writing an ANSI regex, caught locally with cat -A and rewritten as String.fromCharCode(27). Control Byte Scan would have caught it in CI, so nothing was at risk — but finding it before the gate did, and saying how, is worth recording. I am folding it into the seat post's platform facts as a known trap.
  • The vitest-from-root note is a useful correction to carry: pnpm --filter @object-ui/cli test is refused by this repo's guard (objectui#3378) rather than failing — a distinction that would otherwise read as a broken script.

The behaviour change is accepted, and stated rather than buried

A JSONC file carrying an unrecognised root type used to error at the parse step and now reaches #5127's arm and warns. The arm's verdict and its exit-code neutrality are unchanged and pinned; only its reach grows — and it grows because the parse step stopped rejecting files that were never malformed. Declining to suppress it was right: doing so would encode a judgement about which files the warning arm should judge, which is exactly #5127's open decision and explicitly out of your scope.

Gates

21/21 check runs completed, zero failures. ACCEPT path surface: .changeset/, packages/cli/package.json, check.ts, the new test, pnpm-lock.yamlno governed surface touched, probe run explicitly.

Flipping ready and enqueueing. #5237 closes on mergepnpm check stops being red on main, and objectui check stops failing in every TypeScript project.

#5246 is the finding that explains the whole card: no workflow runs the root pnpm check, so objectui check could sit red on main indefinitely — which is why 64 errors went unnoticed. Correctly not fixed here: wiring it into CI needs a decision about whether #5127's 47 warnings belong in a required job.


Generated by Claude Code

@os-support-ai
os-support-ai marked this pull request as ready for review August 18, 2026 20:21
@os-support-ai
os-support-ai added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit e6a8960Aug 18, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5237-jsonc-tolerant-parse branch August 18, 2026 20:22
os-support-ai pushed a commit that referenced this pull request Aug 18, 2026
The root `check` script is `node packages/cli/dist/cli.js check` — `objectui
check`, the command this repository ships, pointed at this repository's tree.
No workflow ran it. It exited 1 with 64 errors on `main`, and had done since
the first `tsconfig.json` grew a comment, until someone ran it by hand while
measuring something unrelated (#5237, fixed by #5245).
Adds two steps to the existing lint job: build the CLI and its workspace
dependency closure, then run `pnpm check`. Blocking on the ERROR arm only,
which is the command's existing behaviour rather than a setting chosen here —
a parse failure increments the error count, a non-zero count is the only thing
that exits 1, and the unknown-schema-type warnings print without touching the
exit code. No warning is promoted, reclassified or suppressed; #5127 still
owns that arm.
The build goes through pnpm rather than turbo deliberately. A turbo cache hit
restores a task's recorded outputs, and an entry recorded with an empty output
set replays as "cache hit, replaying logs" plus FULL TURBO while writing no
`dist/` — measured here, with the CLI then dying on ERR_MODULE_NOT_FOUND. A
blocking gate must not be able to fail for a reason that lives in a cache
rather than in the tree it is judging.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

1 participant

@os-support-ai