Skip to content

fix(config): parse hosted config as TypeScript regardless of file name (production 400) - #3401

Closed
kwakayama wants to merge 4 commits into
mainfrom
fix/hosted-config-typescript-parse
Closed

kwakayama wants to merge 4 commits into
mainfrom
fix/hosted-config-typescript-parse

Conversation

@kwakayama

@kwakayama kwakayama commented Aug 6, 2026 •

Copy link
Copy Markdown
Contributor

Restores a customer site that is returning HTTP 400 in production right now.

Symptom

{"type":"https://veryfront.com/docs/errors/config-parse-error",
 "title":"Failed to parse configuration","status":400,
 "detail":"Hosted configuration rejected (syntax-error: syntax-error)"}

The config is valid TypeScript:

export default {
  router: "pages" as const,   // TypeScript-only syntax
  app: "components/app.tsx",
  ...
};

Cause

pickPlugins selected Babel's typescript plugin from the file name:

const isTypeScript = /\.(?:tsx?|[cm]ts)$/.test(normalizedPath);
if (isTypeScript || !filePath) plugins.push("typescript");

Under a .js or .mjs name a TypeScript config is parsed as JavaScript. as const fails, parserErrorReason matches neither DuplicateDefaultExport nor VarRedeclaration and falls through to the generic "syntax-error", which surfaces as CONFIG_PARSE_ERROR → HTTP 400.

The evaluator cannot trust that name. It is the candidate the hosted loader asked for, not necessarily the file it received.

Reproduced locally, then fixed

Both reproductions are committed as tests, and both fail on main.

At the evaluator boundary — declarative-evaluator.test.ts, the customer's config byte for byte through evaluateDeclarativeConfig:

before   DeclarativeConfigEvaluationError: Hosted configuration rejected (syntax-error: syntax-error)
after    all three file names evaluate; router === "pages"

That error string is verbatim what production returns.

At the parser — parser-only.test.ts:

before   veryfront.config.ts    parses
         veryfront.config.js    SyntaxError: Unexpected token, expected "," (2:18)
         veryfront.config.mjs   SyntaxError
after    all three parse

Column 18 is exactly where as const begins.

Does "always TypeScript" break JavaScript?

Measured rather than assumed, since typescript + jsx puts .js into TSX mode where < can read as type arguments instead of comparison. Same constructs under veryfront.config.js, before and after:

