feat: lightweight worker for static exports#733
feat: lightweight worker for static exports#733raed04 wants to merge 1 commit intocloudflare:mainfrom
Conversation
commit: |
Strip the full worker from static export builds. When next.config sets output: "export", vinext deploy now generates a wrangler config that serves pre-rendered files directly via Cloudflare's built-in asset serving — no worker entry, no ASSETS/IMAGES bindings, no KV namespace. - Add `isStaticExport` to ProjectInfo with heuristic detection (reads next.config, strips single-line and block comments, matches output: "export" / 'export') - generateWranglerConfig: static branch uses not_found_handling: "404-page" - getFilesToGenerate: skip worker/index.ts for static exports - Relax Cloudflare plugin guard: use already-loaded nextConfig.output instead of re-reading disk, so inline config overrides also work - 24 new tests covering detection, config generation, and file generation Closes cloudflare#566
bcff738 to
00a2724
Compare
|
/bigbonk review |
There was a problem hiding this comment.
Review: feat: lightweight worker for static exports
Solid feature addition. The detection heuristic, wrangler config branching, and worker skip logic are well-structured and consistent with the existing detectISR pattern. Tests are thorough. A few issues to address:
Issues
-
Deploy-time Cloudflare plugin guard not skipped for static exports —
deploy()at line 1358 throws if an existingvite.configlacks@cloudflare/vite-plugin, but static export builds don't need it (and this PR already relaxes that guard inindex.ts). A user withoutput: "export"and a vite config fromvinext init(which omits cloudflare) will hit this error during deploy. -
getMissingDepsstill requires@cloudflare/vite-pluginfor static exports — The generated wrangler config has nomainand no worker, butgetMissingDeps()will still install@cloudflare/vite-plugin,@vitejs/plugin-rsc, andreact-server-dom-webpackfor static export projects. These are unnecessary for pure static builds. -
Comment stripping has a known class of false positives — The
//regex will strip content inside string literals containing//(e.g.,const url = "https://example.com"; output: "export"). This is a pre-existing limitation shared with the block comment regex pattern and is documented as a heuristic, so it's acceptable — just noting it for awareness. -
nodejs_compatflag in static-only wrangler config — WhenisStaticExportis true and there's no worker, thenodejs_compatcompatibility flag has no effect (there's no JS execution). It's harmless but misleading. Minor nit.
Positive notes
- Good test coverage (24 tests covering all the edge cases)
- Consistent with existing detection patterns (
detectISR,detectMDX) - Config priority order matches
CONFIG_FILESinnext-config.ts - The
index.tsguard change correctly uses the already-loadednextConfiginstead of re-detecting
| .replace(/\/\/.*$/gm, "") // single-line comments | ||
| .replace(/\/\*[\s\S]*?\*\//g, ""); // block comments | ||
| // Match output: "export" or output: 'export' (enforcing matching quotes). | ||
| return /output\s*:\s*(?:"export"|'export')/.test(stripped); |
There was a problem hiding this comment.
Nit: the comment says "enforcing matching quotes" but the regex uses two separate alternations ("export" and 'export'), each with their own matched delimiters. The phrasing is slightly misleading — it implies there's a backreference enforcing that the opening and closing quotes match, but in practice both alternatives are self-consistent so it works correctly. Consider:
| return /output\s*:\s*(?:"export"|'export')/.test(stripped); | |
| // Match output: "export" or output: 'export' (double or single quotes). |
| config.assets = { | ||
| directory: "dist/client", | ||
| not_found_handling: "404-page", | ||
| }; |
There was a problem hiding this comment.
When isStaticExport is true and there's no worker entry, nodejs_compat has no effect since there's no JavaScript execution at all — Cloudflare just serves static files. Consider omitting it for static exports to keep the generated config minimal and avoid confusing users who read it:
| }; | |
| config.assets = { | |
| directory: "dist/client", | |
| not_found_handling: "404-page", | |
| }; | |
| } else { |
You could conditionally set compatibility_flags only in the non-static branch. Low priority — it's functionally harmless.
| const content = fs.readFileSync(configPath, "utf-8"); | ||
| // Strip comments to avoid matching commented-out config. | ||
| const stripped = content | ||
| .replace(/\/\/.*$/gm, "") // single-line comments |
There was a problem hiding this comment.
The // comment stripping regex will also match // inside string literals, e.g.:
const baseUrl = "https://example.com"; // this gets partially eatenbecomes const baseUrl = "https: after stripping. If the remaining content accidentally matches the output pattern, you'd get a false positive.
This is a pre-existing limitation (same class of issue as detectISR), and for next.config files it's extremely unlikely to matter in practice. Just documenting for awareness — no action needed unless you want to add a string-preserving pass.
| } | ||
|
|
||
| if (!info.hasWorkerEntry) { | ||
| if (!info.hasWorkerEntry && !info.isStaticExport) { |
There was a problem hiding this comment.
Good — correctly skips worker generation for static exports. But two related guards in the deploy() function are not gated on isStaticExport:
-
Line 1358:
viteConfigHasCloudflarePlugin(root)check will throw for static export projects that have a vite config without@cloudflare/vite-plugin. Should be:if (info.hasViteConfig && !info.isStaticExport && !viteConfigHasCloudflarePlugin(root)) {
-
getMissingDeps(): Still installs@cloudflare/vite-plugin,wrangler,@vitejs/plugin-rsc,react-server-dom-webpackeven for static exports where no worker runs. Consider gating the cloudflare/rsc deps behind!info.isStaticExport. (wrangler is still needed forwrangler deploy.)
| hasWranglerConfig(root) && | ||
| !options.disableAppRouter | ||
| !options.disableAppRouter && | ||
| nextConfig.output !== "export" |
There was a problem hiding this comment.
This correctly uses the already-loaded nextConfig from the Vite plugin's config phase rather than re-detecting via file heuristic. Clean approach.
| writeFile(tmpDir, "next.config.mjs", `export default {};`); | ||
| const info = detectProject(tmpDir); | ||
| expect(info.isStaticExport).toBe(false); | ||
| }); |
There was a problem hiding this comment.
Nice edge case — testing the output:"export" form without spaces ensures the \s* in the regex is doing its job.
|
|
||
| it("does not detect output: 'export' inside multiline block comments", () => { | ||
| mkdir(tmpDir, "app"); | ||
| writeFile( |
There was a problem hiding this comment.
Good: this test verifies the config priority order (.ts > .mjs), which matches CONFIG_FILES in next-config.ts. Worth noting in the test name or comment that this is testing first-match-wins semantics, not alphabetical priority.
|
Review posted successfully. Here's a summary of the feedback: Key issues found:
The review also highlighted positive aspects: good test coverage, consistency with existing detection patterns, and the clean approach of using the already-loaded |
Summary
Closes #566 — strips the full worker from static export builds.
next.configsetsoutput: "export",vinext deploynow generates a wrangler config that serves pre-rendered files directly via Cloudflare's built-in asset serving — no worker entry, no ASSETS/IMAGES bindings, no KV namespaceisStaticExportdetection toProjectInfo(heuristic: reads next.config, strips//and/* */comments, matchesoutput: "export")@cloudflare/vite-pluginChanges
packages/vinext/src/deploy.tsdetectStaticExport(),isStaticExportonProjectInfo, static branch ingenerateWranglerConfig, skip worker ingetFilesToGeneratepackages/vinext/src/index.tsnextConfig.output !== "export"(already-loaded config)tests/deploy.test.ts.cjs/.ts/.mjsdetection, block comments, standalone negative, empty/missing config, config priority, directdetectStaticExport, static/non-static wrangler schema, regression guardsTest plan