Skip to content

[APPS] fix(apps): build backend functions under Vite 8 / Rolldown - #468

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 5 commits into
masterfrom
sdkennedy2/apps-parse-backend-module-source
Jul 29, 2026
Merged

[APPS] fix(apps): build backend functions under Vite 8 / Rolldown#468
gh-worker-dd-mergequeue-cf854d[bot] merged 5 commits into
masterfrom
sdkennedy2/apps-parse-backend-module-source

Conversation

@sdkennedy2

@sdkennedy2 sdkennedy2 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

Motivation

Vite 8 switches its bundler to Rolldown, which stubs ModuleInfo#ast to throw (its AST lives in Rust memory — a deliberate, permanent API boundary):

Error: UNSUPPORTED: ModuleInfo#ast     plugin: 'datadog-apps-plugin'  hook: 'closeBundle'

The Apps backend collector read that property to derive each backend function's allowedConnectionIds. So any app with a .backend.ts fails to build on Vite 8 — and fails confusingly: the client bundle prints ✓ built, then the plugin's nested backend build dies in closeBundle with nothing pointing at Vite, Rolldown, or your file.

@datadog/vite-plugin@3.2.8 declares vite: ">= 5.x <= 8.x", so npm installs it on Vite 8 while this documented feature is broken. Trigger is Rolldown, not the Vite major — rolldown-vite on Vite 7 hits it too.

Changes

Parse moduleInfo.code with the plugin context's own parse instead of reading the bundler's pre-built AST. Rolldown withholds only the AST; the source and resolved dependency IDs are still there.

-                    moduleInfo.ast as BaseNode,
+                    parsed,

One file, ~34 changed lines. Three things worth knowing while reading it:

  1. No new dependencies. moduleParsed runs after transform, so types and JSX are already compiled away — no TypeScript-capable parser needed. No lockfile or published-package changes.
  2. The parse is wrapped to fail closed. this.parse is the bundler's parser but not its parser configuration — Rollup binds { jsx } to its internal parse but not to PluginContext.parse, so it can throw on source the bundler accepted. Latent today, but a module we can't parse could hide a connection ID, so it raises the existing unsupportedModuleGraphDependency rather than an opaque Expression expected.
  3. shouldTraverseCollectedModule moved ahead of the parse. Reading the old AST was free; parsing isn't. Without this we'd parse every node_modules module just to discard it.

The typeof moduleInfo.code !== 'string' guard is required to compile (code is string | null), not defensive padding.

Two alternatives implemented and rejected
  • @typescript-eslint/typescript-estree — eagerly requires typescript (an optional peer), so it needed a deferred require to avoid dragging typescript into every consumer of every published plugin. ~50 lines of loader machinery plus a dependency on all five published packages, for capability this path never exercises.
  • Vite's parseAst export — Vite maps its require condition to a reduced CJS entry that omits parseAst, which would break consumers of this plugin's CJS build.

QA Instructions

Verified with a real scaffolded app whose backend function calls @datadog/action-catalog, with the connection ID in a separate module so resolution must walk the module graph. Every cell measured — pre-fix is published 3.2.8, post-fix is this PR:

Path V7 pre V7 post V8 pre V8 post
vite build + manifest allowlist
Dev server → function executes
Upload → published live

Both ❌ paths fail with UNSUPPORTED: ModuleInfo#ast; pre-fix on Vite 8 the upload never reaches the network because closeBundle throws first. Vite 7 unchanged throughout. Full logs, versions and method in the verification comment.

Blast Radius

Only the Apps backend-function build path. The Apps plugin already refuses non-Vite bundlers, and builds without a .backend.ts never reach this code. No dependency changes.

Risk is moderate rather than low for one reason: this collector produces a security-relevant allowlist, and silently collecting fewer connection IDs would break backend functions at runtime while the build still succeeded. Hence the fail-closed parse, the untouched specifier pairing, and QA that asserts exact collected IDs rather than exit codes.

Documentation


