@timber-js/app 0.2.0-alpha.182 → 0.2.0-alpha.184
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/dist/_chunks/{build-output-helper-DF5uScqS.js → build-output-helper-DOGYFb_X.js} +47 -9
- package/dist/_chunks/build-output-helper-DOGYFb_X.js.map +1 -0
- package/dist/_chunks/{cloudflare-AHoWYTYr.js → cloudflare-CnT5Lr7U.js} +21 -3
- package/dist/_chunks/{cloudflare-AHoWYTYr.js.map → cloudflare-CnT5Lr7U.js.map} +1 -1
- package/dist/_chunks/plugin-context---kTF5v8.js.map +1 -1
- package/dist/adapters/build-output-helper.d.ts +6 -0
- package/dist/adapters/build-output-helper.d.ts.map +1 -1
- package/dist/adapters/cloudflare-dev.js +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +1 -1
- package/dist/adapters/cloudflare.d.ts +10 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +2 -2
- package/dist/adapters/nitro.d.ts +14 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -1
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/adapters/shared.d.ts +1 -1
- package/dist/adapters/shared.d.ts.map +1 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.d.ts.map +1 -1
- package/dist/client/internal.js +113 -11
- package/dist/client/internal.js.map +1 -1
- package/dist/client/rsc-fetch.d.ts +9 -0
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/index.js +437 -200
- package/dist/index.js.map +1 -1
- package/dist/plugin-context.d.ts +12 -0
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/adapter-build.d.ts.map +1 -1
- package/dist/plugins/build-report.d.ts +4 -3
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/static-build.d.ts +55 -2
- package/dist/plugins/static-build.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts +12 -0
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/sitemap-generator.d.ts +1 -1
- package/dist/server/sitemap-generator.d.ts.map +1 -1
- package/dist/server/static-generator.d.ts +106 -0
- package/dist/server/static-generator.d.ts.map +1 -0
- package/docs/learn/13-configuration.mdx +5 -1
- package/package.json +1 -1
- package/src/adapters/build-output-helper.ts +40 -1
- package/src/adapters/cloudflare.ts +43 -1
- package/src/adapters/nitro.ts +58 -2
- package/src/adapters/shared.ts +42 -11
- package/src/adapters/types.ts +14 -0
- package/src/client/browser-entry/index.ts +7 -1
- package/src/client/rsc-fetch.ts +187 -19
- package/src/index.ts +1 -1
- package/src/plugin-context.ts +12 -0
- package/src/plugins/adapter-build.ts +5 -0
- package/src/plugins/build-report.ts +6 -12
- package/src/plugins/static-build.ts +391 -2
- package/src/server/rsc-entry/index.ts +24 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/static-generator.ts +692 -0
- package/dist/_chunks/build-output-helper-DF5uScqS.js.map +0 -1
package/src/adapters/types.ts
CHANGED
|
@@ -19,6 +19,20 @@ export interface TimberConfig {
|
|
|
19
19
|
* Undefined when no build manifest was produced (e.g., dev mode or no client assets).
|
|
20
20
|
*/
|
|
21
21
|
manifestInit?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Absolute path to the directory containing pre-rendered static HTML,
|
|
24
|
+
* RSC flight files, and API route outputs. Present only when
|
|
25
|
+
* output: 'static' and the static generation pass completed.
|
|
26
|
+
* Layout: path/index.html, _rsc/path.rsc, api-route-path
|
|
27
|
+
*/
|
|
28
|
+
staticPagesDir?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Content-Type mappings for extensionless API route outputs.
|
|
31
|
+
* Map from URL path (e.g. "/api/data") to Content-Type value.
|
|
32
|
+
* Written during static generation, consumed by adapters when
|
|
33
|
+
* writing _headers. See TIM-1241.
|
|
34
|
+
*/
|
|
35
|
+
staticContentTypes?: Record<string, string>;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
/**
|
|
@@ -40,7 +40,7 @@ import schemaCodecs from 'virtual:timber-schema';
|
|
|
40
40
|
import { setLinkCodecs } from '../../params/codec-registry.js';
|
|
41
41
|
setLinkCodecs(schemaCodecs);
|
|
42
42
|
|
|
43
|
-
import { setClientDeploymentId } from '../rsc-fetch.js';
|
|
43
|
+
import { setClientDeploymentId, setStaticMode } from '../rsc-fetch.js';
|
|
44
44
|
|
|
45
45
|
import { setupServerActions } from './action-dispatch.js';
|
|
46
46
|
import { createRscPayloadStream } from './rsc-stream.js';
|
|
@@ -63,6 +63,12 @@ function bootstrap(runtimeConfig: typeof config): void {
|
|
|
63
63
|
setClientDeploymentId(deploymentId);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
// Static mode: fetch _rsc/*.rsc files instead of route URLs with
|
|
67
|
+
// Accept headers. Static hosts ignore Accept headers (TIM-1243).
|
|
68
|
+
if ((runtimeConfig as Record<string, unknown>).output === 'static') {
|
|
69
|
+
setStaticMode(true);
|
|
70
|
+
}
|
|
71
|
+
|
|
66
72
|
// Step 2: Decode inlined RSC payload (may be null for JS-only clients)
|
|
67
73
|
const rscResult = createRscPayloadStream();
|
|
68
74
|
|
package/src/client/rsc-fetch.ts
CHANGED
|
@@ -88,6 +88,108 @@ export function getClientDeploymentId(): string | null {
|
|
|
88
88
|
return clientDeploymentId;
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
// ─── Static Mode ────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* When true, RSC fetches use _rsc/*.rsc file URLs instead of
|
|
95
|
+
* the route URL with Accept headers. Static hosts ignore Accept
|
|
96
|
+
* headers, so the client must fetch the pre-generated .rsc files
|
|
97
|
+
* directly. Set at bootstrap from virtual:timber-config output mode.
|
|
98
|
+
*/
|
|
99
|
+
let staticMode = false;
|
|
100
|
+
|
|
101
|
+
export function setStaticMode(enabled: boolean): void {
|
|
102
|
+
staticMode = enabled;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isStaticMode(): boolean {
|
|
106
|
+
return staticMode;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* RSC manifest mapping unhashed → hashed URLs. Populated from
|
|
111
|
+
* `window.__TIMBER_RSC_MANIFEST__` (injected into HTML during
|
|
112
|
+
* static generation). See TIM-1254.
|
|
113
|
+
*/
|
|
114
|
+
let rscManifest: Record<string, string> | null = null;
|
|
115
|
+
|
|
116
|
+
function getRscManifest(): Record<string, string> | null {
|
|
117
|
+
if (rscManifest) return rscManifest;
|
|
118
|
+
if (typeof window !== 'undefined' && (window as any).__TIMBER_RSC_MANIFEST__) {
|
|
119
|
+
rscManifest = (window as any).__TIMBER_RSC_MANIFEST__;
|
|
120
|
+
}
|
|
121
|
+
return rscManifest;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @internal Exported for testing. */
|
|
125
|
+
export function setRscManifest(manifest: Record<string, string> | null): void {
|
|
126
|
+
rscManifest = manifest;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Convert a route URL to the corresponding _rsc/*.rsc file path.
|
|
131
|
+
* Mirrors the naming in plugins/static-build.ts staticOutputPath.
|
|
132
|
+
*
|
|
133
|
+
* When an RSC manifest is available (hashed filenames from TIM-1254),
|
|
134
|
+
* the manifest is consulted to resolve to the hashed path.
|
|
135
|
+
*
|
|
136
|
+
* / → /_rsc/index.rsc (or /_rsc/index-B7YxEKdN.rsc with manifest)
|
|
137
|
+
* /about → /_rsc/about.rsc (or /_rsc/about-C8ZzFLfO.rsc with manifest)
|
|
138
|
+
* /blog/hello → /_rsc/blog/hello.rsc
|
|
139
|
+
*/
|
|
140
|
+
function toStaticRscUrl(url: string): string {
|
|
141
|
+
const unhashed = toUnhashedRscUrl(url);
|
|
142
|
+
return manifestLookup(unhashed) ?? unhashed;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Compute the unhashed _rsc/*.rsc URL for a route path.
|
|
147
|
+
* @internal Exported for testing.
|
|
148
|
+
*/
|
|
149
|
+
export function toUnhashedRscUrl(url: string): string {
|
|
150
|
+
const hashIndex = url.indexOf('#');
|
|
151
|
+
const queryIndex = url.indexOf('?');
|
|
152
|
+
const hashEnd = hashIndex === -1 ? url.length : hashIndex;
|
|
153
|
+
const queryEnd = queryIndex === -1 ? url.length : queryIndex;
|
|
154
|
+
const end = Math.min(hashEnd, queryEnd);
|
|
155
|
+
let pathname = url.slice(0, end);
|
|
156
|
+
// Strip trailing slash (unless root) to match static build output naming
|
|
157
|
+
if (pathname.length > 1 && pathname.endsWith('/')) {
|
|
158
|
+
pathname = pathname.slice(0, -1);
|
|
159
|
+
}
|
|
160
|
+
const rscPath = pathname === '/' ? '/index' : pathname;
|
|
161
|
+
return `/_rsc${rscPath}.rsc`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Convert a route URL to the corresponding _rsc/*.params.json sidecar path.
|
|
166
|
+
* Used in static mode to fetch route params that are normally carried
|
|
167
|
+
* in the X-Timber-Params response header. See TIM-1246.
|
|
168
|
+
*
|
|
169
|
+
* Consults the RSC manifest for hashed filenames (TIM-1254).
|
|
170
|
+
*/
|
|
171
|
+
/**
|
|
172
|
+
* Look up a key in the RSC manifest, falling back to percent-decoded
|
|
173
|
+
* lookup for encoded browser URLs. Returns null if not found.
|
|
174
|
+
*/
|
|
175
|
+
function manifestLookup(key: string): string | null {
|
|
176
|
+
const manifest = getRscManifest();
|
|
177
|
+
if (!manifest) return null;
|
|
178
|
+
if (manifest[key]) return manifest[key];
|
|
179
|
+
try {
|
|
180
|
+
const decoded = decodeURIComponent(key);
|
|
181
|
+
if (decoded !== key && manifest[decoded]) return manifest[decoded];
|
|
182
|
+
} catch {
|
|
183
|
+
// Malformed percent sequence (e.g. /100%.rsc) — skip decoded lookup
|
|
184
|
+
}
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function toStaticParamsUrl(url: string): string {
|
|
189
|
+
const unhashed = toUnhashedRscUrl(url).replace(/\.rsc$/, '.params.json');
|
|
190
|
+
return manifestLookup(unhashed) ?? unhashed;
|
|
191
|
+
}
|
|
192
|
+
|
|
91
193
|
// ─── Reload Signal ───────────────────────────────────────────────
|
|
92
194
|
|
|
93
195
|
/** Header name used by the server to signal a version skew reload. */
|
|
@@ -326,8 +428,16 @@ export async function fetchRscPayload(
|
|
|
326
428
|
currentUrl?: string,
|
|
327
429
|
signal?: AbortSignal
|
|
328
430
|
): Promise<FetchResult> {
|
|
329
|
-
|
|
330
|
-
|
|
431
|
+
// In static mode, fetch the pre-generated _rsc/*.rsc file directly
|
|
432
|
+
// instead of the route URL with Accept headers. Static hosts ignore
|
|
433
|
+
// Accept headers, so the route URL would return HTML.
|
|
434
|
+
const fetchTarget = staticMode ? toStaticRscUrl(url) : url;
|
|
435
|
+
// Skip the cache-bust param when the manifest supplies a content-hashed
|
|
436
|
+
// URL — the hash itself guarantees freshness, and the param would defeat
|
|
437
|
+
// the immutable cache headers on /_rsc/* (TIM-1254).
|
|
438
|
+
const isHashedFromManifest = staticMode && fetchTarget !== toUnhashedRscUrl(url);
|
|
439
|
+
const rscUrl = isHashedFromManifest ? fetchTarget : appendRscParam(fetchTarget);
|
|
440
|
+
const headers = buildRscHeaders(staticMode ? undefined : stateTree, currentUrl);
|
|
331
441
|
if (deps.decodeRsc) {
|
|
332
442
|
// Production path: use createFromFetch for streaming RSC decoding.
|
|
333
443
|
// createFromFetch takes a Promise<Response> and progressively parses
|
|
@@ -336,6 +446,21 @@ export async function fetchRscPayload(
|
|
|
336
446
|
// Intercept the response to read segment metadata before createFromFetch
|
|
337
447
|
// consumes the body. Reading headers does NOT consume the body stream.
|
|
338
448
|
const fetchPromise = deps.fetch(rscUrl, { headers, redirect: 'manual', signal });
|
|
449
|
+
// In static mode, fetch the params sidecar in parallel. The .rsc
|
|
450
|
+
// file has no HTTP headers, so params come from a .params.json
|
|
451
|
+
// sidecar written at build time. See TIM-1246.
|
|
452
|
+
const staticParamsFetch = staticMode
|
|
453
|
+
? deps
|
|
454
|
+
.fetch(toStaticParamsUrl(url), { signal })
|
|
455
|
+
.then((r) => (r.ok ? (r.json() as Promise<Record<string, string | string[]>>) : null))
|
|
456
|
+
.catch((e) => {
|
|
457
|
+
if (e instanceof DOMException && e.name === 'AbortError') throw e;
|
|
458
|
+
return null;
|
|
459
|
+
})
|
|
460
|
+
: null;
|
|
461
|
+
// Observe the sidecar promise so its rejection doesn't become
|
|
462
|
+
// unhandled if wrappedPromise rejects first (TIM-1248).
|
|
463
|
+
staticParamsFetch?.catch(() => {});
|
|
339
464
|
let segmentInfo: SegmentInfo[] | null = null;
|
|
340
465
|
let params: Record<string, string | string[]> | null = null;
|
|
341
466
|
let skippedSegments: string[] | null = null;
|
|
@@ -368,14 +493,27 @@ export async function fetchRscPayload(
|
|
|
368
493
|
if (response.headers.get('X-Timber-Error') === '1') {
|
|
369
494
|
throw new ServerErrorResponse(response.status, url);
|
|
370
495
|
}
|
|
371
|
-
// Content-Type guard: reject non-RSC responses
|
|
372
|
-
//
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
496
|
+
// Content-Type guard: reject non-RSC responses before createFromFetch
|
|
497
|
+
// tries to parse the body as Flight data.
|
|
498
|
+
// In static mode, accept octet-stream/text/plain/absent content-type
|
|
499
|
+
// (static hosts serve .rsc files with these), but still reject 404s
|
|
500
|
+
// and text/html (missing .rsc file → host returns 404 page or SPA
|
|
501
|
+
// HTML fallback). See TIM-1231, TIM-1243, TIM-1247.
|
|
502
|
+
if (staticMode) {
|
|
503
|
+
const contentType = response.headers.get('content-type');
|
|
504
|
+
if (
|
|
505
|
+
!response.ok ||
|
|
506
|
+
(contentType && contentType.split(';')[0].trim().toLowerCase() === 'text/html')
|
|
507
|
+
) {
|
|
508
|
+
response.body?.cancel();
|
|
509
|
+
throw new NonRscResponse(url);
|
|
510
|
+
}
|
|
511
|
+
} else {
|
|
512
|
+
const contentType = response.headers.get('content-type');
|
|
513
|
+
if (!contentType || !contentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)) {
|
|
514
|
+
response.body?.cancel();
|
|
515
|
+
throw new NonRscResponse(url);
|
|
516
|
+
}
|
|
379
517
|
}
|
|
380
518
|
// Metadata (<title>/<meta>/<link>) now rides the RSC Flight payload
|
|
381
519
|
// as React elements — React 19 Float handles them. See TIM-1151.
|
|
@@ -400,6 +538,11 @@ export async function fetchRscPayload(
|
|
|
400
538
|
});
|
|
401
539
|
// Await headers so segmentInfo/params are populated.
|
|
402
540
|
await wrappedPromise;
|
|
541
|
+
// In static mode, params come from the sidecar (header extraction
|
|
542
|
+
// returns null because .rsc files have no HTTP headers). TIM-1246.
|
|
543
|
+
if (staticParamsFetch && !params) {
|
|
544
|
+
params = await staticParamsFetch;
|
|
545
|
+
}
|
|
403
546
|
// Start decoding but do NOT await — return the in-progress thenable.
|
|
404
547
|
// React can render a Flight thenable directly: it suspends on unresolved
|
|
405
548
|
// parts and progressively renders as chunks arrive, spreading work across
|
|
@@ -436,20 +579,45 @@ export async function fetchRscPayload(
|
|
|
436
579
|
if (response.headers.get('X-Timber-Error') === '1') {
|
|
437
580
|
throw new ServerErrorResponse(response.status, url);
|
|
438
581
|
}
|
|
439
|
-
// Content-Type guard (same as production path above). See TIM-1231.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
582
|
+
// Content-Type guard (same as production path above). See TIM-1231, TIM-1243, TIM-1247.
|
|
583
|
+
if (staticMode) {
|
|
584
|
+
const fallbackContentType = response.headers.get('content-type');
|
|
585
|
+
if (
|
|
586
|
+
!response.ok ||
|
|
587
|
+
(fallbackContentType &&
|
|
588
|
+
fallbackContentType.split(';')[0].trim().toLowerCase() === 'text/html')
|
|
589
|
+
) {
|
|
590
|
+
response.body?.cancel();
|
|
591
|
+
throw new NonRscResponse(url);
|
|
592
|
+
}
|
|
593
|
+
} else {
|
|
594
|
+
const fallbackContentType = response.headers.get('content-type');
|
|
595
|
+
if (
|
|
596
|
+
!fallbackContentType ||
|
|
597
|
+
!fallbackContentType.split(';')[0].trim().includes(RSC_CONTENT_TYPE)
|
|
598
|
+
) {
|
|
599
|
+
response.body?.cancel();
|
|
600
|
+
throw new NonRscResponse(url);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
let fallbackParams = extractParams(response);
|
|
604
|
+
// In static mode, params come from the sidecar. TIM-1246.
|
|
605
|
+
if (staticMode && !fallbackParams) {
|
|
606
|
+
try {
|
|
607
|
+
const paramsResponse = await deps.fetch(toStaticParamsUrl(url), { signal });
|
|
608
|
+
if (paramsResponse.ok) {
|
|
609
|
+
fallbackParams = (await paramsResponse.json()) as Record<string, string | string[]>;
|
|
610
|
+
}
|
|
611
|
+
} catch (e) {
|
|
612
|
+
if (e instanceof DOMException && e.name === 'AbortError') throw e;
|
|
613
|
+
// No params sidecar — non-dynamic route, params stay null
|
|
614
|
+
}
|
|
447
615
|
}
|
|
448
616
|
return {
|
|
449
617
|
payload: await response.text(),
|
|
450
618
|
decodePromise: null,
|
|
451
619
|
segmentInfo: extractSegmentInfo(response),
|
|
452
|
-
params:
|
|
620
|
+
params: fallbackParams,
|
|
453
621
|
skippedSegments: extractSkippedSegments(response),
|
|
454
622
|
};
|
|
455
623
|
}
|
package/src/index.ts
CHANGED
|
@@ -557,13 +557,13 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
557
557
|
timberRouting(ctx),
|
|
558
558
|
timberEntries(ctx),
|
|
559
559
|
timberBuildManifest(ctx),
|
|
560
|
-
timberStaticBuild(ctx),
|
|
561
560
|
timberFonts(ctx),
|
|
562
561
|
timberMdx(ctx),
|
|
563
562
|
timberRequestDep(ctx),
|
|
564
563
|
timberCacheTransform(ctx),
|
|
565
564
|
timberPrerenderSugar(ctx), // export const prerender → cache.component() desugaring
|
|
566
565
|
timberPrebuilt(ctx), // cache.component() → __prebuilt() callsite rewrite
|
|
566
|
+
timberStaticBuild(ctx), // Static mode: validate + post-build HTML generation
|
|
567
567
|
timberContent(ctx),
|
|
568
568
|
timberServerBundle(), // Bundle all deps in server environments for prod
|
|
569
569
|
timberCloudflareWasm(ctx), // Externalize WASM for Cloudflare Workers
|
package/src/plugin-context.ts
CHANGED
|
@@ -160,6 +160,18 @@ export interface PluginContext {
|
|
|
160
160
|
* ranges and rewrite CODE via MagicString; the AST itself is never mutated.
|
|
161
161
|
*/
|
|
162
162
|
parseCached?: ParseMemo;
|
|
163
|
+
/**
|
|
164
|
+
* Content-Type mappings for extensionless static API route outputs.
|
|
165
|
+
* Populated during static generation, consumed by adapter-build to
|
|
166
|
+
* write per-path Content-Type rules in _headers. See TIM-1241.
|
|
167
|
+
*/
|
|
168
|
+
staticContentTypes?: Record<string, string>;
|
|
169
|
+
/**
|
|
170
|
+
* RSC flight payload manifest mapping unhashed → hashed URLs.
|
|
171
|
+
* Populated during static generation, consumed by adapters to set
|
|
172
|
+
* immutable cache headers for `_rsc/` files. See TIM-1254.
|
|
173
|
+
*/
|
|
174
|
+
rscManifest?: Record<string, string>;
|
|
163
175
|
}
|
|
164
176
|
|
|
165
177
|
// ── AST parse memo ───────────────────────────────────────────────────────
|
|
@@ -88,6 +88,11 @@ export function timberAdapterBuild(ctx: PluginContext): Plugin {
|
|
|
88
88
|
output: ctx.config.output ?? 'server',
|
|
89
89
|
clientJavascriptDisabled: ctx.clientJavascript.disabled,
|
|
90
90
|
manifestInit,
|
|
91
|
+
staticPagesDir:
|
|
92
|
+
(ctx.config.output ?? 'server') === 'static'
|
|
93
|
+
? join(buildDir, 'static-pages')
|
|
94
|
+
: undefined,
|
|
95
|
+
staticContentTypes: ctx.staticContentTypes,
|
|
91
96
|
};
|
|
92
97
|
|
|
93
98
|
await adapter.buildOutput(adapterConfig, buildDir);
|
|
@@ -48,25 +48,19 @@ const ROUTE_TYPE_ICONS: Record<RouteType, string> = {
|
|
|
48
48
|
/**
|
|
49
49
|
* Classify a route by its segment chain and output mode.
|
|
50
50
|
*
|
|
51
|
-
* In server mode (default), all pages are dynamic (rendered per-request)
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* In server mode (default), all pages are dynamic (rendered per-request)
|
|
52
|
+
* and API routes (route.ts) are classified as function.
|
|
53
|
+
* In static mode, everything is pre-rendered at build time — all routes
|
|
54
|
+
* are static regardless of dynamic segments or route.ts presence.
|
|
54
55
|
*/
|
|
55
56
|
export function classifyRoute(
|
|
56
57
|
segments: SegmentNode[],
|
|
57
58
|
outputMode: 'server' | 'static' = 'server'
|
|
58
59
|
): RouteType {
|
|
60
|
+
if (outputMode === 'static') return 'static';
|
|
59
61
|
const leaf = segments[segments.length - 1];
|
|
60
62
|
if (leaf?.route) return 'function';
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const isDynamic = segments.some(
|
|
64
|
-
(s) =>
|
|
65
|
-
s.segmentType === 'dynamic' ||
|
|
66
|
-
s.segmentType === 'catch-all' ||
|
|
67
|
-
s.segmentType === 'optional-catch-all'
|
|
68
|
-
);
|
|
69
|
-
return isDynamic ? 'dynamic' : 'static';
|
|
63
|
+
return 'dynamic';
|
|
70
64
|
}
|
|
71
65
|
|
|
72
66
|
// ─── Size helpers ─────────────────────────────────────────────────────────
|