vinext 0.0.41 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/dist/build/client-build-config.d.ts +119 -0
- package/dist/build/client-build-config.js +149 -0
- package/dist/build/client-build-config.js.map +1 -0
- package/dist/build/layout-classification-types.d.ts +62 -0
- package/dist/build/layout-classification-types.js +1 -0
- package/dist/build/layout-classification.d.ts +60 -0
- package/dist/build/layout-classification.js +98 -0
- package/dist/build/layout-classification.js.map +1 -0
- package/dist/build/report.d.ts +15 -1
- package/dist/build/report.js +50 -1
- package/dist/build/report.js.map +1 -1
- package/dist/build/route-classification-manifest.d.ts +53 -0
- package/dist/build/route-classification-manifest.js +145 -0
- package/dist/build/route-classification-manifest.js.map +1 -0
- package/dist/build/run-prerender.js +1 -1
- package/dist/build/ssr-manifest.d.ts +19 -0
- package/dist/build/ssr-manifest.js +71 -0
- package/dist/build/ssr-manifest.js.map +1 -0
- package/dist/check.js +2 -2
- package/dist/check.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/client/entry.js +1 -1
- package/dist/config/config-matchers.js +1 -0
- package/dist/config/config-matchers.js.map +1 -1
- package/dist/entries/app-rsc-entry.js +287 -95
- package/dist/entries/app-rsc-entry.js.map +1 -1
- package/dist/index.d.ts +1 -169
- package/dist/index.js +112 -432
- package/dist/index.js.map +1 -1
- package/dist/plugins/fonts.d.ts +49 -1
- package/dist/plugins/fonts.js +96 -3
- package/dist/plugins/fonts.js.map +1 -1
- package/dist/plugins/postcss.d.ts +27 -0
- package/dist/plugins/postcss.js +94 -0
- package/dist/plugins/postcss.js.map +1 -0
- package/dist/plugins/strip-server-exports.d.ts +14 -0
- package/dist/plugins/strip-server-exports.js +73 -0
- package/dist/plugins/strip-server-exports.js.map +1 -0
- package/dist/routing/app-router.d.ts +6 -4
- package/dist/routing/app-router.js +21 -22
- package/dist/routing/app-router.js.map +1 -1
- package/dist/server/app-browser-entry.js +235 -97
- package/dist/server/app-browser-entry.js.map +1 -1
- package/dist/server/app-browser-error.d.ts +8 -0
- package/dist/server/app-browser-error.js +9 -0
- package/dist/server/app-browser-error.js.map +1 -0
- package/dist/server/app-browser-state.d.ts +93 -0
- package/dist/server/app-browser-state.js +132 -0
- package/dist/server/app-browser-state.js.map +1 -0
- package/dist/server/app-elements.d.ts +92 -0
- package/dist/server/app-elements.js +122 -0
- package/dist/server/app-elements.js.map +1 -0
- package/dist/server/app-page-boundary-render.d.ts +2 -1
- package/dist/server/app-page-boundary-render.js +40 -1
- package/dist/server/app-page-boundary-render.js.map +1 -1
- package/dist/server/app-page-cache.d.ts +6 -3
- package/dist/server/app-page-cache.js +14 -8
- package/dist/server/app-page-cache.js.map +1 -1
- package/dist/server/app-page-execution.d.ts +36 -3
- package/dist/server/app-page-execution.js +50 -10
- package/dist/server/app-page-execution.js.map +1 -1
- package/dist/server/app-page-probe.d.ts +10 -4
- package/dist/server/app-page-probe.js +24 -15
- package/dist/server/app-page-probe.js.map +1 -1
- package/dist/server/app-page-render.d.ts +7 -4
- package/dist/server/app-page-render.js +13 -4
- package/dist/server/app-page-render.js.map +1 -1
- package/dist/server/app-page-request.d.ts +52 -4
- package/dist/server/app-page-request.js +86 -16
- package/dist/server/app-page-request.js.map +1 -1
- package/dist/server/app-page-response.d.ts +1 -0
- package/dist/server/app-page-response.js +1 -0
- package/dist/server/app-page-response.js.map +1 -1
- package/dist/server/app-page-route-wiring.d.ts +22 -8
- package/dist/server/app-page-route-wiring.js +219 -83
- package/dist/server/app-page-route-wiring.js.map +1 -1
- package/dist/server/app-render-dependency.d.ts +13 -0
- package/dist/server/app-render-dependency.js +35 -0
- package/dist/server/app-render-dependency.js.map +1 -0
- package/dist/server/app-route-handler-execution.d.ts +1 -0
- package/dist/server/app-route-handler-execution.js +1 -0
- package/dist/server/app-route-handler-execution.js.map +1 -1
- package/dist/server/app-route-handler-runtime.d.ts +1 -0
- package/dist/server/app-route-handler-runtime.js +26 -1
- package/dist/server/app-route-handler-runtime.js.map +1 -1
- package/dist/server/app-ssr-entry.js +6 -2
- package/dist/server/app-ssr-entry.js.map +1 -1
- package/dist/server/dev-server.js +2 -4
- package/dist/server/dev-server.js.map +1 -1
- package/dist/server/middleware.js +1 -5
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/prod-server.d.ts +3 -3
- package/dist/server/prod-server.js +1 -1
- package/dist/server/prod-server.js.map +1 -1
- package/dist/server/request-pipeline.d.ts +2 -1
- package/dist/server/request-pipeline.js +34 -5
- package/dist/server/request-pipeline.js.map +1 -1
- package/dist/shims/cache-runtime.d.ts +1 -0
- package/dist/shims/cache-runtime.js +0 -5
- package/dist/shims/cache-runtime.js.map +1 -1
- package/dist/shims/cache.d.ts +1 -0
- package/dist/shims/cache.js +1 -8
- package/dist/shims/cache.js.map +1 -1
- package/dist/shims/client-hook-error.d.ts +14 -0
- package/dist/shims/client-hook-error.js +19 -0
- package/dist/shims/client-hook-error.js.map +1 -0
- package/dist/shims/constants.d.ts +3 -3
- package/dist/shims/constants.js +3 -3
- package/dist/shims/constants.js.map +1 -1
- package/dist/shims/document.d.ts +6 -6
- package/dist/shims/error-boundary.d.ts +4 -4
- package/dist/shims/error-boundary.js +1 -1
- package/dist/shims/error-boundary.js.map +1 -1
- package/dist/shims/form.d.ts +3 -3
- package/dist/shims/head-state.d.ts +1 -0
- package/dist/shims/head-state.js +0 -5
- package/dist/shims/head-state.js.map +1 -1
- package/dist/shims/headers.d.ts +11 -0
- package/dist/shims/headers.js +13 -10
- package/dist/shims/headers.js.map +1 -1
- package/dist/shims/i18n-state.d.ts +1 -0
- package/dist/shims/i18n-state.js +0 -4
- package/dist/shims/i18n-state.js.map +1 -1
- package/dist/shims/internal/app-router-context.d.ts +6 -6
- package/dist/shims/internal/router-context.d.ts +2 -2
- package/dist/shims/layout-segment-context.d.ts +2 -2
- package/dist/shims/link.js +19 -11
- package/dist/shims/link.js.map +1 -1
- package/dist/shims/metadata.d.ts +3 -3
- package/dist/shims/navigation-state.d.ts +2 -0
- package/dist/shims/navigation-state.js +0 -13
- package/dist/shims/navigation-state.js.map +1 -1
- package/dist/shims/navigation.d.ts +55 -8
- package/dist/shims/navigation.js +97 -23
- package/dist/shims/navigation.js.map +1 -1
- package/dist/shims/navigation.react-server.d.ts +14 -0
- package/dist/shims/navigation.react-server.js +29 -0
- package/dist/shims/navigation.react-server.js.map +1 -0
- package/dist/shims/request-context.d.ts +1 -0
- package/dist/shims/request-context.js +0 -9
- package/dist/shims/request-context.js.map +1 -1
- package/dist/shims/request-state-types.d.ts +1 -1
- package/dist/shims/router-state.d.ts +1 -0
- package/dist/shims/router-state.js +0 -5
- package/dist/shims/router-state.js.map +1 -1
- package/dist/shims/slot.d.ts +11 -7
- package/dist/shims/slot.js +28 -19
- package/dist/shims/slot.js.map +1 -1
- package/dist/shims/unified-request-context.d.ts +2 -0
- package/dist/shims/unified-request-context.js +0 -14
- package/dist/shims/unified-request-context.js.map +1 -1
- package/dist/utils/mdx-scan.d.ts +10 -0
- package/dist/utils/mdx-scan.js +36 -0
- package/dist/utils/mdx-scan.js.map +1 -0
- package/dist/utils/public-routes.d.ts +5 -0
- package/dist/utils/public-routes.js +50 -0
- package/dist/utils/public-routes.js.map +1 -0
- package/package.json +3 -3
- package/dist/plugins/fix-use-server-closure-collision.d.ts +0 -29
- package/dist/plugins/fix-use-server-closure-collision.js +0 -204
- package/dist/plugins/fix-use-server-closure-collision.js.map +0 -1
package/README.md
CHANGED
|
@@ -558,7 +558,6 @@ These are intentional exclusions:
|
|
|
558
558
|
|
|
559
559
|
- **Image optimization doesn't happen at build time.** Remote images work via `@unpic/react` (auto-detects 28 CDN providers). Local images are routed through a `/_vinext/image` endpoint that can resize and transcode on Cloudflare Workers (via the Images binding) in production, but no build-time optimization or static resizing occurs.
|
|
560
560
|
- **Google Fonts are loaded from the CDN, not self-hosted.** No `size-adjust` fallback font metrics. Local fonts work but `@font-face` CSS is injected at runtime, not extracted at build time.
|
|
561
|
-
- **`useSelectedLayoutSegment(s)`** derives segments from the pathname rather than being truly layout-aware. May differ from Next.js in edge cases with parallel routes.
|
|
562
561
|
- **Route segment config** — `runtime` and `preferredRegion` are ignored (everything runs in the same environment).
|
|
563
562
|
- **Node.js production server (`vinext start`)** works for testing but is less complete than Workers deployment. Cloudflare Workers is the primary target.
|
|
564
563
|
- **Native Node modules (sharp, resvg, satori, lightningcss, @napi-rs/canvas)** crash Vite's RSC dev environment. Dynamic OG image/icon routes using these work in production builds but not in dev mode. These are auto-stubbed during `vinext deploy`.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { UserConfig } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/build/client-build-config.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Create a manualChunks function for client builds.
|
|
6
|
+
*
|
|
7
|
+
* Splits the client bundle into:
|
|
8
|
+
* - "framework" — React, ReactDOM, and scheduler (loaded on every page)
|
|
9
|
+
* - "vinext" — vinext shims (router, head, link, etc.)
|
|
10
|
+
*
|
|
11
|
+
* All other vendor code is left to Rollup's default chunk-splitting
|
|
12
|
+
* algorithm. Rollup automatically deduplicates shared modules into
|
|
13
|
+
* common chunks based on the import graph — no manual intervention
|
|
14
|
+
* needed.
|
|
15
|
+
*
|
|
16
|
+
* Why not split every npm package into its own chunk?
|
|
17
|
+
* - Per-package splitting (`vendor-X`) creates 50-200+ chunks for a
|
|
18
|
+
* typical app, far exceeding the ~25-request sweet spot for HTTP/2.
|
|
19
|
+
* - gzip/brotli compress small files poorly — each file restarts with
|
|
20
|
+
* an empty dictionary, losing ~5-15% total compressed size vs fewer
|
|
21
|
+
* larger chunks (Khan Academy measured +2.5% wire size with 10x
|
|
22
|
+
* more files containing less raw code).
|
|
23
|
+
* - ES module evaluation has per-module overhead that compounds on
|
|
24
|
+
* mobile devices.
|
|
25
|
+
* - No major Vite-based framework (Remix, SvelteKit, Astro, TanStack)
|
|
26
|
+
* uses per-package splitting. Next.js only isolates packages >160KB.
|
|
27
|
+
* - Rollup's graph-based splitting already handles the common case
|
|
28
|
+
* well: shared dependencies between routes get their own chunks,
|
|
29
|
+
* and route-specific code stays in route chunks.
|
|
30
|
+
*/
|
|
31
|
+
declare function createClientManualChunks(shimsDir: string): (id: string) => string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Rollup output config with manualChunks for client code-splitting.
|
|
34
|
+
* Used by both CLI builds and multi-environment builds.
|
|
35
|
+
*
|
|
36
|
+
* experimentalMinChunkSize merges tiny shared chunks (< 10KB) back into
|
|
37
|
+
* their importers. This reduces HTTP request count and improves gzip
|
|
38
|
+
* compression efficiency — small files restart the compression dictionary,
|
|
39
|
+
* adding ~5-15% wire overhead vs fewer larger chunks.
|
|
40
|
+
*/
|
|
41
|
+
declare function createClientOutputConfig(clientManualChunks: (id: string) => string | undefined): {
|
|
42
|
+
manualChunks: (id: string) => string | undefined;
|
|
43
|
+
experimentalMinChunkSize: number;
|
|
44
|
+
};
|
|
45
|
+
declare function createClientCodeSplittingConfig(clientManualChunks: (id: string) => string | undefined): {
|
|
46
|
+
minSize: number;
|
|
47
|
+
groups: {
|
|
48
|
+
name(moduleId: string): string | null;
|
|
49
|
+
}[];
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Rollup treeshake configuration for production client builds.
|
|
53
|
+
*
|
|
54
|
+
* Uses the 'recommended' preset as a safe base, then overrides
|
|
55
|
+
* moduleSideEffects to strip unused re-exports from npm packages.
|
|
56
|
+
*
|
|
57
|
+
* The 'no-external' value for moduleSideEffects means:
|
|
58
|
+
* - Local project modules: preserve side effects (CSS imports, polyfills)
|
|
59
|
+
* - node_modules packages: treat as side-effect-free unless exports are used
|
|
60
|
+
*
|
|
61
|
+
* This is the single highest-impact optimization for large barrel-exporting
|
|
62
|
+
* libraries like mermaid, @mui/material, lucide-react, etc. These libraries
|
|
63
|
+
* re-export hundreds of sub-modules through barrel files. Without this,
|
|
64
|
+
* Rollup preserves every sub-module even when only a few exports are consumed.
|
|
65
|
+
*
|
|
66
|
+
* Why 'no-external' instead of false (global side-effect-free)?
|
|
67
|
+
* - User code may rely on import-time side effects (e.g., `import './global.css'`)
|
|
68
|
+
* - 'no-external' is safe for app code while still enabling aggressive DCE for deps
|
|
69
|
+
*
|
|
70
|
+
* Why not the 'smallest' preset?
|
|
71
|
+
* - 'smallest' also sets propertyReadSideEffects: false and
|
|
72
|
+
* tryCatchDeoptimization: false, which can break specific libraries
|
|
73
|
+
* that rely on property access side effects or try/catch for feature detection
|
|
74
|
+
* - 'recommended' + 'no-external' gives most of the benefit with less risk
|
|
75
|
+
*
|
|
76
|
+
* @deprecated Use getClientTreeshakeConfigForVite(viteMajorVersion) instead
|
|
77
|
+
* for Vite version compatibility. Kept for backward compatibility.
|
|
78
|
+
*/
|
|
79
|
+
declare const clientTreeshakeConfig: {
|
|
80
|
+
preset: "recommended";
|
|
81
|
+
moduleSideEffects: "no-external";
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Returns treeshake configuration appropriate for the Vite version.
|
|
85
|
+
*
|
|
86
|
+
* Rollup (Vite 7) supports presets like "recommended" which set multiple
|
|
87
|
+
* treeshake options at once. Rolldown (Vite 8+) doesn't support presets,
|
|
88
|
+
* so we only return moduleSideEffects for Vite 8+.
|
|
89
|
+
*
|
|
90
|
+
* The Rollup "recommended" preset sets:
|
|
91
|
+
* - annotations: true (Rolldown default is also true)
|
|
92
|
+
* - manualPureFunctions: [] (Rolldown default is also [])
|
|
93
|
+
* - propertyReadSideEffects: true (Rolldown equivalent is 'always', the default)
|
|
94
|
+
* - unknownGlobalSideEffects: false (Rolldown default is true — this is a known acceptable
|
|
95
|
+
* divergence. Slightly less aggressive DCE on unknown globals, acceptable for client bundles)
|
|
96
|
+
* - correctVarValueBeforeDeclaration and tryCatchDeoptimization (Rolldown handles these differently)
|
|
97
|
+
*
|
|
98
|
+
* The key optimization is moduleSideEffects: "no-external", which is supported
|
|
99
|
+
* by both bundlers and provides the DCE benefits for barrel-exporting libraries.
|
|
100
|
+
* It treats node_modules as side-effect-free (enabling aggressive DCE) while
|
|
101
|
+
* preserving side effects in local code.
|
|
102
|
+
*/
|
|
103
|
+
declare function getClientTreeshakeConfigForVite(viteMajorVersion: number): {
|
|
104
|
+
moduleSideEffects: "no-external";
|
|
105
|
+
preset?: undefined;
|
|
106
|
+
} | {
|
|
107
|
+
preset: "recommended";
|
|
108
|
+
moduleSideEffects: "no-external";
|
|
109
|
+
};
|
|
110
|
+
type VinextBuildConfig = NonNullable<UserConfig["build"]>;
|
|
111
|
+
type VinextBuildBundlerOptions = NonNullable<VinextBuildConfig["rolldownOptions"]>;
|
|
112
|
+
type VinextBuildConfigWithLegacy = VinextBuildConfig & {
|
|
113
|
+
rollupOptions?: VinextBuildBundlerOptions;
|
|
114
|
+
};
|
|
115
|
+
declare function getBuildBundlerOptions(build: UserConfig["build"] | undefined): VinextBuildBundlerOptions | undefined;
|
|
116
|
+
declare function withBuildBundlerOptions(viteMajorVersion: number, bundlerOptions: VinextBuildBundlerOptions): Partial<VinextBuildConfigWithLegacy>;
|
|
117
|
+
//#endregion
|
|
118
|
+
export { clientTreeshakeConfig, createClientCodeSplittingConfig, createClientManualChunks, createClientOutputConfig, getBuildBundlerOptions, getClientTreeshakeConfigForVite, withBuildBundlerOptions };
|
|
119
|
+
//# sourceMappingURL=client-build-config.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
//#region src/build/client-build-config.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extract the npm package name from a module ID (file path).
|
|
4
|
+
* Returns null if not in node_modules.
|
|
5
|
+
*
|
|
6
|
+
* Handles scoped packages (@org/pkg) and pnpm-style paths
|
|
7
|
+
* (node_modules/.pnpm/pkg@ver/node_modules/pkg).
|
|
8
|
+
*/
|
|
9
|
+
function getPackageName(id) {
|
|
10
|
+
const nmIdx = id.lastIndexOf("node_modules/");
|
|
11
|
+
if (nmIdx === -1) return null;
|
|
12
|
+
const rest = id.slice(nmIdx + 13);
|
|
13
|
+
if (rest.startsWith("@")) {
|
|
14
|
+
const parts = rest.split("/");
|
|
15
|
+
return parts.length >= 2 ? parts[0] + "/" + parts[1] : null;
|
|
16
|
+
}
|
|
17
|
+
return rest.split("/")[0] || null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create a manualChunks function for client builds.
|
|
21
|
+
*
|
|
22
|
+
* Splits the client bundle into:
|
|
23
|
+
* - "framework" — React, ReactDOM, and scheduler (loaded on every page)
|
|
24
|
+
* - "vinext" — vinext shims (router, head, link, etc.)
|
|
25
|
+
*
|
|
26
|
+
* All other vendor code is left to Rollup's default chunk-splitting
|
|
27
|
+
* algorithm. Rollup automatically deduplicates shared modules into
|
|
28
|
+
* common chunks based on the import graph — no manual intervention
|
|
29
|
+
* needed.
|
|
30
|
+
*
|
|
31
|
+
* Why not split every npm package into its own chunk?
|
|
32
|
+
* - Per-package splitting (`vendor-X`) creates 50-200+ chunks for a
|
|
33
|
+
* typical app, far exceeding the ~25-request sweet spot for HTTP/2.
|
|
34
|
+
* - gzip/brotli compress small files poorly — each file restarts with
|
|
35
|
+
* an empty dictionary, losing ~5-15% total compressed size vs fewer
|
|
36
|
+
* larger chunks (Khan Academy measured +2.5% wire size with 10x
|
|
37
|
+
* more files containing less raw code).
|
|
38
|
+
* - ES module evaluation has per-module overhead that compounds on
|
|
39
|
+
* mobile devices.
|
|
40
|
+
* - No major Vite-based framework (Remix, SvelteKit, Astro, TanStack)
|
|
41
|
+
* uses per-package splitting. Next.js only isolates packages >160KB.
|
|
42
|
+
* - Rollup's graph-based splitting already handles the common case
|
|
43
|
+
* well: shared dependencies between routes get their own chunks,
|
|
44
|
+
* and route-specific code stays in route chunks.
|
|
45
|
+
*/
|
|
46
|
+
function createClientManualChunks(shimsDir) {
|
|
47
|
+
return function clientManualChunks(id) {
|
|
48
|
+
if (id.includes("node_modules")) {
|
|
49
|
+
const pkg = getPackageName(id);
|
|
50
|
+
if (!pkg) return void 0;
|
|
51
|
+
if (pkg === "react" || pkg === "react-dom" || pkg === "scheduler") return "framework";
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (id.startsWith(shimsDir)) return "vinext";
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Rollup output config with manualChunks for client code-splitting.
|
|
59
|
+
* Used by both CLI builds and multi-environment builds.
|
|
60
|
+
*
|
|
61
|
+
* experimentalMinChunkSize merges tiny shared chunks (< 10KB) back into
|
|
62
|
+
* their importers. This reduces HTTP request count and improves gzip
|
|
63
|
+
* compression efficiency — small files restart the compression dictionary,
|
|
64
|
+
* adding ~5-15% wire overhead vs fewer larger chunks.
|
|
65
|
+
*/
|
|
66
|
+
function createClientOutputConfig(clientManualChunks) {
|
|
67
|
+
return {
|
|
68
|
+
manualChunks: clientManualChunks,
|
|
69
|
+
experimentalMinChunkSize: 1e4
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function createClientCodeSplittingConfig(clientManualChunks) {
|
|
73
|
+
return {
|
|
74
|
+
minSize: 1e4,
|
|
75
|
+
groups: [{ name(moduleId) {
|
|
76
|
+
return clientManualChunks(moduleId) ?? null;
|
|
77
|
+
} }]
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Rollup treeshake configuration for production client builds.
|
|
82
|
+
*
|
|
83
|
+
* Uses the 'recommended' preset as a safe base, then overrides
|
|
84
|
+
* moduleSideEffects to strip unused re-exports from npm packages.
|
|
85
|
+
*
|
|
86
|
+
* The 'no-external' value for moduleSideEffects means:
|
|
87
|
+
* - Local project modules: preserve side effects (CSS imports, polyfills)
|
|
88
|
+
* - node_modules packages: treat as side-effect-free unless exports are used
|
|
89
|
+
*
|
|
90
|
+
* This is the single highest-impact optimization for large barrel-exporting
|
|
91
|
+
* libraries like mermaid, @mui/material, lucide-react, etc. These libraries
|
|
92
|
+
* re-export hundreds of sub-modules through barrel files. Without this,
|
|
93
|
+
* Rollup preserves every sub-module even when only a few exports are consumed.
|
|
94
|
+
*
|
|
95
|
+
* Why 'no-external' instead of false (global side-effect-free)?
|
|
96
|
+
* - User code may rely on import-time side effects (e.g., `import './global.css'`)
|
|
97
|
+
* - 'no-external' is safe for app code while still enabling aggressive DCE for deps
|
|
98
|
+
*
|
|
99
|
+
* Why not the 'smallest' preset?
|
|
100
|
+
* - 'smallest' also sets propertyReadSideEffects: false and
|
|
101
|
+
* tryCatchDeoptimization: false, which can break specific libraries
|
|
102
|
+
* that rely on property access side effects or try/catch for feature detection
|
|
103
|
+
* - 'recommended' + 'no-external' gives most of the benefit with less risk
|
|
104
|
+
*
|
|
105
|
+
* @deprecated Use getClientTreeshakeConfigForVite(viteMajorVersion) instead
|
|
106
|
+
* for Vite version compatibility. Kept for backward compatibility.
|
|
107
|
+
*/
|
|
108
|
+
const clientTreeshakeConfig = {
|
|
109
|
+
preset: "recommended",
|
|
110
|
+
moduleSideEffects: "no-external"
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Returns treeshake configuration appropriate for the Vite version.
|
|
114
|
+
*
|
|
115
|
+
* Rollup (Vite 7) supports presets like "recommended" which set multiple
|
|
116
|
+
* treeshake options at once. Rolldown (Vite 8+) doesn't support presets,
|
|
117
|
+
* so we only return moduleSideEffects for Vite 8+.
|
|
118
|
+
*
|
|
119
|
+
* The Rollup "recommended" preset sets:
|
|
120
|
+
* - annotations: true (Rolldown default is also true)
|
|
121
|
+
* - manualPureFunctions: [] (Rolldown default is also [])
|
|
122
|
+
* - propertyReadSideEffects: true (Rolldown equivalent is 'always', the default)
|
|
123
|
+
* - unknownGlobalSideEffects: false (Rolldown default is true — this is a known acceptable
|
|
124
|
+
* divergence. Slightly less aggressive DCE on unknown globals, acceptable for client bundles)
|
|
125
|
+
* - correctVarValueBeforeDeclaration and tryCatchDeoptimization (Rolldown handles these differently)
|
|
126
|
+
*
|
|
127
|
+
* The key optimization is moduleSideEffects: "no-external", which is supported
|
|
128
|
+
* by both bundlers and provides the DCE benefits for barrel-exporting libraries.
|
|
129
|
+
* It treats node_modules as side-effect-free (enabling aggressive DCE) while
|
|
130
|
+
* preserving side effects in local code.
|
|
131
|
+
*/
|
|
132
|
+
function getClientTreeshakeConfigForVite(viteMajorVersion) {
|
|
133
|
+
if (viteMajorVersion >= 8) return { moduleSideEffects: "no-external" };
|
|
134
|
+
return {
|
|
135
|
+
preset: "recommended",
|
|
136
|
+
moduleSideEffects: "no-external"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function getBuildBundlerOptions(build) {
|
|
140
|
+
const buildConfig = build;
|
|
141
|
+
return buildConfig?.rolldownOptions ?? buildConfig?.rollupOptions;
|
|
142
|
+
}
|
|
143
|
+
function withBuildBundlerOptions(viteMajorVersion, bundlerOptions) {
|
|
144
|
+
return viteMajorVersion >= 8 ? { rolldownOptions: bundlerOptions } : { rollupOptions: bundlerOptions };
|
|
145
|
+
}
|
|
146
|
+
//#endregion
|
|
147
|
+
export { clientTreeshakeConfig, createClientCodeSplittingConfig, createClientManualChunks, createClientOutputConfig, getBuildBundlerOptions, getClientTreeshakeConfigForVite, withBuildBundlerOptions };
|
|
148
|
+
|
|
149
|
+
//# sourceMappingURL=client-build-config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-build-config.js","names":[],"sources":["../../src/build/client-build-config.ts"],"sourcesContent":["import type { UserConfig } from \"vite\";\n\n/**\n * Extract the npm package name from a module ID (file path).\n * Returns null if not in node_modules.\n *\n * Handles scoped packages (@org/pkg) and pnpm-style paths\n * (node_modules/.pnpm/pkg@ver/node_modules/pkg).\n */\nfunction getPackageName(id: string): string | null {\n const nmIdx = id.lastIndexOf(\"node_modules/\");\n if (nmIdx === -1) return null;\n const rest = id.slice(nmIdx + \"node_modules/\".length);\n if (rest.startsWith(\"@\")) {\n // Scoped package: @org/pkg\n const parts = rest.split(\"/\");\n return parts.length >= 2 ? parts[0] + \"/\" + parts[1] : null;\n }\n return rest.split(\"/\")[0] || null;\n}\n\n/**\n * Create a manualChunks function for client builds.\n *\n * Splits the client bundle into:\n * - \"framework\" — React, ReactDOM, and scheduler (loaded on every page)\n * - \"vinext\" — vinext shims (router, head, link, etc.)\n *\n * All other vendor code is left to Rollup's default chunk-splitting\n * algorithm. Rollup automatically deduplicates shared modules into\n * common chunks based on the import graph — no manual intervention\n * needed.\n *\n * Why not split every npm package into its own chunk?\n * - Per-package splitting (`vendor-X`) creates 50-200+ chunks for a\n * typical app, far exceeding the ~25-request sweet spot for HTTP/2.\n * - gzip/brotli compress small files poorly — each file restarts with\n * an empty dictionary, losing ~5-15% total compressed size vs fewer\n * larger chunks (Khan Academy measured +2.5% wire size with 10x\n * more files containing less raw code).\n * - ES module evaluation has per-module overhead that compounds on\n * mobile devices.\n * - No major Vite-based framework (Remix, SvelteKit, Astro, TanStack)\n * uses per-package splitting. Next.js only isolates packages >160KB.\n * - Rollup's graph-based splitting already handles the common case\n * well: shared dependencies between routes get their own chunks,\n * and route-specific code stays in route chunks.\n */\nexport function createClientManualChunks(shimsDir: string) {\n return function clientManualChunks(id: string): string | undefined {\n // React framework — always loaded, shared across all pages.\n // Isolating React into its own chunk is the single highest-value\n // split: it's ~130KB compressed, loaded on every page, and its\n // content hash rarely changes between deploys.\n if (id.includes(\"node_modules\")) {\n const pkg = getPackageName(id);\n if (!pkg) return undefined;\n if (pkg === \"react\" || pkg === \"react-dom\" || pkg === \"scheduler\") {\n return \"framework\";\n }\n // Let Rollup handle all other vendor code via its default\n // graph-based splitting. This produces a reasonable number of\n // shared chunks (typically 5-15) based on actual import patterns,\n // with good compression efficiency.\n return undefined;\n }\n\n // vinext shims — small runtime, shared across all pages.\n // Use the absolute shims directory path to avoid matching user files\n // that happen to have \"/shims/\" in their path.\n if (id.startsWith(shimsDir)) {\n return \"vinext\";\n }\n\n return undefined;\n };\n}\n\n/**\n * Rollup output config with manualChunks for client code-splitting.\n * Used by both CLI builds and multi-environment builds.\n *\n * experimentalMinChunkSize merges tiny shared chunks (< 10KB) back into\n * their importers. This reduces HTTP request count and improves gzip\n * compression efficiency — small files restart the compression dictionary,\n * adding ~5-15% wire overhead vs fewer larger chunks.\n */\nexport function createClientOutputConfig(clientManualChunks: (id: string) => string | undefined) {\n return {\n manualChunks: clientManualChunks,\n experimentalMinChunkSize: 10_000,\n };\n}\n\nexport function createClientCodeSplittingConfig(\n clientManualChunks: (id: string) => string | undefined,\n) {\n return {\n minSize: 10_000,\n groups: [\n {\n name(moduleId: string) {\n return clientManualChunks(moduleId) ?? null;\n },\n },\n ],\n };\n}\n\n/**\n * Rollup treeshake configuration for production client builds.\n *\n * Uses the 'recommended' preset as a safe base, then overrides\n * moduleSideEffects to strip unused re-exports from npm packages.\n *\n * The 'no-external' value for moduleSideEffects means:\n * - Local project modules: preserve side effects (CSS imports, polyfills)\n * - node_modules packages: treat as side-effect-free unless exports are used\n *\n * This is the single highest-impact optimization for large barrel-exporting\n * libraries like mermaid, @mui/material, lucide-react, etc. These libraries\n * re-export hundreds of sub-modules through barrel files. Without this,\n * Rollup preserves every sub-module even when only a few exports are consumed.\n *\n * Why 'no-external' instead of false (global side-effect-free)?\n * - User code may rely on import-time side effects (e.g., `import './global.css'`)\n * - 'no-external' is safe for app code while still enabling aggressive DCE for deps\n *\n * Why not the 'smallest' preset?\n * - 'smallest' also sets propertyReadSideEffects: false and\n * tryCatchDeoptimization: false, which can break specific libraries\n * that rely on property access side effects or try/catch for feature detection\n * - 'recommended' + 'no-external' gives most of the benefit with less risk\n *\n * @deprecated Use getClientTreeshakeConfigForVite(viteMajorVersion) instead\n * for Vite version compatibility. Kept for backward compatibility.\n */\nexport const clientTreeshakeConfig = {\n preset: \"recommended\" as const,\n moduleSideEffects: \"no-external\" as const,\n};\n\n/**\n * Returns treeshake configuration appropriate for the Vite version.\n *\n * Rollup (Vite 7) supports presets like \"recommended\" which set multiple\n * treeshake options at once. Rolldown (Vite 8+) doesn't support presets,\n * so we only return moduleSideEffects for Vite 8+.\n *\n * The Rollup \"recommended\" preset sets:\n * - annotations: true (Rolldown default is also true)\n * - manualPureFunctions: [] (Rolldown default is also [])\n * - propertyReadSideEffects: true (Rolldown equivalent is 'always', the default)\n * - unknownGlobalSideEffects: false (Rolldown default is true — this is a known acceptable\n * divergence. Slightly less aggressive DCE on unknown globals, acceptable for client bundles)\n * - correctVarValueBeforeDeclaration and tryCatchDeoptimization (Rolldown handles these differently)\n *\n * The key optimization is moduleSideEffects: \"no-external\", which is supported\n * by both bundlers and provides the DCE benefits for barrel-exporting libraries.\n * It treats node_modules as side-effect-free (enabling aggressive DCE) while\n * preserving side effects in local code.\n */\nexport function getClientTreeshakeConfigForVite(viteMajorVersion: number) {\n if (viteMajorVersion >= 8) {\n // Rolldown (Vite 8+) - no preset support, only specific options.\n // Rolldown's built-in defaults already cover what Rollup's 'recommended'\n // preset provides (annotations, correctContext, tryCatchDeoptimization).\n return {\n moduleSideEffects: \"no-external\" as const,\n };\n }\n // Rollup (Vite 7) - supports presets for convenient option grouping\n return {\n preset: \"recommended\" as const,\n moduleSideEffects: \"no-external\" as const,\n };\n}\n\ntype VinextBuildConfig = NonNullable<UserConfig[\"build\"]>;\ntype VinextBuildBundlerOptions = NonNullable<VinextBuildConfig[\"rolldownOptions\"]>;\ntype VinextBuildConfigWithLegacy = VinextBuildConfig & {\n rollupOptions?: VinextBuildBundlerOptions;\n};\n\nexport function getBuildBundlerOptions(\n build: UserConfig[\"build\"] | undefined,\n): VinextBuildBundlerOptions | undefined {\n const buildConfig = build as VinextBuildConfigWithLegacy | undefined;\n return buildConfig?.rolldownOptions ?? buildConfig?.rollupOptions;\n}\n\nexport function withBuildBundlerOptions(\n viteMajorVersion: number,\n bundlerOptions: VinextBuildBundlerOptions,\n): Partial<VinextBuildConfigWithLegacy> {\n return viteMajorVersion >= 8\n ? { rolldownOptions: bundlerOptions }\n : { rollupOptions: bundlerOptions };\n}\n"],"mappings":";;;;;;;;AASA,SAAS,eAAe,IAA2B;CACjD,MAAM,QAAQ,GAAG,YAAY,gBAAgB;AAC7C,KAAI,UAAU,GAAI,QAAO;CACzB,MAAM,OAAO,GAAG,MAAM,QAAQ,GAAuB;AACrD,KAAI,KAAK,WAAW,IAAI,EAAE;EAExB,MAAM,QAAQ,KAAK,MAAM,IAAI;AAC7B,SAAO,MAAM,UAAU,IAAI,MAAM,KAAK,MAAM,MAAM,KAAK;;AAEzD,QAAO,KAAK,MAAM,IAAI,CAAC,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8B/B,SAAgB,yBAAyB,UAAkB;AACzD,QAAO,SAAS,mBAAmB,IAAgC;AAKjE,MAAI,GAAG,SAAS,eAAe,EAAE;GAC/B,MAAM,MAAM,eAAe,GAAG;AAC9B,OAAI,CAAC,IAAK,QAAO,KAAA;AACjB,OAAI,QAAQ,WAAW,QAAQ,eAAe,QAAQ,YACpD,QAAO;AAMT;;AAMF,MAAI,GAAG,WAAW,SAAS,CACzB,QAAO;;;;;;;;;;;;AAgBb,SAAgB,yBAAyB,oBAAwD;AAC/F,QAAO;EACL,cAAc;EACd,0BAA0B;EAC3B;;AAGH,SAAgB,gCACd,oBACA;AACA,QAAO;EACL,SAAS;EACT,QAAQ,CACN,EACE,KAAK,UAAkB;AACrB,UAAO,mBAAmB,SAAS,IAAI;KAE1C,CACF;EACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BH,MAAa,wBAAwB;CACnC,QAAQ;CACR,mBAAmB;CACpB;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,gCAAgC,kBAA0B;AACxE,KAAI,oBAAoB,EAItB,QAAO,EACL,mBAAmB,eACpB;AAGH,QAAO;EACL,QAAQ;EACR,mBAAmB;EACpB;;AASH,SAAgB,uBACd,OACuC;CACvC,MAAM,cAAc;AACpB,QAAO,aAAa,mBAAmB,aAAa;;AAGtD,SAAgB,wBACd,kBACA,gBACsC;AACtC,QAAO,oBAAoB,IACvB,EAAE,iBAAiB,gBAAgB,GACnC,EAAE,eAAe,gBAAgB"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
//#region src/build/layout-classification-types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Shared types for the layout classification pipeline.
|
|
4
|
+
*
|
|
5
|
+
* Kept in a leaf module so both `report.ts` (which implements segment-config
|
|
6
|
+
* classification) and `layout-classification.ts` (which composes the full
|
|
7
|
+
* pipeline) can import them without forming a cycle.
|
|
8
|
+
*
|
|
9
|
+
* The wire contract between build and runtime is intentionally narrow: the
|
|
10
|
+
* runtime only cares about the `"static" | "dynamic"` decision for a layout.
|
|
11
|
+
* Reasons live in a sidecar structure so operators can trace how each
|
|
12
|
+
* decision was made without bloating the hot-path payload.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Structured record of which classifier layer produced a decision and what
|
|
16
|
+
* evidence it used. Kept as a discriminated union so each layer can carry
|
|
17
|
+
* its own diagnostic shape without the consumer having to fall back to
|
|
18
|
+
* stringly-typed `reason` fields.
|
|
19
|
+
*/
|
|
20
|
+
type ClassificationReason = {
|
|
21
|
+
layer: "segment-config";
|
|
22
|
+
key: "dynamic" | "revalidate";
|
|
23
|
+
value: string | number;
|
|
24
|
+
} | {
|
|
25
|
+
layer: "module-graph";
|
|
26
|
+
result: "static" | "needs-probe";
|
|
27
|
+
firstShimMatch?: string;
|
|
28
|
+
} | {
|
|
29
|
+
layer: "runtime-probe";
|
|
30
|
+
outcome: "static" | "dynamic";
|
|
31
|
+
error?: string;
|
|
32
|
+
} | {
|
|
33
|
+
layer: "no-classifier";
|
|
34
|
+
};
|
|
35
|
+
type ModuleGraphStaticReason = {
|
|
36
|
+
layer: "module-graph";
|
|
37
|
+
result: "static";
|
|
38
|
+
firstShimMatch?: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Build-time classification outcome for a single layout. Tagged with `kind`
|
|
42
|
+
* so callers can branch exhaustively and carry diagnostic reasons alongside
|
|
43
|
+
* the decision.
|
|
44
|
+
*
|
|
45
|
+
* `absent` means no classifier layer had anything to say — the caller should
|
|
46
|
+
* defer to the next layer (or to the runtime probe).
|
|
47
|
+
*/
|
|
48
|
+
type LayoutBuildClassification = {
|
|
49
|
+
kind: "absent";
|
|
50
|
+
} | {
|
|
51
|
+
kind: "static";
|
|
52
|
+
reason: ClassificationReason;
|
|
53
|
+
} | {
|
|
54
|
+
kind: "dynamic";
|
|
55
|
+
reason: ClassificationReason;
|
|
56
|
+
} | {
|
|
57
|
+
kind: "needs-probe";
|
|
58
|
+
reason: ClassificationReason;
|
|
59
|
+
};
|
|
60
|
+
//#endregion
|
|
61
|
+
export { ClassificationReason, LayoutBuildClassification, ModuleGraphStaticReason };
|
|
62
|
+
//# sourceMappingURL=layout-classification-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ClassificationReason, LayoutBuildClassification, ModuleGraphStaticReason } from "./layout-classification-types.js";
|
|
2
|
+
|
|
3
|
+
//#region src/build/layout-classification.d.ts
|
|
4
|
+
type ModuleGraphClassification = "static" | "needs-probe";
|
|
5
|
+
type ModuleGraphClassificationResult = {
|
|
6
|
+
result: ModuleGraphClassification; /** First dynamic shim module ID encountered during BFS, when any. */
|
|
7
|
+
firstShimMatch?: string;
|
|
8
|
+
};
|
|
9
|
+
type ModuleInfoProvider = {
|
|
10
|
+
getModuleInfo(id: string): {
|
|
11
|
+
importedIds: string[];
|
|
12
|
+
dynamicImportedIds: string[];
|
|
13
|
+
} | null;
|
|
14
|
+
};
|
|
15
|
+
type LayoutEntry = {
|
|
16
|
+
/** Rollup/Vite module ID for the layout file. */moduleId: string; /** Directory depth from the app root, used to build the stable layout ID. */
|
|
17
|
+
treePosition: number; /** Segment config source code extracted at build time, or null when absent. */
|
|
18
|
+
segmentConfig?: {
|
|
19
|
+
code: string;
|
|
20
|
+
} | null;
|
|
21
|
+
};
|
|
22
|
+
type RouteForClassification = {
|
|
23
|
+
layouts: readonly LayoutEntry[];
|
|
24
|
+
routeSegments: string[];
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* BFS traversal of a layout's dependency tree. If any transitive import
|
|
28
|
+
* resolves to a dynamic shim path (headers, cache, server), the layout
|
|
29
|
+
* cannot be proven static at build time and needs a runtime probe.
|
|
30
|
+
*
|
|
31
|
+
* The returned object carries the classification plus the first matching
|
|
32
|
+
* shim module ID (when any). Operators use the shim ID via the debug
|
|
33
|
+
* channel to trace why a layout was flagged for probing.
|
|
34
|
+
*/
|
|
35
|
+
declare function classifyLayoutByModuleGraph(layoutModuleId: string, dynamicShimPaths: ReadonlySet<string>, moduleInfo: ModuleInfoProvider): ModuleGraphClassificationResult;
|
|
36
|
+
declare function moduleGraphReason(graphResult: ModuleGraphClassificationResult & {
|
|
37
|
+
result: "static";
|
|
38
|
+
}): ModuleGraphStaticReason;
|
|
39
|
+
declare function moduleGraphReason(graphResult: ModuleGraphClassificationResult): ClassificationReason;
|
|
40
|
+
declare function isStaticModuleGraphResult(graphResult: ModuleGraphClassificationResult): graphResult is ModuleGraphClassificationResult & {
|
|
41
|
+
result: "static";
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Classifies all layouts across all routes using a two-layer strategy:
|
|
45
|
+
*
|
|
46
|
+
* 1. Segment config (Layer 1) — short-circuits to "static" or "dynamic"
|
|
47
|
+
* 2. Module graph (Layer 2) — BFS for dynamic shim imports → "static" or "needs-probe"
|
|
48
|
+
*
|
|
49
|
+
* Shared layouts (same file appearing in multiple routes) are classified once
|
|
50
|
+
* and deduplicated by layout ID.
|
|
51
|
+
*
|
|
52
|
+
* @internal Not called by production code. The `generateBundle` hook in
|
|
53
|
+
* `index.ts` calls `classifyLayoutByModuleGraph` directly and composes
|
|
54
|
+
* via the numeric-index manifest in `route-classification-manifest.ts`.
|
|
55
|
+
* Used only by `tests/layout-classification.test.ts`.
|
|
56
|
+
*/
|
|
57
|
+
declare function classifyAllRouteLayouts(routes: readonly RouteForClassification[], dynamicShimPaths: ReadonlySet<string>, moduleInfo: ModuleInfoProvider): Map<string, LayoutBuildClassification>;
|
|
58
|
+
//#endregion
|
|
59
|
+
export { type ClassificationReason, type LayoutBuildClassification, ModuleGraphClassification, ModuleGraphClassificationResult, type ModuleGraphStaticReason, ModuleInfoProvider, classifyAllRouteLayouts, classifyLayoutByModuleGraph, isStaticModuleGraphResult, moduleGraphReason };
|
|
60
|
+
//# sourceMappingURL=layout-classification.d.ts.map
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { classifyLayoutSegmentConfig } from "./report.js";
|
|
2
|
+
import { createAppPageTreePath } from "../server/app-page-route-wiring.js";
|
|
3
|
+
//#region src/build/layout-classification.ts
|
|
4
|
+
/**
|
|
5
|
+
* Layout classification — determines whether each layout in an App Router
|
|
6
|
+
* route tree is static or dynamic via two complementary detection layers:
|
|
7
|
+
*
|
|
8
|
+
* Layer 1: Segment config (`export const dynamic`, `export const revalidate`)
|
|
9
|
+
* Layer 2: Module graph traversal (checks for transitive dynamic shim imports)
|
|
10
|
+
*
|
|
11
|
+
* Layer 3 (probe-based runtime detection) is handled separately in
|
|
12
|
+
* `app-page-execution.ts` at request time.
|
|
13
|
+
*
|
|
14
|
+
* Every result is carried as a `LayoutBuildClassification` tagged variant so
|
|
15
|
+
* operators can trace which layer produced a decision via the structured
|
|
16
|
+
* `ClassificationReason` sidecar without that metadata leaking onto the wire.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* BFS traversal of a layout's dependency tree. If any transitive import
|
|
20
|
+
* resolves to a dynamic shim path (headers, cache, server), the layout
|
|
21
|
+
* cannot be proven static at build time and needs a runtime probe.
|
|
22
|
+
*
|
|
23
|
+
* The returned object carries the classification plus the first matching
|
|
24
|
+
* shim module ID (when any). Operators use the shim ID via the debug
|
|
25
|
+
* channel to trace why a layout was flagged for probing.
|
|
26
|
+
*/
|
|
27
|
+
function classifyLayoutByModuleGraph(layoutModuleId, dynamicShimPaths, moduleInfo) {
|
|
28
|
+
const visited = /* @__PURE__ */ new Set();
|
|
29
|
+
const queue = [layoutModuleId];
|
|
30
|
+
let head = 0;
|
|
31
|
+
while (head < queue.length) {
|
|
32
|
+
const currentId = queue[head++];
|
|
33
|
+
if (visited.has(currentId)) continue;
|
|
34
|
+
visited.add(currentId);
|
|
35
|
+
if (dynamicShimPaths.has(currentId)) return {
|
|
36
|
+
result: "needs-probe",
|
|
37
|
+
firstShimMatch: currentId
|
|
38
|
+
};
|
|
39
|
+
const info = moduleInfo.getModuleInfo(currentId);
|
|
40
|
+
if (!info) continue;
|
|
41
|
+
for (const importedId of info.importedIds) if (!visited.has(importedId)) queue.push(importedId);
|
|
42
|
+
for (const dynamicId of info.dynamicImportedIds) if (!visited.has(dynamicId)) queue.push(dynamicId);
|
|
43
|
+
}
|
|
44
|
+
return { result: "static" };
|
|
45
|
+
}
|
|
46
|
+
function moduleGraphReason(graphResult) {
|
|
47
|
+
if (graphResult.firstShimMatch === void 0) return {
|
|
48
|
+
layer: "module-graph",
|
|
49
|
+
result: graphResult.result
|
|
50
|
+
};
|
|
51
|
+
return {
|
|
52
|
+
layer: "module-graph",
|
|
53
|
+
result: graphResult.result,
|
|
54
|
+
firstShimMatch: graphResult.firstShimMatch
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function isStaticModuleGraphResult(graphResult) {
|
|
58
|
+
return graphResult.result === "static";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Classifies all layouts across all routes using a two-layer strategy:
|
|
62
|
+
*
|
|
63
|
+
* 1. Segment config (Layer 1) — short-circuits to "static" or "dynamic"
|
|
64
|
+
* 2. Module graph (Layer 2) — BFS for dynamic shim imports → "static" or "needs-probe"
|
|
65
|
+
*
|
|
66
|
+
* Shared layouts (same file appearing in multiple routes) are classified once
|
|
67
|
+
* and deduplicated by layout ID.
|
|
68
|
+
*
|
|
69
|
+
* @internal Not called by production code. The `generateBundle` hook in
|
|
70
|
+
* `index.ts` calls `classifyLayoutByModuleGraph` directly and composes
|
|
71
|
+
* via the numeric-index manifest in `route-classification-manifest.ts`.
|
|
72
|
+
* Used only by `tests/layout-classification.test.ts`.
|
|
73
|
+
*/
|
|
74
|
+
function classifyAllRouteLayouts(routes, dynamicShimPaths, moduleInfo) {
|
|
75
|
+
const result = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const route of routes) for (const layout of route.layouts) {
|
|
77
|
+
const layoutId = `layout:${createAppPageTreePath(route.routeSegments, layout.treePosition)}`;
|
|
78
|
+
if (result.has(layoutId)) continue;
|
|
79
|
+
if (layout.segmentConfig) {
|
|
80
|
+
const configResult = classifyLayoutSegmentConfig(layout.segmentConfig.code);
|
|
81
|
+
if (configResult.kind !== "absent") {
|
|
82
|
+
result.set(layoutId, configResult);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const graphResult = classifyLayoutByModuleGraph(layout.moduleId, dynamicShimPaths, moduleInfo);
|
|
87
|
+
const reason = moduleGraphReason(graphResult);
|
|
88
|
+
result.set(layoutId, {
|
|
89
|
+
kind: graphResult.result,
|
|
90
|
+
reason
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { classifyAllRouteLayouts, classifyLayoutByModuleGraph, isStaticModuleGraphResult, moduleGraphReason };
|
|
97
|
+
|
|
98
|
+
//# sourceMappingURL=layout-classification.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-classification.js","names":[],"sources":["../../src/build/layout-classification.ts"],"sourcesContent":["/**\n * Layout classification — determines whether each layout in an App Router\n * route tree is static or dynamic via two complementary detection layers:\n *\n * Layer 1: Segment config (`export const dynamic`, `export const revalidate`)\n * Layer 2: Module graph traversal (checks for transitive dynamic shim imports)\n *\n * Layer 3 (probe-based runtime detection) is handled separately in\n * `app-page-execution.ts` at request time.\n *\n * Every result is carried as a `LayoutBuildClassification` tagged variant so\n * operators can trace which layer produced a decision via the structured\n * `ClassificationReason` sidecar without that metadata leaking onto the wire.\n */\n\nimport { classifyLayoutSegmentConfig } from \"./report.js\";\nimport { createAppPageTreePath } from \"../server/app-page-route-wiring.js\";\nimport type {\n ClassificationReason,\n LayoutBuildClassification,\n ModuleGraphStaticReason,\n} from \"./layout-classification-types.js\";\n\nexport type {\n ClassificationReason,\n LayoutBuildClassification,\n ModuleGraphStaticReason,\n} from \"./layout-classification-types.js\";\n\nexport type ModuleGraphClassification = \"static\" | \"needs-probe\";\n\nexport type ModuleGraphClassificationResult = {\n result: ModuleGraphClassification;\n /** First dynamic shim module ID encountered during BFS, when any. */\n firstShimMatch?: string;\n};\n\nexport type ModuleInfoProvider = {\n getModuleInfo(id: string): {\n importedIds: string[];\n dynamicImportedIds: string[];\n } | null;\n};\n\ntype LayoutEntry = {\n /** Rollup/Vite module ID for the layout file. */\n moduleId: string;\n /** Directory depth from the app root, used to build the stable layout ID. */\n treePosition: number;\n /** Segment config source code extracted at build time, or null when absent. */\n segmentConfig?: { code: string } | null;\n};\n\ntype RouteForClassification = {\n layouts: readonly LayoutEntry[];\n routeSegments: string[];\n};\n\n/**\n * BFS traversal of a layout's dependency tree. If any transitive import\n * resolves to a dynamic shim path (headers, cache, server), the layout\n * cannot be proven static at build time and needs a runtime probe.\n *\n * The returned object carries the classification plus the first matching\n * shim module ID (when any). Operators use the shim ID via the debug\n * channel to trace why a layout was flagged for probing.\n */\nexport function classifyLayoutByModuleGraph(\n layoutModuleId: string,\n dynamicShimPaths: ReadonlySet<string>,\n moduleInfo: ModuleInfoProvider,\n): ModuleGraphClassificationResult {\n const visited = new Set<string>();\n const queue: string[] = [layoutModuleId];\n let head = 0;\n\n while (head < queue.length) {\n const currentId = queue[head++]!;\n\n if (visited.has(currentId)) continue;\n visited.add(currentId);\n\n if (dynamicShimPaths.has(currentId)) {\n return { result: \"needs-probe\", firstShimMatch: currentId };\n }\n\n const info = moduleInfo.getModuleInfo(currentId);\n if (!info) continue;\n\n for (const importedId of info.importedIds) {\n if (!visited.has(importedId)) queue.push(importedId);\n }\n for (const dynamicId of info.dynamicImportedIds) {\n if (!visited.has(dynamicId)) queue.push(dynamicId);\n }\n }\n\n return { result: \"static\" };\n}\n\nexport function moduleGraphReason(\n graphResult: ModuleGraphClassificationResult & { result: \"static\" },\n): ModuleGraphStaticReason;\nexport function moduleGraphReason(\n graphResult: ModuleGraphClassificationResult,\n): ClassificationReason;\nexport function moduleGraphReason(\n graphResult: ModuleGraphClassificationResult,\n): ClassificationReason {\n if (graphResult.firstShimMatch === undefined) {\n return { layer: \"module-graph\", result: graphResult.result };\n }\n return {\n layer: \"module-graph\",\n result: graphResult.result,\n firstShimMatch: graphResult.firstShimMatch,\n };\n}\n\nexport function isStaticModuleGraphResult(\n graphResult: ModuleGraphClassificationResult,\n): graphResult is ModuleGraphClassificationResult & { result: \"static\" } {\n return graphResult.result === \"static\";\n}\n\n/**\n * Classifies all layouts across all routes using a two-layer strategy:\n *\n * 1. Segment config (Layer 1) — short-circuits to \"static\" or \"dynamic\"\n * 2. Module graph (Layer 2) — BFS for dynamic shim imports → \"static\" or \"needs-probe\"\n *\n * Shared layouts (same file appearing in multiple routes) are classified once\n * and deduplicated by layout ID.\n *\n * @internal Not called by production code. The `generateBundle` hook in\n * `index.ts` calls `classifyLayoutByModuleGraph` directly and composes\n * via the numeric-index manifest in `route-classification-manifest.ts`.\n * Used only by `tests/layout-classification.test.ts`.\n */\nexport function classifyAllRouteLayouts(\n routes: readonly RouteForClassification[],\n dynamicShimPaths: ReadonlySet<string>,\n moduleInfo: ModuleInfoProvider,\n): Map<string, LayoutBuildClassification> {\n const result = new Map<string, LayoutBuildClassification>();\n\n for (const route of routes) {\n for (const layout of route.layouts) {\n const layoutId = `layout:${createAppPageTreePath(route.routeSegments, layout.treePosition)}`;\n\n if (result.has(layoutId)) continue;\n\n // Layer 1: segment config\n if (layout.segmentConfig) {\n const configResult = classifyLayoutSegmentConfig(layout.segmentConfig.code);\n if (configResult.kind !== \"absent\") {\n result.set(layoutId, configResult);\n continue;\n }\n }\n\n // Layer 2: module graph\n const graphResult = classifyLayoutByModuleGraph(\n layout.moduleId,\n dynamicShimPaths,\n moduleInfo,\n );\n const reason = moduleGraphReason(graphResult);\n result.set(layoutId, { kind: graphResult.result, reason });\n }\n }\n\n return result;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAmEA,SAAgB,4BACd,gBACA,kBACA,YACiC;CACjC,MAAM,0BAAU,IAAI,KAAa;CACjC,MAAM,QAAkB,CAAC,eAAe;CACxC,IAAI,OAAO;AAEX,QAAO,OAAO,MAAM,QAAQ;EAC1B,MAAM,YAAY,MAAM;AAExB,MAAI,QAAQ,IAAI,UAAU,CAAE;AAC5B,UAAQ,IAAI,UAAU;AAEtB,MAAI,iBAAiB,IAAI,UAAU,CACjC,QAAO;GAAE,QAAQ;GAAe,gBAAgB;GAAW;EAG7D,MAAM,OAAO,WAAW,cAAc,UAAU;AAChD,MAAI,CAAC,KAAM;AAEX,OAAK,MAAM,cAAc,KAAK,YAC5B,KAAI,CAAC,QAAQ,IAAI,WAAW,CAAE,OAAM,KAAK,WAAW;AAEtD,OAAK,MAAM,aAAa,KAAK,mBAC3B,KAAI,CAAC,QAAQ,IAAI,UAAU,CAAE,OAAM,KAAK,UAAU;;AAItD,QAAO,EAAE,QAAQ,UAAU;;AAS7B,SAAgB,kBACd,aACsB;AACtB,KAAI,YAAY,mBAAmB,KAAA,EACjC,QAAO;EAAE,OAAO;EAAgB,QAAQ,YAAY;EAAQ;AAE9D,QAAO;EACL,OAAO;EACP,QAAQ,YAAY;EACpB,gBAAgB,YAAY;EAC7B;;AAGH,SAAgB,0BACd,aACuE;AACvE,QAAO,YAAY,WAAW;;;;;;;;;;;;;;;;AAiBhC,SAAgB,wBACd,QACA,kBACA,YACwC;CACxC,MAAM,yBAAS,IAAI,KAAwC;AAE3D,MAAK,MAAM,SAAS,OAClB,MAAK,MAAM,UAAU,MAAM,SAAS;EAClC,MAAM,WAAW,UAAU,sBAAsB,MAAM,eAAe,OAAO,aAAa;AAE1F,MAAI,OAAO,IAAI,SAAS,CAAE;AAG1B,MAAI,OAAO,eAAe;GACxB,MAAM,eAAe,4BAA4B,OAAO,cAAc,KAAK;AAC3E,OAAI,aAAa,SAAS,UAAU;AAClC,WAAO,IAAI,UAAU,aAAa;AAClC;;;EAKJ,MAAM,cAAc,4BAClB,OAAO,UACP,kBACA,WACD;EACD,MAAM,SAAS,kBAAkB,YAAY;AAC7C,SAAO,IAAI,UAAU;GAAE,MAAM,YAAY;GAAQ;GAAQ,CAAC;;AAI9D,QAAO"}
|
package/dist/build/report.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { LayoutBuildClassification } from "./layout-classification-types.js";
|
|
1
2
|
import { Route } from "../routing/pages-router.js";
|
|
2
3
|
import { AppRoute } from "../routing/app-router.js";
|
|
3
4
|
import { PrerenderResult } from "./prerender.js";
|
|
@@ -49,6 +50,19 @@ declare function extractExportConstNumber(code: string, name: string): number |
|
|
|
49
50
|
* null — no `revalidate` key found (fully static)
|
|
50
51
|
*/
|
|
51
52
|
declare function extractGetStaticPropsRevalidate(code: string): number | false | null;
|
|
53
|
+
/**
|
|
54
|
+
* Classifies a layout file by its segment config exports (`dynamic`, `revalidate`).
|
|
55
|
+
*
|
|
56
|
+
* Returns a tagged `LayoutBuildClassification` carrying both the decision and
|
|
57
|
+
* the specific segment-config field that produced it. `{ kind: "absent" }`
|
|
58
|
+
* means no segment config is present and the caller should defer to the next
|
|
59
|
+
* layer (module graph analysis).
|
|
60
|
+
*
|
|
61
|
+
* Unlike page classification, positive `revalidate` values are not meaningful
|
|
62
|
+
* for layout skip decisions — ISR is a page-level concept. Only the extremes
|
|
63
|
+
* (`revalidate = 0` → dynamic, `revalidate = Infinity` → static) are decisive.
|
|
64
|
+
*/
|
|
65
|
+
declare function classifyLayoutSegmentConfig(code: string): LayoutBuildClassification;
|
|
52
66
|
/**
|
|
53
67
|
* Classifies a Pages Router page file by reading its source and examining
|
|
54
68
|
* which data-fetching exports it contains.
|
|
@@ -111,5 +125,5 @@ declare function printBuildReport(options: {
|
|
|
111
125
|
prerenderResult?: PrerenderResult;
|
|
112
126
|
}): Promise<void>;
|
|
113
127
|
//#endregion
|
|
114
|
-
export { RouteRow, RouteType, buildReportRows, classifyAppRoute, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, findDir, formatBuildReport, hasNamedExport, printBuildReport };
|
|
128
|
+
export { RouteRow, RouteType, buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, findDir, formatBuildReport, hasNamedExport, printBuildReport };
|
|
115
129
|
//# sourceMappingURL=report.d.ts.map
|
package/dist/build/report.js
CHANGED
|
@@ -408,6 +408,55 @@ function findMatchingToken(code, start, openToken, closeToken) {
|
|
|
408
408
|
return -1;
|
|
409
409
|
}
|
|
410
410
|
/**
|
|
411
|
+
* Classifies a layout file by its segment config exports (`dynamic`, `revalidate`).
|
|
412
|
+
*
|
|
413
|
+
* Returns a tagged `LayoutBuildClassification` carrying both the decision and
|
|
414
|
+
* the specific segment-config field that produced it. `{ kind: "absent" }`
|
|
415
|
+
* means no segment config is present and the caller should defer to the next
|
|
416
|
+
* layer (module graph analysis).
|
|
417
|
+
*
|
|
418
|
+
* Unlike page classification, positive `revalidate` values are not meaningful
|
|
419
|
+
* for layout skip decisions — ISR is a page-level concept. Only the extremes
|
|
420
|
+
* (`revalidate = 0` → dynamic, `revalidate = Infinity` → static) are decisive.
|
|
421
|
+
*/
|
|
422
|
+
function classifyLayoutSegmentConfig(code) {
|
|
423
|
+
const dynamicValue = extractExportConstString(code, "dynamic");
|
|
424
|
+
if (dynamicValue === "force-dynamic") return {
|
|
425
|
+
kind: "dynamic",
|
|
426
|
+
reason: {
|
|
427
|
+
layer: "segment-config",
|
|
428
|
+
key: "dynamic",
|
|
429
|
+
value: "force-dynamic"
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
if (dynamicValue === "force-static" || dynamicValue === "error") return {
|
|
433
|
+
kind: "static",
|
|
434
|
+
reason: {
|
|
435
|
+
layer: "segment-config",
|
|
436
|
+
key: "dynamic",
|
|
437
|
+
value: dynamicValue
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
const revalidateValue = extractExportConstNumber(code, "revalidate");
|
|
441
|
+
if (revalidateValue === Infinity) return {
|
|
442
|
+
kind: "static",
|
|
443
|
+
reason: {
|
|
444
|
+
layer: "segment-config",
|
|
445
|
+
key: "revalidate",
|
|
446
|
+
value: Infinity
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
if (revalidateValue === 0) return {
|
|
450
|
+
kind: "dynamic",
|
|
451
|
+
reason: {
|
|
452
|
+
layer: "segment-config",
|
|
453
|
+
key: "revalidate",
|
|
454
|
+
value: 0
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
return { kind: "absent" };
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
411
460
|
* Classifies a Pages Router page file by reading its source and examining
|
|
412
461
|
* which data-fetching exports it contains.
|
|
413
462
|
*
|
|
@@ -603,6 +652,6 @@ async function printBuildReport(options) {
|
|
|
603
652
|
}
|
|
604
653
|
}
|
|
605
654
|
//#endregion
|
|
606
|
-
export { buildReportRows, classifyAppRoute, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, findDir, formatBuildReport, hasNamedExport, printBuildReport };
|
|
655
|
+
export { buildReportRows, classifyAppRoute, classifyLayoutSegmentConfig, classifyPagesRoute, extractExportConstNumber, extractExportConstString, extractGetStaticPropsRevalidate, findDir, formatBuildReport, hasNamedExport, printBuildReport };
|
|
607
656
|
|
|
608
657
|
//# sourceMappingURL=report.js.map
|