Not here, deliberately: a separate specifier-mispairing bug affecting Vite 6/7 today (#469, stacked); narrowing the Vite peer range (unnecessary now that Vite 8 works); adding Rolldown to the automated test matrix (repo pins vite@6.3.5 — the verification above was manual); the create-apps bump (different repo).

🤖 Generated with Claude Code

The Apps backend module-graph collector read `moduleInfo.ast` in
`moduleParsed`. Rolldown — the bundler Vite 8 uses by default — stubs that
getter to throw `UNSUPPORTED: ModuleInfo#ast`, so any app with a
`.backend.ts` built under Vite 8 (or `rolldown-vite` on Vite 7) failed in
`closeBundle`, after the client bundle had already succeeded.

Parse `moduleInfo.code` with the plugin context's own `parse` instead.
Rolldown withholds only the pre-built AST; the source and the resolved
dependency IDs are both still available.

Using the context's parser rather than bundling one keeps this correct by
construction: the bundler already parsed this exact source to compute
`importedIds`, so whatever it accepted parses here too. It also means no
TypeScript-capable parser is needed, since `moduleParsed` runs after
`transform` — verified against a real Vite 8.1.5 + Rolldown 1.1.5 build,
where `'x' as string` arrives as `"x"` and `<div/>` arrives as a
`_jsx(...)` call. No new dependency, and `this.parse` returns a node that
is directly assignable, so the previous `as BaseNode` cast goes away.

Filtering with `shouldTraverseCollectedModule` moves ahead of the parse.
`createParsedModuleRecord` applies the same predicate, but only once an AST
exists — checking first avoids parsing every `node_modules` module just to
discard the result, which reading the pre-built AST never cost.

Co-Authored-By: Claude <noreply@anthropic.com>
@sdkennedy2

Copy link
Copy Markdown
Collaborator Author

@codex review
@cursor review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: ffc0a88600

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sdkennedy2

Copy link
Copy Markdown
Collaborator Author

@codex review

Note: the previous review covered ffc0a886. Commit ba9de237 has since addressed internal review findings — a non-mutating test helper, removal of an as cast in favour of a typeof narrowing guard, and a comment split. Please review the head of the branch.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: ba9de237ea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…tion' claim

Adversarial review disproved the claim that the plugin context's parse is
identical to the bundler's own. Rollup's internal module parse passes
{ jsx: this.options.jsx !== false } (rollup.js:19213) while PluginContext.parse
is bound to bare parseAst with no options (rollup.js:928), so this.parse can
throw on source the bundler accepted.

Latent today — the nested backend build never enables jsx and moduleParsed runs
post-transform — but an uncaught throw there would kill the backend build with
an opaque 'Expression expected'. Wrap it and reuse the existing
unsupportedModuleGraphDependency helper so a parser mismatch fails closed with
an actionable message, consistent with the rest of the module.
…esolutions fallback

- Inject parse as a jest.fn so a test asserts the node_modules/virtual/out-of-root
  modules never reach the parser. Deleting the guard now fails a test instead of
  silently costing a parse per module.
- Include the parser's own message in the fail-closed error; an actionable error
  that hides the syntax error is only half useful.
- Branch importedIdResolutions on .length rather than nullishness. Rolldown omits
  the property entirely so ?? works today, but a bundler reporting it as [] with a
  populated importedIds would silently yield zero static dependencies.
@sdkennedy2

Copy link
Copy Markdown
Collaborator Author

@codex review

Head is now f65d1ff4; the last review covered ba9de237. Two commits since, both worth a look:

  • c207499d wraps this.parse to fail closed via unsupportedModuleGraphDependency, after review disproved the claim that the plugin context's parser matches the bundler's own (Rollup binds { jsx } to its internal parse but not to PluginContext.parse).
  • f65d1ff4 makes the filter-before-parse guard observable via a jest.fn parser, keeps the parser's own diagnostic in the thrown error, and branches importedIdResolutions on .length rather than nullishness (Rolldown omits the property entirely).

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: f65d1ff40c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/plugins/apps/src/vite/backend-module-graph-collector.ts Outdated
Review: 'Why did this need to change?' It didn't. Reverting to the original
nullish-coalescing form.

The change guarded a bundler reporting importedIdResolutions as [] alongside a
populated importedIds. Neither supported bundler does that: Rollup exposes a real
getter (rollup.js:18382) that is only empty when importedIds is empty too, and
Rolldown omits the property entirely, so both forms behave identically across the
whole peer range. It was speculative hardening on an untouched context line in a
PR scoped to the Rolldown fix.
@sdkennedy2 sdkennedy2 changed the title [APPS] Parse backend module source instead of reading ModuleInfo#ast [APPS] fix(apps): build backend functions under Vite 8 / Rolldown Jul 28, 2026
@sdkennedy2

Copy link
Copy Markdown
Collaborator Author

Corrected coverage table — every cell now measured before and after the fix

The earlier table only exercised the dev-server and upload paths with the fix, so nothing in it actually demonstrated the fix mattered for those paths. Filled in the missing controls. All runs against production datadoghq.com, app on Vite 7.3.6 / 8.1.5, same fixture, same backend function.

"pre-fix" = published @datadog/vite-plugin@3.2.8 from npm. "post-fix" = this PR, packed and installed as a real tarball so it resolves the app's Vite rather than its own.

Path Vite 7 pre-fix Vite 7 post-fix Vite 8 pre-fix Vite 8 post-fix
vite build + manifest allowlist UNSUPPORTED: ModuleInfo#ast
Dev server → function executes UNSUPPORTED: ModuleInfo#ast
Upload → published to live 4494a99dbebde29f 0bfcea55c5f1556f ❌ fails in build, upload never reached 3f973f03f38b5efe

Three things this establishes that the previous table did not:

  1. The dev-server path is broken pre-fix under Vite 8 too, not just vite build. That matters because the dev server runs the same collector through a separate nested build (dev-server.tscreateBackendConnectionIdCollector), so it was a genuinely independent path. A developer on Vite 8 could not run their app locally either.
  2. Pre-fix on Vite 8, the upload never happens at allcloseBundle throws during the backend build, so nothing is archived or transmitted. The failure is total, not partial.
  3. Vite 7 is byte-for-byte unaffected, verified on all three paths rather than assumed. Under Rollup, PluginContext.parse is the same parseAst the old moduleInfo.ast getter used, so this is expected — but it is now measured.

Each of the four ✅ upload cells is a distinct published version in the app's history, so the runs are independently auditable.

@sdkennedy2

sdkennedy2 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: ab3b2936f7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@sdkennedy2
sdkennedy2 marked this pull request as ready for review July 28, 2026 21:19
@sdkennedy2
sdkennedy2 requested review from a team as code owners July 28, 2026 21:19
@sdkennedy2
sdkennedy2 requested review from otorrillas and removed request for a team July 28, 2026 21:19
@sdkennedy2

Copy link
Copy Markdown
Collaborator Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Jul 29, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-29 13:39:12 UTC ℹ️ Start processing command /merge


2026-07-29 13:39:17 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1m (p90).


2026-07-29 13:40:36 UTC ℹ️ MergeQueue: This merge request was merged

@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot merged commit a6302ed into master Jul 29, 2026
5 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854d Bot deleted the sdkennedy2/apps-parse-backend-module-source branch July 29, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants