@@ -19,6 +19,7 @@ import {
1919 viteConfigHasCloudflarePlugin ,
2020 hasWranglerConfig ,
2121 formatMissingCloudflarePluginError ,
22+ detectStaticExport ,
2223} from "../packages/vinext/src/deploy.js" ;
2324import {
2425 detectPackageManager ,
@@ -288,6 +289,127 @@ describe("detectProject", () => {
288289 const info = detectProject ( tmpDir ) ;
289290 expect ( info . hasISR ) . toBe ( false ) ;
290291 } ) ;
292+
293+ it ( "detects output: 'export' in next.config.mjs" , ( ) => {
294+ mkdir ( tmpDir , "app" ) ;
295+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
296+ const info = detectProject ( tmpDir ) ;
297+ expect ( info . isStaticExport ) . toBe ( true ) ;
298+ } ) ;
299+
300+ it ( 'detects output:"export" without spaces' , ( ) => {
301+ mkdir ( tmpDir , "app" ) ;
302+ writeFile ( tmpDir , "next.config.mjs" , `export default {output:"export"};` ) ;
303+ const info = detectProject ( tmpDir ) ;
304+ expect ( info . isStaticExport ) . toBe ( true ) ;
305+ } ) ;
306+
307+ it ( "isStaticExport is false when no export config" , ( ) => {
308+ mkdir ( tmpDir , "app" ) ;
309+ writeFile ( tmpDir , "next.config.mjs" , `export default {};` ) ;
310+ const info = detectProject ( tmpDir ) ;
311+ expect ( info . isStaticExport ) . toBe ( false ) ;
312+ } ) ;
313+
314+ it ( "detects output: 'export' in next.config.ts" , ( ) => {
315+ mkdir ( tmpDir , "app" ) ;
316+ writeFile (
317+ tmpDir ,
318+ "next.config.ts" ,
319+ `const config = { output: 'export' };\nexport default config;` ,
320+ ) ;
321+ const info = detectProject ( tmpDir ) ;
322+ expect ( info . isStaticExport ) . toBe ( true ) ;
323+ } ) ;
324+
325+ it ( "detects output: 'export' in next.config.cjs" , ( ) => {
326+ mkdir ( tmpDir , "app" ) ;
327+ writeFile ( tmpDir , "next.config.cjs" , `module.exports = { output: "export" };` ) ;
328+ const info = detectProject ( tmpDir ) ;
329+ expect ( info . isStaticExport ) . toBe ( true ) ;
330+ } ) ;
331+
332+ it ( "does not detect commented-out output: 'export'" , ( ) => {
333+ mkdir ( tmpDir , "app" ) ;
334+ writeFile ( tmpDir , "next.config.mjs" , `// output: "export"\nexport default {};` ) ;
335+ const info = detectProject ( tmpDir ) ;
336+ expect ( info . isStaticExport ) . toBe ( false ) ;
337+ } ) ;
338+
339+ it ( "does not detect output: 'standalone' as static export" , ( ) => {
340+ mkdir ( tmpDir , "app" ) ;
341+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "standalone" };` ) ;
342+ const info = detectProject ( tmpDir ) ;
343+ expect ( info . isStaticExport ) . toBe ( false ) ;
344+ } ) ;
345+
346+ it ( "isStaticExport is false when no next.config exists" , ( ) => {
347+ mkdir ( tmpDir , "app" ) ;
348+ const info = detectProject ( tmpDir ) ;
349+ expect ( info . isStaticExport ) . toBe ( false ) ;
350+ } ) ;
351+
352+ it ( "handles empty next.config.js gracefully" , ( ) => {
353+ mkdir ( tmpDir , "app" ) ;
354+ writeFile ( tmpDir , "next.config.js" , "" ) ;
355+ const info = detectProject ( tmpDir ) ;
356+ expect ( info . isStaticExport ) . toBe ( false ) ;
357+ } ) ;
358+
359+ it ( "prefers next.config.ts over next.config.mjs for static export detection" , ( ) => {
360+ mkdir ( tmpDir , "app" ) ;
361+ writeFile ( tmpDir , "next.config.ts" , `export default {};` ) ;
362+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
363+ const info = detectProject ( tmpDir ) ;
364+ // .ts is checked first and has no output: "export", so result is false
365+ expect ( info . isStaticExport ) . toBe ( false ) ;
366+ } ) ;
367+
368+ it ( "does not detect output: 'export' inside block comments" , ( ) => {
369+ mkdir ( tmpDir , "app" ) ;
370+ writeFile ( tmpDir , "next.config.mjs" , `/* output: "export" */\nexport default {};` ) ;
371+ const info = detectProject ( tmpDir ) ;
372+ expect ( info . isStaticExport ) . toBe ( false ) ;
373+ } ) ;
374+
375+ it ( "does not detect output: 'export' inside multiline block comments" , ( ) => {
376+ mkdir ( tmpDir , "app" ) ;
377+ writeFile (
378+ tmpDir ,
379+ "next.config.mjs" ,
380+ "/*\n * Previously: output: 'export'\n */\nexport default {};" ,
381+ ) ;
382+ const info = detectProject ( tmpDir ) ;
383+ expect ( info . isStaticExport ) . toBe ( false ) ;
384+ } ) ;
385+ } ) ;
386+
387+ // ─── detectStaticExport (direct) ────────────────────────────────────────────
388+
389+ describe ( "detectStaticExport" , ( ) => {
390+ it ( 'returns true for output: "export"' , ( ) => {
391+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
392+ expect ( detectStaticExport ( tmpDir ) ) . toBe ( true ) ;
393+ } ) ;
394+
395+ it ( "returns false when no next.config exists" , ( ) => {
396+ expect ( detectStaticExport ( tmpDir ) ) . toBe ( false ) ;
397+ } ) ;
398+
399+ it ( 'returns false for output: "standalone"' , ( ) => {
400+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "standalone" };` ) ;
401+ expect ( detectStaticExport ( tmpDir ) ) . toBe ( false ) ;
402+ } ) ;
403+
404+ it ( "strips block comments before matching" , ( ) => {
405+ writeFile ( tmpDir , "next.config.mjs" , `/* output: "export" */ export default {};` ) ;
406+ expect ( detectStaticExport ( tmpDir ) ) . toBe ( false ) ;
407+ } ) ;
408+
409+ it ( "strips single-line comments before matching" , ( ) => {
410+ writeFile ( tmpDir , "next.config.mjs" , `// output: "export"\nexport default {};` ) ;
411+ expect ( detectStaticExport ( tmpDir ) ) . toBe ( false ) ;
412+ } ) ;
291413} ) ;
292414
293415// ─── generateWranglerConfig ─────────────────────────────────────────────────
@@ -365,6 +487,67 @@ describe("generateWranglerConfig", () => {
365487 expect ( parsed . images ) . toBeDefined ( ) ;
366488 expect ( parsed . images . binding ) . toBe ( "IMAGES" ) ;
367489 } ) ;
490+
491+ it ( "generates static-only config when isStaticExport is true" , ( ) => {
492+ mkdir ( tmpDir , "app" ) ;
493+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
494+ const info = detectProject ( tmpDir ) ;
495+ const config = generateWranglerConfig ( info ) ;
496+ const parsed = JSON . parse ( config ) ;
497+
498+ // No worker entry — Cloudflare serves static files directly
499+ expect ( parsed . main ) . toBeUndefined ( ) ;
500+ // Uses 404-page handling instead of routing all requests to worker
501+ expect ( parsed . assets . not_found_handling ) . toBe ( "404-page" ) ;
502+ // No ASSETS binding needed (no worker to bind to)
503+ expect ( parsed . assets . binding ) . toBeUndefined ( ) ;
504+ // Still has directory for asset serving
505+ expect ( parsed . assets . directory ) . toBe ( "dist/client" ) ;
506+ // No image optimization binding
507+ expect ( parsed . images ) . toBeUndefined ( ) ;
508+ } ) ;
509+
510+ it ( "omits KV namespace for static exports even with ISR-like patterns" , ( ) => {
511+ mkdir ( tmpDir , "app" ) ;
512+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
513+ writeFile (
514+ tmpDir ,
515+ "app/page.tsx" ,
516+ "export const revalidate = 30;\nexport default function() { return <div/> }" ,
517+ ) ;
518+ const info = detectProject ( tmpDir ) ;
519+ const config = generateWranglerConfig ( info ) ;
520+ const parsed = JSON . parse ( config ) ;
521+
522+ expect ( parsed . kv_namespaces ) . toBeUndefined ( ) ;
523+ } ) ;
524+
525+ it ( "static export config still includes $schema, compatibility_date, and compatibility_flags" , ( ) => {
526+ mkdir ( tmpDir , "app" ) ;
527+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
528+ const info = detectProject ( tmpDir ) ;
529+ const config = generateWranglerConfig ( info ) ;
530+ const parsed = JSON . parse ( config ) ;
531+
532+ expect ( parsed . $schema ) . toBe ( "node_modules/wrangler/config-schema.json" ) ;
533+ const today = new Date ( ) . toISOString ( ) . split ( "T" ) [ 0 ] ;
534+ expect ( parsed . compatibility_date ) . toBe ( today ) ;
535+ expect ( parsed . compatibility_flags ) . toContain ( "nodejs_compat" ) ;
536+ } ) ;
537+
538+ it ( "non-static config has main, ASSETS binding, and images (regression guard)" , ( ) => {
539+ mkdir ( tmpDir , "app" ) ;
540+ writeFile ( tmpDir , "next.config.mjs" , `export default {};` ) ;
541+ const info = detectProject ( tmpDir ) ;
542+ const config = generateWranglerConfig ( info ) ;
543+ const parsed = JSON . parse ( config ) ;
544+
545+ expect ( parsed . main ) . toBe ( "./worker/index.ts" ) ;
546+ expect ( parsed . assets . binding ) . toBe ( "ASSETS" ) ;
547+ expect ( parsed . assets . not_found_handling ) . toBe ( "none" ) ;
548+ expect ( parsed . images ) . toBeDefined ( ) ;
549+ expect ( parsed . images . binding ) . toBe ( "IMAGES" ) ;
550+ } ) ;
368551} ) ;
369552
370553// ─── Worker Entry Generation ─────────────────────────────────────────────────
@@ -1096,6 +1279,36 @@ describe("getFilesToGenerate", () => {
10961279 expect ( viteFile ) . toBeDefined ( ) ;
10971280 expect ( viteFile ! . content ) . not . toContain ( "plugin-rsc" ) ;
10981281 } ) ;
1282+
1283+ it ( "skips worker/index.ts for static exports" , ( ) => {
1284+ mkdir ( tmpDir , "app" ) ;
1285+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
1286+ const info = detectProject ( tmpDir ) ;
1287+ const files = getFilesToGenerate ( info ) ;
1288+
1289+ const workerFile = files . find ( ( f ) => f . description === "worker/index.ts" ) ;
1290+ expect ( workerFile ) . toBeUndefined ( ) ;
1291+ } ) ;
1292+
1293+ it ( "still generates wrangler.jsonc for static exports" , ( ) => {
1294+ mkdir ( tmpDir , "app" ) ;
1295+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
1296+ const info = detectProject ( tmpDir ) ;
1297+ const files = getFilesToGenerate ( info ) ;
1298+
1299+ const wranglerFile = files . find ( ( f ) => f . description === "wrangler.jsonc" ) ;
1300+ expect ( wranglerFile ) . toBeDefined ( ) ;
1301+ } ) ;
1302+
1303+ it ( "still generates vite.config.ts for static exports" , ( ) => {
1304+ mkdir ( tmpDir , "app" ) ;
1305+ writeFile ( tmpDir , "next.config.mjs" , `export default { output: "export" };` ) ;
1306+ const info = detectProject ( tmpDir ) ;
1307+ const files = getFilesToGenerate ( info ) ;
1308+
1309+ const viteFile = files . find ( ( f ) => f . description === "vite.config.ts" ) ;
1310+ expect ( viteFile ) . toBeDefined ( ) ;
1311+ } ) ;
10991312} ) ;
11001313
11011314// ─── viteConfigHasCloudflarePlugin ───────────────────────────────────────────
0 commit comments