case before after
comparison chain a < b > (c) ok ok
generic-looking call f < g > (h) ok ok
regex literal /a<b>/g ok ok
JSX in .js ok ok
template with angles, arrow, spread, optional chain ok ok
class fields (#p, static), dynamic import() ok ok

No difference on any of them, including the ambiguous ones. TypeScript is a superset, so enabling it cannot reject valid JavaScript — it only widens what parses. The pre-existing !filePath branch already depended on that; this makes the behaviour uniform rather than introducing a new one.

Blast radius is small: parser-only.ts has two consumers, both the declarative config evaluator.

What this does not fix

Why a .ts config is labelled .js. This stops the parser guessing language from an untrusted name; it does not explain the mislabel. The runtime /files/ endpoint is not extension-tolerant — veryfront.config.js genuinely 404s for this release while .ts returns the content — so it is not simply the loader's candidate order. VERYFRONT_RELEASE_ASSET_MANIFEST is enabled in production and that seam carries its own fileName; that is where I would look next.

This matters beyond parsing: DeclarativeConfigFileName also feeds configFileProvenance, parse-error locations, and the hosted config cache key. If the release-asset path records .js for a .ts file, that is a data-integrity bug which will resurface somewhere less obvious — and this PR makes it silent rather than loud. Worth tracking separately rather than letting the disappearing 400 close the question.

tomcode.com, also down, with a different defect: 404 API request failed. Its release has no veryfront.config.* at all, which should hit the loader's own "No config file found, using defaults" fallback. It does not, because the API-backed filesystem reports a missing file as API_CLIENT_ERROR with status: 404, while isNotFoundError matches only ENOENT/ENOTDIR, Deno's NotFound, or slug file-not-found. The 404 propagates instead of being read as absence. The fix belongs in the fs adapter — translate the transport 404 into the platform not-found error — not in the classifier, which should not sniff HTTP status or every API failure starts looking like an absent file.

Both defects were latent behind the missing worker entry fixed in #3399: that bug crashed the evaluator before it parsed anything, so this path had never run against a real project config in production.

Tests

  • src/config/ — 44 passed, 402 steps.
  • extensions/ext-parser-babel/ — 3 passed, 36 steps.

Summary by CodeRabbit

  • Bug Fixes

    • Improved configuration parsing so TypeScript syntax works in .js, .mjs, and .ts configuration files.
    • Preserved support for standard JavaScript configuration files across supported extensions.
    • Fixed evaluation of TypeScript configurations using as const.
    • Improved handling of mixed JavaScript and TypeScript configuration content.
  • Chores

    • Updated the application version to 0.1.1204.

Babel's typescript plugin was gated on the config file name. A TypeScript
config evaluated under a .js or .mjs name is therefore parsed as JavaScript,
and any TS-only syntax fails.

Production 2026-08-06: a customer's veryfront.config.ts containing
`router: "pages" as const` returned HTTP 400 'Hosted configuration
rejected (syntax-error: syntax-error)' and took their site down.

Reproduced against the real config through evaluateHostedConfigSource, the
same seam a hosted request uses:

  before   veryfront.config.ts   parses
           veryfront.config.js   config-parse-error 400 syntax-error
           veryfront.config.mjs  config-parse-error 400 syntax-error

  after    all three             parse

The 400 is byte-identical to production, and only a non-.ts name produces
it. Which seam supplies the non-.ts name is not yet pinned; the release
asset path is enabled in production and carries its own fileName. That
question is worth answering separately, but it does not change this fix:
the parser should not infer language from a name it cannot trust.

Latent behind the missing worker entry fixed in #3399 — that bug crashed
the evaluator before it could parse anything, so the hosted declarative
path had never run against a real project config in production.

TypeScript is a superset, so enabling it unconditionally cannot reject
valid JavaScript. The existing `!filePath` branch already relied on that;
this makes the behaviour uniform rather than adding a new one.
@kwakayama
kwakayama requested a review from kojiwakayama as a code owner August 6, 2026 00:59
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@coderabbitai

coderabbitai Bot commented Aug 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c9e2bf83-f4e0-4e74-ba70-fc55bbf32148

📥 Commits

Reviewing files that changed from the base of the PR and between 7a60d93 and d453a05.

📒 Files selected for processing (1)
  • src/config/declarative-evaluator.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/config/declarative-evaluator.test.ts

📝 Walkthrough

Walkthrough

The Babel parser now accepts TypeScript syntax for all supported configuration filenames. Regression tests cover TypeScript and JavaScript configurations. The project version changed from 0.1.1203 to 0.1.1204.

Changes

Babel parser behavior

Layer / File(s) Summary
Unconditional TypeScript parsing and regression coverage
extensions/ext-parser-babel/src/parser-only.ts, extensions/ext-parser-babel/src/parser-only.test.ts, src/config/declarative-evaluator.test.ts
pickPlugins always enables Babel’s TypeScript plugin. Tests cover TypeScript and JavaScript configurations for .js, .mjs, and .ts filenames. Declarative evaluation tests cover TypeScript configurations with as const.

Project version update

Layer / File(s) Summary
Version synchronization
deno.json, src/utils/version-constant.ts
The project metadata and exported VERSION constant now use 0.1.1204.

Estimated code review effort: 2 (Simple) | ~15 minutes

Possibly related PRs

Suggested reviewers: kojiwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: parsing hosted configuration as TypeScript regardless of its filename.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/hosted-config-typescript-parse

Comment @coderabbitai help to get the list of available commands.

@kwakayama

Copy link
Copy Markdown
Contributor Author

Is "always TypeScript" actually safe? Measured, not assumed.

Fair question on review, and "TypeScript is a superset" is a claim worth testing rather than asserting — the real risk is that typescript + jsx together puts .js into TSX mode, where < after an identifier can be read as type arguments instead of a comparison.

Same constructs, parsed under veryfront.config.js on main and on this branch:

case before after
comparison chain a < b > (c) ok ok
generic-looking call f < g > (h) ok ok
regex literal /a<b>/g ok ok
JSX in .js ok ok
template with angles ok ok
arrow, object spread, optional chain ok ok
class fields (#p, static) ok ok
dynamic import() ok ok

No difference on any of them, including the ambiguous ones. The .ts + JSX case fails in both columns, which is correct and unchanged: .ts does not get the jsx plugin, matching tsc.

What this does not fix

This makes the parser stop guessing language from a name it cannot trust. It does not explain why a .ts config is being handed to the evaluator under a .js name, and that is worth keeping visible:

DeclarativeConfigFileName is not only a parser hint. It feeds configFileProvenance, the location attached to parse errors, and the hosted config cache key. If the release-asset seam records .js for a file that is actually .ts, that is a data-integrity problem which will resurface somewhere less obvious — and this PR makes it silent rather than loud.

So I would land this as hardening, on the grounds that a parser should not infer language from an untrusted name, and track the mislabel separately rather than let the disappearing 400 close the question.

Verified: extensions/ext-parser-babel/ 3 passed / 36 steps; src/config/ 44 passed / 401 steps.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@extensions/ext-parser-babel/src/parser-only.test.ts`:
- Around line 140-160: Update the assertion import in parser-only.test.ts to use
the repository helper from `#veryfront/testing/assert.ts` instead of `@std/assert`,
while preserving the existing assert calls in the TypeScript parsing tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 91db55b0-48b9-4562-8bf4-54fc865a6b3f

📥 Commits

Reviewing files that changed from the base of the PR and between 5dfcdfe and c066f3b.

📒 Files selected for processing (4)
  • deno.json
  • extensions/ext-parser-babel/src/parser-only.test.ts
  • extensions/ext-parser-babel/src/parser-only.ts
  • src/utils/version-constant.ts

Comment on lines +140 to +160
it("parses TypeScript syntax under a .js file name", async () => {
// The hosted config loader cannot trust the extension. Its file endpoint is
// extension-tolerant, so a request for veryfront.config.js is answered with
// the project's veryfront.config.ts content, and the loader reports the
// candidate it asked for. Gating the TypeScript plugin on that name parsed
// TypeScript as JavaScript and rejected every TS config as a syntax error.
//
// Production 2026-08-06: `router: "pages" as const` in a customer's
// veryfront.config.ts returned HTTP 400 "Hosted configuration rejected
// (syntax-error: syntax-error)" and took their site down.
const source = [
"export default {",
' router: "pages" as const,',
' app: "components/app.tsx",',
"};",
].join("\n");

for (const filePath of ["veryfront.config.js", "veryfront.config.mjs", "veryfront.config.ts"]) {
const ast = await parser.parse({ code: source, filePath });
assert(ast, `expected ${filePath} to parse TypeScript syntax`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Use the repository assertion helper.

The new tests call assert, but parser-only.test.ts imports it from @std/assert on Line 1. The repository guideline requires assertions from #veryfront/testing/assert.ts. Update the assertion import before merge.

Also applies to: 163-175

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@extensions/ext-parser-babel/src/parser-only.test.ts` around lines 140 - 160,
Update the assertion import in parser-only.test.ts to use the repository helper
from `#veryfront/testing/assert.ts` instead of `@std/assert`, while preserving the
existing assert calls in the TypeScript parsing tests.

Source: Coding guidelines

The parser-level test proves the plugin selection. This one reproduces the
actual production failure signature end to end: the customer's config, byte
for byte, evaluated through evaluateDeclarativeConfig under each recognized
file name.

Against the shipped parser it fails with the production string verbatim:

  DeclarativeConfigEvaluationError: Hosted configuration rejected
  (syntax-error: syntax-error)

With the fix all three names evaluate and router resolves to "pages".
@kwakayama kwakayama changed the title fix(config): parse hosted config as TypeScript regardless of file name fix(config): parse hosted config as TypeScript regardless of file name (production 400) Aug 6, 2026
@kwakayama kwakayama closed this Aug 6, 2026
@kwakayama
kwakayama deleted the fix/hosted-config-typescript-parse branch August 6, 2026 01:20
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