@timber-js/app 0.2.0-alpha.146 → 0.2.0-alpha.148
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/{actions-OIZ8siQu.js → actions-a0RczDnB.js} +8 -34
- package/dist/_chunks/{actions-OIZ8siQu.js.map → actions-a0RczDnB.js.map} +1 -1
- package/dist/_chunks/cache-api-Co-yI-gP.js +424 -0
- package/dist/_chunks/cache-api-Co-yI-gP.js.map +1 -0
- package/dist/_chunks/{metadata-routes-BjepjefT.js → canonicalize-P41GR6tY.js} +65 -2
- package/dist/_chunks/canonicalize-P41GR6tY.js.map +1 -0
- package/dist/_chunks/{cli-schema-sync-D2IpXicg.js → cli-schema-sync-DJDdyEae.js} +157 -313
- package/dist/_chunks/cli-schema-sync-DJDdyEae.js.map +1 -0
- package/dist/_chunks/cli-schema-sync-D_IhHEUW.js +2 -0
- package/dist/_chunks/{define-DMVhsipD.js → define-Bssfp6ot.js} +3 -33
- package/dist/_chunks/define-Bssfp6ot.js.map +1 -0
- package/dist/_chunks/{define-C3RvHQPK.js → define-CFmvb4Bt.js} +15 -7
- package/dist/_chunks/define-CFmvb4Bt.js.map +1 -0
- package/dist/_chunks/{href-validation-DyZIzfMJ.js → href-validation-FiA_rToW.js} +2 -1
- package/dist/_chunks/{href-validation-DyZIzfMJ.js.map → href-validation-FiA_rToW.js.map} +1 -1
- package/dist/_chunks/{tracing-M_zKubhk.js → logger-CM0clugc.js} +394 -107
- package/dist/_chunks/logger-CM0clugc.js.map +1 -0
- package/dist/_chunks/navigation-root-bUDZLw74.js.map +1 -1
- package/dist/_chunks/plugin-context-DurqFBuY.js +2 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js +268 -0
- package/dist/_chunks/plugin-context-GfDWsdd2.js.map +1 -0
- package/dist/_chunks/{resolve-schema-Dj8tQloi.js → resolve-schema-3iUvBV5T.js} +3 -30
- package/dist/_chunks/resolve-schema-3iUvBV5T.js.map +1 -0
- package/dist/_chunks/{schema-bridge-DK_89h8O.js → schema-bridge-BY3QLBL7.js} +35 -2
- package/dist/_chunks/schema-bridge-BY3QLBL7.js.map +1 -0
- package/dist/_chunks/tree-match-D2l830j2.js +102 -0
- package/dist/_chunks/tree-match-D2l830j2.js.map +1 -0
- package/dist/_chunks/use-segment-params-D1CzlgBo.js.map +1 -1
- package/dist/_chunks/{walkers-7FBPM-hX.js → walkers-DvWDj453.js} +154 -88
- package/dist/_chunks/walkers-DvWDj453.js.map +1 -0
- package/dist/adapters/cloudflare-kv-cache.d.ts.map +1 -1
- package/dist/adapters/cloudflare-kv-cache.js +36 -14
- package/dist/adapters/cloudflare-kv-cache.js.map +1 -1
- package/dist/adapters/compress-module.d.ts +5 -4
- package/dist/adapters/compress-module.d.ts.map +1 -1
- package/dist/adapters/nitro.d.ts +1 -3
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +14 -134
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/cache/cache-api.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +20 -235
- package/dist/cache/index.js.map +1 -1
- package/dist/cache/invalidation-epoch.d.ts +43 -0
- package/dist/cache/invalidation-epoch.d.ts.map +1 -0
- package/dist/cache/redis-handler.d.ts +13 -0
- package/dist/cache/redis-handler.d.ts.map +1 -1
- package/dist/cache/timber-cache.d.ts +17 -9
- package/dist/cache/timber-cache.d.ts.map +1 -1
- package/dist/cli-schema-sync.d.ts +21 -1
- package/dist/cli-schema-sync.d.ts.map +1 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +26 -31
- package/dist/cli.js.map +1 -1
- package/dist/client/browser-entry/hmr.d.ts.map +1 -1
- package/dist/client/browser-entry/hydrate.d.ts.map +1 -1
- package/dist/client/browser-entry/post-hydration.d.ts.map +1 -1
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +13 -4
- package/dist/client/error-boundary.js.map +1 -1
- package/dist/client/error-reconstituter.d.ts +8 -1
- package/dist/client/error-reconstituter.d.ts.map +1 -1
- package/dist/client/history.d.ts +10 -17
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.js +30 -33
- package/dist/client/internal.js.map +1 -1
- package/dist/client/navigation-api.d.ts +9 -2
- package/dist/client/navigation-api.d.ts.map +1 -1
- package/dist/client/navigation-context.d.ts +0 -11
- package/dist/client/navigation-context.d.ts.map +1 -1
- package/dist/client/navigation-root.d.ts +0 -5
- package/dist/client/navigation-root.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/use-search-params.d.ts +10 -5
- package/dist/client/use-search-params.d.ts.map +1 -1
- package/dist/codec.js +1 -1
- package/dist/config-validation.d.ts.map +1 -1
- package/dist/cookies/define-cookie.d.ts.map +1 -1
- package/dist/cookies/index.js +2 -4
- package/dist/cookies/index.js.map +1 -1
- package/dist/dev-tools/dev-page-shell.d.ts +1 -0
- package/dist/dev-tools/dev-page-shell.d.ts.map +1 -1
- package/dist/dev-tools/hydration-overlay.d.ts +11 -0
- package/dist/dev-tools/hydration-overlay.d.ts.map +1 -0
- package/dist/dev-tools/index.d.ts +1 -2
- package/dist/dev-tools/index.d.ts.map +1 -1
- package/dist/fonts/ast-walk.d.ts +68 -0
- package/dist/fonts/ast-walk.d.ts.map +1 -0
- package/dist/fonts/ast.d.ts +23 -11
- package/dist/fonts/ast.d.ts.map +1 -1
- package/dist/fonts/bundle.d.ts +3 -1
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/dev-middleware.d.ts +1 -1
- package/dist/fonts/dev-middleware.d.ts.map +1 -1
- package/dist/fonts/fallbacks.d.ts +0 -7
- package/dist/fonts/fallbacks.d.ts.map +1 -1
- package/dist/fonts/google.d.ts +4 -2
- package/dist/fonts/google.d.ts.map +1 -1
- package/dist/fonts/local.d.ts +3 -3
- package/dist/fonts/local.d.ts.map +1 -1
- package/dist/fonts/paths.d.ts +43 -0
- package/dist/fonts/paths.d.ts.map +1 -0
- package/dist/fonts/pipeline.d.ts +7 -21
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/transform.d.ts.map +1 -1
- package/dist/fonts/virtual-modules.d.ts +1 -18
- package/dist/fonts/virtual-modules.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +609 -406
- package/dist/index.js.map +1 -1
- package/dist/params/index.js +1 -1
- package/dist/params/resolve-schema.d.ts.map +1 -1
- package/dist/plugin-context.d.ts +28 -5
- package/dist/plugin-context.d.ts.map +1 -1
- package/dist/plugins/cache.d.ts +13 -5
- package/dist/plugins/cache.d.ts.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +1 -2
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/codegen-types.d.ts +4 -0
- package/dist/routing/codegen-types.d.ts.map +1 -1
- package/dist/routing/codegen.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts.map +1 -1
- package/dist/routing/file-cache.d.ts +8 -0
- package/dist/routing/file-cache.d.ts.map +1 -0
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +0 -5
- package/dist/routing/interception.d.ts.map +1 -1
- package/dist/routing/manifest-codegen.d.ts +32 -0
- package/dist/routing/manifest-codegen.d.ts.map +1 -0
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/schema-bridge.d.ts +15 -0
- package/dist/schema-bridge.d.ts.map +1 -1
- package/dist/search-params/define.d.ts.map +1 -1
- package/dist/search-params/index.js +7 -3
- package/dist/search-params/index.js.map +1 -1
- package/dist/search-params/wrappers.d.ts.map +1 -1
- package/dist/segment-params/define.d.ts.map +1 -1
- package/dist/segment-params/index.js +1 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/actions.d.ts +0 -14
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/build-manifest.d.ts +0 -29
- package/dist/server/build-manifest.d.ts.map +1 -1
- package/dist/server/canonicalize.d.ts +10 -0
- package/dist/server/canonicalize.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +27 -2
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +8 -0
- package/dist/server/html-injector-core.d.ts.map +1 -1
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/internal.d.ts +1 -5
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +238 -349
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +0 -11
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/render-utils.d.ts +25 -0
- package/dist/server/render-utils.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -25
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/helpers.d.ts +2 -18
- package/dist/server/rsc-entry/helpers.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/ssr-render.d.ts.map +1 -1
- package/dist/server/status-code-resolver.d.ts +6 -0
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/utils/mdx-file.d.ts +15 -0
- package/dist/server/utils/mdx-file.d.ts.map +1 -0
- package/dist/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +3 -3
- package/src/adapters/cloudflare-kv-cache.ts +56 -19
- package/src/adapters/compress-module.ts +27 -118
- package/src/adapters/nitro.ts +3 -41
- package/src/adapters/raw-imports.d.ts +5 -0
- package/src/cache/cache-api.ts +9 -1
- package/src/cache/index.ts +5 -2
- package/src/cache/invalidation-epoch.ts +93 -0
- package/src/cache/redis-handler.ts +33 -2
- package/src/cache/timber-cache.ts +109 -53
- package/src/cli-schema-sync.ts +123 -30
- package/src/cli.ts +33 -37
- package/src/client/browser-entry/hmr.ts +0 -11
- package/src/client/browser-entry/hydrate.ts +39 -5
- package/src/client/browser-entry/post-hydration.ts +6 -15
- package/src/client/browser-entry/router-init.ts +98 -3
- package/src/client/error-boundary.tsx +14 -0
- package/src/client/error-reconstituter.tsx +15 -1
- package/src/client/history.ts +10 -24
- package/src/client/navigation-api.ts +14 -9
- package/src/client/navigation-context.ts +1 -21
- package/src/client/navigation-root.tsx +0 -8
- package/src/client/router.ts +18 -8
- package/src/client/rsc-fetch.ts +7 -2
- package/src/client/use-search-params.ts +10 -5
- package/src/config-validation.ts +33 -25
- package/src/cookies/define-cookie.ts +2 -18
- package/src/dev-tools/dev-page-shell.ts +6 -2
- package/src/dev-tools/hydration-overlay.ts +158 -0
- package/src/dev-tools/index.ts +3 -16
- package/src/fonts/ast-walk.ts +235 -0
- package/src/fonts/ast.ts +91 -249
- package/src/fonts/bundle.ts +16 -9
- package/src/fonts/dev-middleware.ts +16 -5
- package/src/fonts/fallbacks.ts +0 -23
- package/src/fonts/google.ts +7 -3
- package/src/fonts/local.ts +7 -6
- package/src/fonts/paths.ts +58 -0
- package/src/fonts/pipeline.ts +27 -48
- package/src/fonts/transform.ts +27 -0
- package/src/fonts/virtual-modules.ts +1 -57
- package/src/index.ts +5 -11
- package/src/params/resolve-schema.ts +2 -34
- package/src/plugin-context.ts +95 -17
- package/src/plugins/cache.ts +64 -17
- package/src/plugins/dev-server.ts +61 -9
- package/src/plugins/fonts.ts +21 -6
- package/src/plugins/routing.ts +5 -348
- package/src/routing/codegen-types.ts +4 -0
- package/src/routing/codegen.ts +66 -8
- package/src/routing/convention-lint.ts +4 -3
- package/src/routing/file-cache.ts +23 -0
- package/src/routing/interception.ts +2 -21
- package/src/routing/manifest-codegen.ts +290 -0
- package/src/routing/scanner.ts +85 -80
- package/src/routing/schema-validation.ts +2 -2
- package/src/schema-bridge.ts +63 -2
- package/src/search-params/define.ts +34 -2
- package/src/search-params/wrappers.ts +21 -3
- package/src/segment-params/define.ts +2 -62
- package/src/server/access-gate.tsx +8 -0
- package/src/server/actions.ts +8 -31
- package/src/server/build-manifest.ts +0 -56
- package/src/server/canonicalize.ts +6 -3
- package/src/server/deny-boundary.ts +106 -13
- package/src/server/deny-renderer.ts +46 -10
- package/src/server/error-boundary-wrapper.ts +1 -14
- package/src/server/html-injector-core.ts +19 -0
- package/src/server/html-injectors.ts +14 -4
- package/src/server/internal.ts +1 -18
- package/src/server/node-stream-transforms.ts +5 -72
- package/src/server/render-utils.ts +31 -0
- package/src/server/route-element-builder.ts +77 -47
- package/src/server/route-matcher.ts +22 -6
- package/src/server/rsc-entry/error-renderer.ts +2 -12
- package/src/server/rsc-entry/helpers.ts +6 -34
- package/src/server/rsc-entry/index.ts +11 -0
- package/src/server/rsc-entry/render-route.ts +7 -9
- package/src/server/rsc-entry/rsc-payload.ts +2 -1
- package/src/server/rsc-entry/ssr-renderer.ts +2 -2
- package/src/server/ssr-render.ts +1 -11
- package/src/server/status-code-resolver.ts +3 -2
- package/src/server/tree-match.ts +15 -5
- package/src/server/utils/mdx-file.ts +22 -0
- package/src/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-D2IpXicg.js.map +0 -1
- package/dist/_chunks/cli-schema-sync-DH-M8Poy.js +0 -2
- package/dist/_chunks/define-C3RvHQPK.js.map +0 -1
- package/dist/_chunks/define-DMVhsipD.js.map +0 -1
- package/dist/_chunks/handler-store-BVDgyTh7.js +0 -78
- package/dist/_chunks/handler-store-BVDgyTh7.js.map +0 -1
- package/dist/_chunks/logger-Dt-jQaBO.js +0 -291
- package/dist/_chunks/logger-Dt-jQaBO.js.map +0 -1
- package/dist/_chunks/metadata-routes-BjepjefT.js.map +0 -1
- package/dist/_chunks/resolve-schema-Dj8tQloi.js.map +0 -1
- package/dist/_chunks/schema-bridge-DK_89h8O.js.map +0 -1
- package/dist/_chunks/tracing-M_zKubhk.js.map +0 -1
- package/dist/_chunks/walkers-7FBPM-hX.js.map +0 -1
- package/dist/_chunks/warnings-CNPXaypM.js +0 -125
- package/dist/_chunks/warnings-CNPXaypM.js.map +0 -1
- package/dist/client/link-pending-store.d.ts +0 -65
- package/dist/client/link-pending-store.d.ts.map +0 -1
- package/dist/client/nav-link-store.d.ts +0 -27
- package/dist/client/nav-link-store.d.ts.map +0 -1
- package/dist/dev-tools/warnings.d.ts +0 -109
- package/dist/dev-tools/warnings.d.ts.map +0 -1
- package/dist/server/flush.d.ts +0 -74
- package/dist/server/flush.d.ts.map +0 -1
- package/src/client/link-pending-store.ts +0 -116
- package/src/client/nav-link-store.ts +0 -37
- package/src/dev-tools/warnings.ts +0 -238
- package/src/server/flush.ts +0 -183
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { h as swallow } from "./_chunks/logger-
|
|
4
|
-
import {
|
|
5
|
-
import { a as generateRouteMap, i as collectInterceptionRewrites,
|
|
1
|
+
import { c as isDynamicMetadataExtension, l as isMetadataRouteServePath, r as canonicalize } from "./_chunks/canonicalize-P41GR6tY.js";
|
|
2
|
+
import { t as matchUrlParts } from "./_chunks/tree-match-D2l830j2.js";
|
|
3
|
+
import { h as swallow } from "./_chunks/logger-CM0clugc.js";
|
|
4
|
+
import { s as scanRoutes } from "./_chunks/cli-schema-sync-DJDdyEae.js";
|
|
5
|
+
import { a as generateRouteMap, i as validateSchemaAgainstRoutes, n as collectInterceptionRewrites, o as readFileCached, t as collectLeafRoutes } from "./_chunks/walkers-DvWDj453.js";
|
|
6
|
+
import { a as mergeFileConfig, c as resolveClientJavascript, i as loadTimberConfigFile, o as resolveAppDir, r as createPluginContext, s as resolveBuildDir, u as createNoopTimer } from "./_chunks/plugin-context-GfDWsdd2.js";
|
|
6
7
|
import { t as fnv1aHash } from "./_chunks/fast-hash-D6hIVt1Y.js";
|
|
7
8
|
import { t as formatSize } from "./_chunks/format-CfwjgPz9.js";
|
|
8
|
-
import { basename, dirname, extname, join, normalize, relative, resolve } from "node:path";
|
|
9
|
+
import { basename, dirname, extname, join, normalize, relative, resolve, win32 } from "node:path";
|
|
9
10
|
import { builtinModules, createRequire } from "node:module";
|
|
10
11
|
import react, { reactCompilerPreset } from "@vitejs/plugin-react";
|
|
11
12
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -616,7 +617,7 @@ function extractHmrOptions(config) {
|
|
|
616
617
|
const opts = {
|
|
617
618
|
protocol: hmrOpts.protocol,
|
|
618
619
|
host: hmrOpts.host,
|
|
619
|
-
port: hmrOpts.port,
|
|
620
|
+
port: hmrOpts.clientPort || hmrOpts.port,
|
|
620
621
|
path: hmrOpts.path,
|
|
621
622
|
token: config.webSocketToken
|
|
622
623
|
};
|
|
@@ -1152,29 +1153,6 @@ function validateConfig(config) {
|
|
|
1152
1153
|
value: config.sitemap.defaultPriority
|
|
1153
1154
|
});
|
|
1154
1155
|
}
|
|
1155
|
-
const knownKeys = new Set([
|
|
1156
|
-
"output",
|
|
1157
|
-
"debug",
|
|
1158
|
-
"clientJavascript",
|
|
1159
|
-
"adapter",
|
|
1160
|
-
"cacheHandler",
|
|
1161
|
-
"allowedOrigins",
|
|
1162
|
-
"csrf",
|
|
1163
|
-
"limits",
|
|
1164
|
-
"pageExtensions",
|
|
1165
|
-
"slowRequestMs",
|
|
1166
|
-
"renderTimeoutMs",
|
|
1167
|
-
"devBrowserLogs",
|
|
1168
|
-
"dev",
|
|
1169
|
-
"serverTiming",
|
|
1170
|
-
"appDir",
|
|
1171
|
-
"mdx",
|
|
1172
|
-
"actionEncryption",
|
|
1173
|
-
"reactCompiler",
|
|
1174
|
-
"sitemap",
|
|
1175
|
-
"buildDir",
|
|
1176
|
-
"topLoader"
|
|
1177
|
-
]);
|
|
1178
1156
|
for (const key of Object.keys(config)) if (!knownKeys.has(key)) errors.push({
|
|
1179
1157
|
field: key,
|
|
1180
1158
|
message: `Unknown config option: "${key}".`,
|
|
@@ -1245,8 +1223,33 @@ function formatMissingPeers(results) {
|
|
|
1245
1223
|
lines.push(` ${BOLD$1}pnpm add ${names.join(" ")}${RESET$1}`);
|
|
1246
1224
|
return lines.join("\n");
|
|
1247
1225
|
}
|
|
1248
|
-
var RED$1, BOLD$1, DIM$1, RESET$1, VIRTUAL_MODULE_NAMES, REQUIRED_PEERS;
|
|
1226
|
+
var knownKeys, RED$1, BOLD$1, DIM$1, RESET$1, VIRTUAL_MODULE_NAMES, REQUIRED_PEERS;
|
|
1249
1227
|
var init_config_validation = __esmMin((() => {
|
|
1228
|
+
knownKeys = new Set(Object.keys({
|
|
1229
|
+
output: true,
|
|
1230
|
+
debug: true,
|
|
1231
|
+
clientJavascript: true,
|
|
1232
|
+
adapter: true,
|
|
1233
|
+
cacheHandler: true,
|
|
1234
|
+
allowedOrigins: true,
|
|
1235
|
+
csrf: true,
|
|
1236
|
+
limits: true,
|
|
1237
|
+
actions: true,
|
|
1238
|
+
forms: true,
|
|
1239
|
+
pageExtensions: true,
|
|
1240
|
+
slowRequestMs: true,
|
|
1241
|
+
renderTimeoutMs: true,
|
|
1242
|
+
devBrowserLogs: true,
|
|
1243
|
+
dev: true,
|
|
1244
|
+
serverTiming: true,
|
|
1245
|
+
appDir: true,
|
|
1246
|
+
mdx: true,
|
|
1247
|
+
actionEncryption: true,
|
|
1248
|
+
reactCompiler: true,
|
|
1249
|
+
sitemap: true,
|
|
1250
|
+
buildDir: true,
|
|
1251
|
+
topLoader: true
|
|
1252
|
+
}));
|
|
1250
1253
|
RED$1 = "\x1B[31m";
|
|
1251
1254
|
BOLD$1 = "\x1B[1m";
|
|
1252
1255
|
DIM$1 = "\x1B[2m";
|
|
@@ -1433,9 +1436,8 @@ function timberDevServer(ctx) {
|
|
|
1433
1436
|
configureServer(server) {
|
|
1434
1437
|
const configPaths = CONFIG_FILE_NAMES.map((name) => join(ctx.root, name));
|
|
1435
1438
|
server.watcher.add(configPaths);
|
|
1436
|
-
setViteServer(server);
|
|
1437
1439
|
listenForClientErrors(server, ctx.root);
|
|
1438
|
-
server.middlewares.use(createTimberMiddleware(server, ctx
|
|
1440
|
+
server.middlewares.use(createTimberMiddleware(server, ctx));
|
|
1439
1441
|
if (ctx.holdingServer) {
|
|
1440
1442
|
const originalListen = server.listen.bind(server);
|
|
1441
1443
|
const holdingRef = ctx.holdingServer;
|
|
@@ -1463,7 +1465,8 @@ function timberDevServer(ctx) {
|
|
|
1463
1465
|
* For non-route requests (assets, Vite internals, HMR), the middleware
|
|
1464
1466
|
* calls next() to let Vite handle them.
|
|
1465
1467
|
*/
|
|
1466
|
-
function createTimberMiddleware(server,
|
|
1468
|
+
function createTimberMiddleware(server, ctx) {
|
|
1469
|
+
const projectRoot = ctx.root;
|
|
1467
1470
|
const hmrOptions = extractHmrOptions(server.config);
|
|
1468
1471
|
return async (req, res, next) => {
|
|
1469
1472
|
const url = req.url;
|
|
@@ -1475,7 +1478,7 @@ function createTimberMiddleware(server, projectRoot) {
|
|
|
1475
1478
|
next();
|
|
1476
1479
|
return;
|
|
1477
1480
|
}
|
|
1478
|
-
if (isAssetRequest(url) && !isMetadataRouteServePath(url)) {
|
|
1481
|
+
if (isAssetRequest(url) && !isMetadataRouteServePath(url) && !isRouteOwnedAsset(ctx.routeTree, server, url)) {
|
|
1479
1482
|
next();
|
|
1480
1483
|
return;
|
|
1481
1484
|
}
|
|
@@ -1658,6 +1661,40 @@ function isAssetRequest(url) {
|
|
|
1658
1661
|
return ASSET_EXTENSIONS.test(url);
|
|
1659
1662
|
}
|
|
1660
1663
|
/**
|
|
1664
|
+
* Check whether an asset-extension URL belongs to a scanned route rather
|
|
1665
|
+
* than a static file.
|
|
1666
|
+
*
|
|
1667
|
+
* Route handlers may live at paths ending in asset-like extensions
|
|
1668
|
+
* (app/feed.json/route.ts → /feed.json), which the asset bypass would
|
|
1669
|
+
* otherwise hand to Vite — 404ing in dev while working in production.
|
|
1670
|
+
* Uses the same canonicalization and tree walker as the runtime matcher
|
|
1671
|
+
* so dev classification agrees with what the pipeline would match.
|
|
1672
|
+
*
|
|
1673
|
+
* A file that actually exists on disk wins over the route match: the
|
|
1674
|
+
* production server serves public/ statics before the RSC handler (see
|
|
1675
|
+
* adapters/nitro.ts), and Vite serves root-relative source files (e.g.
|
|
1676
|
+
* /app/global.css) in dev. Without this, a broad catch-all route like
|
|
1677
|
+
* app/[...slug] would shadow every static asset. Joining the canonical
|
|
1678
|
+
* pathname is traversal-safe — canonicalize() resolves `..` segments and
|
|
1679
|
+
* rejects root escapes and encoded separators.
|
|
1680
|
+
*
|
|
1681
|
+
* Reads ctx.routeTree per request — the routing plugin rescans it on
|
|
1682
|
+
* file add/unlink/change, so newly created routes are picked up live.
|
|
1683
|
+
*/
|
|
1684
|
+
function isRouteOwnedAsset(routeTree, server, url) {
|
|
1685
|
+
if (!routeTree) return false;
|
|
1686
|
+
const rawPathname = url.split("?")[0];
|
|
1687
|
+
const result = canonicalize(rawPathname);
|
|
1688
|
+
if (!result.ok) return false;
|
|
1689
|
+
const parts = result.pathname === "/" ? [] : result.pathname.slice(1).split("/");
|
|
1690
|
+
if (matchUrlParts(routeTree.root, parts) === null) return false;
|
|
1691
|
+
const relativePath = result.pathname.slice(1);
|
|
1692
|
+
const { publicDir, root } = server.config;
|
|
1693
|
+
if (publicDir && existsSync(join(publicDir, relativePath))) return false;
|
|
1694
|
+
if (existsSync(join(root, relativePath))) return false;
|
|
1695
|
+
return true;
|
|
1696
|
+
}
|
|
1697
|
+
/**
|
|
1661
1698
|
* Listen for client-side errors forwarded from the browser via HMR.
|
|
1662
1699
|
*
|
|
1663
1700
|
* The browser entry catches uncaught errors and unhandled rejections,
|
|
@@ -2132,6 +2169,208 @@ async function writeSegmentTypes(routeTree, appDir, root) {
|
|
|
2132
2169
|
await Promise.all(writes);
|
|
2133
2170
|
}
|
|
2134
2171
|
//#endregion
|
|
2172
|
+
//#region src/routing/manifest-codegen.ts
|
|
2173
|
+
/**
|
|
2174
|
+
* Route manifest codegen — generates virtual module source for the route
|
|
2175
|
+
* manifest, search-params registry, and global schema.
|
|
2176
|
+
*
|
|
2177
|
+
* Extracted from plugins/routing.ts to keep it under the 500-line budget.
|
|
2178
|
+
* See design/18-build-system.md §"Virtual Modules".
|
|
2179
|
+
*
|
|
2180
|
+
* Task: TIM-1071 item 8
|
|
2181
|
+
*/
|
|
2182
|
+
var SINGLE_FILE_KEYS = [
|
|
2183
|
+
"page",
|
|
2184
|
+
"layout",
|
|
2185
|
+
"middleware",
|
|
2186
|
+
"access",
|
|
2187
|
+
"route",
|
|
2188
|
+
"error",
|
|
2189
|
+
"default",
|
|
2190
|
+
"denied",
|
|
2191
|
+
"params"
|
|
2192
|
+
];
|
|
2193
|
+
var RECORD_FILE_KEYS = [
|
|
2194
|
+
"statusFiles",
|
|
2195
|
+
"jsonStatusFiles",
|
|
2196
|
+
"legacyStatusFiles",
|
|
2197
|
+
"metadataRoutes"
|
|
2198
|
+
];
|
|
2199
|
+
var SCHEMA_EXTENSIONS = [
|
|
2200
|
+
".ts",
|
|
2201
|
+
".tsx",
|
|
2202
|
+
".js",
|
|
2203
|
+
".jsx"
|
|
2204
|
+
];
|
|
2205
|
+
/**
|
|
2206
|
+
* Generate the virtual module source for the route manifest.
|
|
2207
|
+
*
|
|
2208
|
+
* The output is a default-exported object containing the serialized route tree.
|
|
2209
|
+
* All file references use absolute paths (required for virtual modules).
|
|
2210
|
+
*/
|
|
2211
|
+
function generateManifestModule(tree, viteRoot) {
|
|
2212
|
+
const imports = [];
|
|
2213
|
+
let importIndex = 0;
|
|
2214
|
+
function addImport(file) {
|
|
2215
|
+
const varName = `_route${importIndex++}`;
|
|
2216
|
+
imports.push(`const ${varName} = () => import("${file.filePath}");`);
|
|
2217
|
+
return varName;
|
|
2218
|
+
}
|
|
2219
|
+
function serializeRouteFile(key, file, indent) {
|
|
2220
|
+
return `${indent}${key}: { load: ${addImport(file)}, filePath: ${JSON.stringify(file.filePath)} },`;
|
|
2221
|
+
}
|
|
2222
|
+
function serializeRouteFileRecord(key, record, indent) {
|
|
2223
|
+
const entries = Object.entries(record);
|
|
2224
|
+
if (entries.length === 0) return null;
|
|
2225
|
+
return `${indent}${key}: {\n${entries.map(([name, file]) => {
|
|
2226
|
+
const v = addImport(file);
|
|
2227
|
+
return `${indent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`;
|
|
2228
|
+
}).join(",\n")}\n${indent}},`;
|
|
2229
|
+
}
|
|
2230
|
+
function serializeNode(node, indent) {
|
|
2231
|
+
const nextIndent = indent + " ";
|
|
2232
|
+
const parts = [];
|
|
2233
|
+
parts.push(`${nextIndent}segmentName: ${JSON.stringify(node.segmentName)},`);
|
|
2234
|
+
parts.push(`${nextIndent}segmentType: ${JSON.stringify(node.segmentType)},`);
|
|
2235
|
+
parts.push(`${nextIndent}urlPath: ${JSON.stringify(node.urlPath)},`);
|
|
2236
|
+
if (node.paramName) parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
|
|
2237
|
+
if (node.interceptionMarker) parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
|
|
2238
|
+
if (node.interceptedSegmentName) parts.push(`${nextIndent}interceptedSegmentName: ${JSON.stringify(node.interceptedSegmentName)},`);
|
|
2239
|
+
for (const key of SINGLE_FILE_KEYS) {
|
|
2240
|
+
const file = node[key];
|
|
2241
|
+
if (file) parts.push(serializeRouteFile(key, file, nextIndent));
|
|
2242
|
+
}
|
|
2243
|
+
for (const key of RECORD_FILE_KEYS) {
|
|
2244
|
+
const record = node[key];
|
|
2245
|
+
if (record) {
|
|
2246
|
+
const line = serializeRouteFileRecord(key, record, nextIndent);
|
|
2247
|
+
if (line) parts.push(line);
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
if (node.children.length > 0) {
|
|
2251
|
+
const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
|
|
2252
|
+
parts.push(`${nextIndent}children: [\n${childNodes.join(",\n")}\n${nextIndent}],`);
|
|
2253
|
+
} else parts.push(`${nextIndent}children: [],`);
|
|
2254
|
+
const slotKeys = Object.keys(node.slots);
|
|
2255
|
+
if (slotKeys.length > 0) {
|
|
2256
|
+
const slotEntries = [];
|
|
2257
|
+
for (const slotName of slotKeys) {
|
|
2258
|
+
const slotNode = node.slots[slotName];
|
|
2259
|
+
slotEntries.push(`${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + " ")}`);
|
|
2260
|
+
}
|
|
2261
|
+
parts.push(`${nextIndent}slots: {\n${slotEntries.join(",\n")}\n${nextIndent}},`);
|
|
2262
|
+
} else parts.push(`${nextIndent}slots: {},`);
|
|
2263
|
+
return `${indent}{\n${parts.join("\n")}\n${indent}}`;
|
|
2264
|
+
}
|
|
2265
|
+
const rootSerialized = serializeNode(tree.root, " ");
|
|
2266
|
+
let proxyLine = "";
|
|
2267
|
+
if (tree.proxy) proxyLine = ` proxy: { load: ${addImport(tree.proxy)}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
|
|
2268
|
+
let globalErrorLine = "";
|
|
2269
|
+
if (tree.globalError) globalErrorLine = ` globalError: { load: ${addImport(tree.globalError)}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
2270
|
+
const rewrites = collectInterceptionRewrites(tree.root);
|
|
2271
|
+
const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({
|
|
2272
|
+
interceptedPattern: r.interceptedPattern,
|
|
2273
|
+
interceptingPrefix: r.interceptingPrefix
|
|
2274
|
+
})))},` : "";
|
|
2275
|
+
return [
|
|
2276
|
+
"// Auto-generated route manifest — do not edit.",
|
|
2277
|
+
"// Generated by timber-routing plugin.",
|
|
2278
|
+
"",
|
|
2279
|
+
...imports,
|
|
2280
|
+
"",
|
|
2281
|
+
"const manifest = {",
|
|
2282
|
+
` viteRoot: ${JSON.stringify(viteRoot)},`,
|
|
2283
|
+
proxyLine,
|
|
2284
|
+
globalErrorLine,
|
|
2285
|
+
rewritesLine,
|
|
2286
|
+
` root: ${rootSerialized},`,
|
|
2287
|
+
"};",
|
|
2288
|
+
"",
|
|
2289
|
+
"export default manifest;"
|
|
2290
|
+
].filter((line) => line !== void 0 && line !== "").join("\n");
|
|
2291
|
+
}
|
|
2292
|
+
/**
|
|
2293
|
+
* Walk the route tree and collect every page that exports a
|
|
2294
|
+
* `searchParams` definition, then emit a module that statically imports
|
|
2295
|
+
* each export and registers it into the shared search-params registry.
|
|
2296
|
+
*/
|
|
2297
|
+
function generateSearchParamsRegistryModule(tree) {
|
|
2298
|
+
const entries = [];
|
|
2299
|
+
function walk(node) {
|
|
2300
|
+
if (node.page) {
|
|
2301
|
+
if (node.params && fileHasSearchParamsExport(node.params.filePath)) entries.push({
|
|
2302
|
+
urlPath: node.urlPath,
|
|
2303
|
+
filePath: node.params.filePath
|
|
2304
|
+
});
|
|
2305
|
+
else if (fileHasSearchParamsExport(node.page.filePath)) entries.push({
|
|
2306
|
+
urlPath: node.urlPath,
|
|
2307
|
+
filePath: node.page.filePath
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
for (const child of node.children) walk(child);
|
|
2311
|
+
for (const slot of Object.values(node.slots)) walk(slot);
|
|
2312
|
+
}
|
|
2313
|
+
walk(tree.root);
|
|
2314
|
+
entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
2315
|
+
const lines = [];
|
|
2316
|
+
lines.push("// Auto-generated search-params registry — do not edit.");
|
|
2317
|
+
lines.push("// Generated by timber-routing plugin (TIM-830).");
|
|
2318
|
+
lines.push("");
|
|
2319
|
+
lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
|
|
2320
|
+
lines.push("");
|
|
2321
|
+
entries.forEach((entry, i) => {
|
|
2322
|
+
lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
|
|
2323
|
+
});
|
|
2324
|
+
lines.push("");
|
|
2325
|
+
entries.forEach((entry, i) => {
|
|
2326
|
+
lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
|
|
2327
|
+
});
|
|
2328
|
+
lines.push("");
|
|
2329
|
+
lines.push("export {};");
|
|
2330
|
+
lines.push("");
|
|
2331
|
+
return lines.join("\n");
|
|
2332
|
+
}
|
|
2333
|
+
function fileHasSearchParamsExport(filePath) {
|
|
2334
|
+
try {
|
|
2335
|
+
if (!existsSync(filePath)) return false;
|
|
2336
|
+
const content = readFileCached(filePath);
|
|
2337
|
+
return /export\s+(const|let|var)\s+searchParams\b/.test(content) || /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content);
|
|
2338
|
+
} catch {
|
|
2339
|
+
return false;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Generate the virtual:timber-schema module.
|
|
2344
|
+
*
|
|
2345
|
+
* When app/schema.{ts,tsx,js,jsx} exists, generates a module that
|
|
2346
|
+
* imports the schema and resolves its segmentParams codecs into a
|
|
2347
|
+
* bare-name codec map for runtime coercion.
|
|
2348
|
+
*/
|
|
2349
|
+
function generateSchemaModule(appDir) {
|
|
2350
|
+
let schemaPath = null;
|
|
2351
|
+
for (const ext of SCHEMA_EXTENSIONS) {
|
|
2352
|
+
const candidate = join(appDir, `schema${ext}`);
|
|
2353
|
+
if (existsSync(candidate)) {
|
|
2354
|
+
schemaPath = candidate;
|
|
2355
|
+
break;
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
if (!schemaPath) return ["// Auto-generated schema loader — no app/schema.ts found.", "export default null;"].join("\n");
|
|
2359
|
+
return [
|
|
2360
|
+
"// Auto-generated schema loader — do not edit.",
|
|
2361
|
+
"// Generated by timber-routing plugin (TIM-931).",
|
|
2362
|
+
"",
|
|
2363
|
+
`import schema from ${JSON.stringify(schemaPath)};`,
|
|
2364
|
+
`import { resolveSchemaCodecs } from '@timber-js/app/params';`,
|
|
2365
|
+
"",
|
|
2366
|
+
"const globalCodecs = schema?.segmentParams",
|
|
2367
|
+
" ? resolveSchemaCodecs(schema.segmentParams)",
|
|
2368
|
+
" : null;",
|
|
2369
|
+
"",
|
|
2370
|
+
"export default globalCodecs;"
|
|
2371
|
+
].join("\n");
|
|
2372
|
+
}
|
|
2373
|
+
//#endregion
|
|
2135
2374
|
//#region src/routing/convention-lint.ts
|
|
2136
2375
|
/**
|
|
2137
2376
|
* Convention linter — validates common misconfigurations in the route tree.
|
|
@@ -2231,7 +2470,7 @@ function checkRouteExports(node, warnings) {
|
|
|
2231
2470
|
if (node.route) {
|
|
2232
2471
|
const filePath = node.route.filePath;
|
|
2233
2472
|
try {
|
|
2234
|
-
const source =
|
|
2473
|
+
const source = readFileCached(filePath);
|
|
2235
2474
|
if (!EXPORT_PATTERN.test(source)) {
|
|
2236
2475
|
const hint = /export\s+default\b/.test(source) ? " It looks like you have a default export. route.ts uses named exports\n for HTTP methods (GET, POST, etc.), not a default export.\n" : "";
|
|
2237
2476
|
warnings.push({
|
|
@@ -2297,7 +2536,7 @@ function isScriptExtension(ext) {
|
|
|
2297
2536
|
}
|
|
2298
2537
|
function checkFileDefaultExport(filePath, fileType, warnings) {
|
|
2299
2538
|
try {
|
|
2300
|
-
const source =
|
|
2539
|
+
const source = readFileCached(filePath);
|
|
2301
2540
|
if (!DEFAULT_EXPORT_PATTERN.test(source)) warnings.push({
|
|
2302
2541
|
id: `NO_DEFAULT_EXPORT:${filePath}`,
|
|
2303
2542
|
summary: `${fileType}.tsx has no default export: ${filePath}`,
|
|
@@ -2620,245 +2859,6 @@ function invalidateManifest(server) {
|
|
|
2620
2859
|
}
|
|
2621
2860
|
server.hot.send({ type: "full-reload" });
|
|
2622
2861
|
}
|
|
2623
|
-
/**
|
|
2624
|
-
* TIM-830: Walk the route tree and collect every page that exports a
|
|
2625
|
-
* `searchParams` definition, then emit a module that statically imports
|
|
2626
|
-
* each export and registers it into the shared search-params registry.
|
|
2627
|
-
*
|
|
2628
|
-
* Keys use the un-interpolated route pattern (e.g. '/products/[id]') so
|
|
2629
|
-
* <Link> can look up the definition from the original `href` prop before
|
|
2630
|
-
* segmentParams are interpolated.
|
|
2631
|
-
*/
|
|
2632
|
-
/** Schema file extensions to check, in priority order. */
|
|
2633
|
-
var SCHEMA_EXTENSIONS = [
|
|
2634
|
-
".ts",
|
|
2635
|
-
".tsx",
|
|
2636
|
-
".js",
|
|
2637
|
-
".jsx"
|
|
2638
|
-
];
|
|
2639
|
-
/**
|
|
2640
|
-
* TIM-931: Generate the virtual:timber-schema module.
|
|
2641
|
-
*
|
|
2642
|
-
* When app/schema.{ts,tsx,js,jsx} exists, generates a module that
|
|
2643
|
-
* imports the schema and resolves its segmentParams codecs into a
|
|
2644
|
-
* bare-name codec map for runtime coercion.
|
|
2645
|
-
*
|
|
2646
|
-
* When no schema exists, exports null.
|
|
2647
|
-
*/
|
|
2648
|
-
function generateSchemaModule(appDir) {
|
|
2649
|
-
let schemaPath = null;
|
|
2650
|
-
for (const ext of SCHEMA_EXTENSIONS) {
|
|
2651
|
-
const candidate = join(appDir, `schema${ext}`);
|
|
2652
|
-
if (existsSync(candidate)) {
|
|
2653
|
-
schemaPath = candidate;
|
|
2654
|
-
break;
|
|
2655
|
-
}
|
|
2656
|
-
}
|
|
2657
|
-
if (!schemaPath) return ["// Auto-generated schema loader — no app/schema.ts found.", "export default null;"].join("\n");
|
|
2658
|
-
return [
|
|
2659
|
-
"// Auto-generated schema loader — do not edit.",
|
|
2660
|
-
"// Generated by timber-routing plugin (TIM-931).",
|
|
2661
|
-
"",
|
|
2662
|
-
`import schema from ${JSON.stringify(schemaPath)};`,
|
|
2663
|
-
`import { resolveSchemaCodecs } from '@timber-js/app/params';`,
|
|
2664
|
-
"",
|
|
2665
|
-
"const globalCodecs = schema?.segmentParams",
|
|
2666
|
-
" ? resolveSchemaCodecs(schema.segmentParams)",
|
|
2667
|
-
" : null;",
|
|
2668
|
-
"",
|
|
2669
|
-
"export default globalCodecs;"
|
|
2670
|
-
].join("\n");
|
|
2671
|
-
}
|
|
2672
|
-
function generateSearchParamsRegistryModule(tree) {
|
|
2673
|
-
const entries = [];
|
|
2674
|
-
function walk(node) {
|
|
2675
|
-
if (node.page) {
|
|
2676
|
-
if (node.params && fileHasSearchParamsExport(node.params.filePath)) entries.push({
|
|
2677
|
-
urlPath: node.urlPath,
|
|
2678
|
-
filePath: node.params.filePath
|
|
2679
|
-
});
|
|
2680
|
-
else if (fileHasSearchParamsExport(node.page.filePath)) entries.push({
|
|
2681
|
-
urlPath: node.urlPath,
|
|
2682
|
-
filePath: node.page.filePath
|
|
2683
|
-
});
|
|
2684
|
-
}
|
|
2685
|
-
for (const child of node.children) walk(child);
|
|
2686
|
-
for (const slot of Object.values(node.slots)) walk(slot);
|
|
2687
|
-
}
|
|
2688
|
-
walk(tree.root);
|
|
2689
|
-
entries.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
2690
|
-
const lines = [];
|
|
2691
|
-
lines.push("// Auto-generated search-params registry — do not edit.");
|
|
2692
|
-
lines.push("// Generated by timber-routing plugin (TIM-830).");
|
|
2693
|
-
lines.push("");
|
|
2694
|
-
lines.push("import { registerSearchParams } from '@timber-js/app/search-params';");
|
|
2695
|
-
lines.push("");
|
|
2696
|
-
entries.forEach((entry, i) => {
|
|
2697
|
-
lines.push(`import { searchParams as r${i} } from ${JSON.stringify(entry.filePath)};`);
|
|
2698
|
-
});
|
|
2699
|
-
lines.push("");
|
|
2700
|
-
entries.forEach((entry, i) => {
|
|
2701
|
-
lines.push(`registerSearchParams(${JSON.stringify(entry.urlPath)}, r${i});`);
|
|
2702
|
-
});
|
|
2703
|
-
lines.push("");
|
|
2704
|
-
lines.push("export {};");
|
|
2705
|
-
lines.push("");
|
|
2706
|
-
return lines.join("\n");
|
|
2707
|
-
}
|
|
2708
|
-
/**
|
|
2709
|
-
* Lightweight check for a `searchParams` named export. Mirrors
|
|
2710
|
-
* `fileHasExport('searchParams')` in routing/codegen.ts so we stay
|
|
2711
|
-
* in sync with what the type codegen detects.
|
|
2712
|
-
*/
|
|
2713
|
-
function fileHasSearchParamsExport(filePath) {
|
|
2714
|
-
try {
|
|
2715
|
-
if (!existsSync(filePath)) return false;
|
|
2716
|
-
const content = readFileSync(filePath, "utf-8");
|
|
2717
|
-
return /export\s+(const|let|var)\s+searchParams\b/.test(content) || /export\s*\{[^}]*\bsearchParams\b[^}]*\}/.test(content);
|
|
2718
|
-
} catch {
|
|
2719
|
-
return false;
|
|
2720
|
-
}
|
|
2721
|
-
}
|
|
2722
|
-
/**
|
|
2723
|
-
* Generate the virtual module source code for the route manifest.
|
|
2724
|
-
*
|
|
2725
|
-
* The output is a default-exported object containing the serialized route tree.
|
|
2726
|
-
* All file references use absolute paths (required for virtual modules).
|
|
2727
|
-
*/
|
|
2728
|
-
function generateManifestModule(tree, viteRoot) {
|
|
2729
|
-
const imports = [];
|
|
2730
|
-
let importIndex = 0;
|
|
2731
|
-
/**
|
|
2732
|
-
* Create a lazy import expression for a route file.
|
|
2733
|
-
* Returns the import variable name.
|
|
2734
|
-
*/
|
|
2735
|
-
function addImport(file) {
|
|
2736
|
-
const varName = `_route${importIndex++}`;
|
|
2737
|
-
imports.push(`const ${varName} = () => import("${file.filePath}");`);
|
|
2738
|
-
return varName;
|
|
2739
|
-
}
|
|
2740
|
-
/**
|
|
2741
|
-
* Serialize a segment node to a JS object literal.
|
|
2742
|
-
*/
|
|
2743
|
-
function serializeNode(node, indent) {
|
|
2744
|
-
const nextIndent = indent + " ";
|
|
2745
|
-
const parts = [];
|
|
2746
|
-
parts.push(`${nextIndent}segmentName: ${JSON.stringify(node.segmentName)},`);
|
|
2747
|
-
parts.push(`${nextIndent}segmentType: ${JSON.stringify(node.segmentType)},`);
|
|
2748
|
-
parts.push(`${nextIndent}urlPath: ${JSON.stringify(node.urlPath)},`);
|
|
2749
|
-
if (node.paramName) parts.push(`${nextIndent}paramName: ${JSON.stringify(node.paramName)},`);
|
|
2750
|
-
if (node.interceptionMarker) parts.push(`${nextIndent}interceptionMarker: ${JSON.stringify(node.interceptionMarker)},`);
|
|
2751
|
-
if (node.interceptedSegmentName) parts.push(`${nextIndent}interceptedSegmentName: ${JSON.stringify(node.interceptedSegmentName)},`);
|
|
2752
|
-
if (node.page) {
|
|
2753
|
-
const v = addImport(node.page);
|
|
2754
|
-
parts.push(`${nextIndent}page: { load: ${v}, filePath: ${JSON.stringify(node.page.filePath)} },`);
|
|
2755
|
-
}
|
|
2756
|
-
if (node.layout) {
|
|
2757
|
-
const v = addImport(node.layout);
|
|
2758
|
-
parts.push(`${nextIndent}layout: { load: ${v}, filePath: ${JSON.stringify(node.layout.filePath)} },`);
|
|
2759
|
-
}
|
|
2760
|
-
if (node.middleware) {
|
|
2761
|
-
const v = addImport(node.middleware);
|
|
2762
|
-
parts.push(`${nextIndent}middleware: { load: ${v}, filePath: ${JSON.stringify(node.middleware.filePath)} },`);
|
|
2763
|
-
}
|
|
2764
|
-
if (node.access) {
|
|
2765
|
-
const v = addImport(node.access);
|
|
2766
|
-
parts.push(`${nextIndent}access: { load: ${v}, filePath: ${JSON.stringify(node.access.filePath)} },`);
|
|
2767
|
-
}
|
|
2768
|
-
if (node.route) {
|
|
2769
|
-
const v = addImport(node.route);
|
|
2770
|
-
parts.push(`${nextIndent}route: { load: ${v}, filePath: ${JSON.stringify(node.route.filePath)} },`);
|
|
2771
|
-
}
|
|
2772
|
-
if (node.error) {
|
|
2773
|
-
const v = addImport(node.error);
|
|
2774
|
-
parts.push(`${nextIndent}error: { load: ${v}, filePath: ${JSON.stringify(node.error.filePath)} },`);
|
|
2775
|
-
}
|
|
2776
|
-
if (node.default) {
|
|
2777
|
-
const v = addImport(node.default);
|
|
2778
|
-
parts.push(`${nextIndent}default: { load: ${v}, filePath: ${JSON.stringify(node.default.filePath)} },`);
|
|
2779
|
-
}
|
|
2780
|
-
if (node.denied) {
|
|
2781
|
-
const v = addImport(node.denied);
|
|
2782
|
-
parts.push(`${nextIndent}denied: { load: ${v}, filePath: ${JSON.stringify(node.denied.filePath)} },`);
|
|
2783
|
-
}
|
|
2784
|
-
if (node.params) {
|
|
2785
|
-
const v = addImport(node.params);
|
|
2786
|
-
parts.push(`${nextIndent}params: { load: ${v}, filePath: ${JSON.stringify(node.params.filePath)} },`);
|
|
2787
|
-
}
|
|
2788
|
-
if (node.statusFiles && Object.keys(node.statusFiles).length > 0) {
|
|
2789
|
-
const statusEntries = [];
|
|
2790
|
-
for (const [code, file] of Object.entries(node.statusFiles)) {
|
|
2791
|
-
const v = addImport(file);
|
|
2792
|
-
statusEntries.push(`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
|
|
2793
|
-
}
|
|
2794
|
-
parts.push(`${nextIndent}statusFiles: {\n${statusEntries.join(",\n")}\n${nextIndent}},`);
|
|
2795
|
-
}
|
|
2796
|
-
if (node.jsonStatusFiles && Object.keys(node.jsonStatusFiles).length > 0) {
|
|
2797
|
-
const jsonEntries = [];
|
|
2798
|
-
for (const [code, file] of Object.entries(node.jsonStatusFiles)) {
|
|
2799
|
-
const v = addImport(file);
|
|
2800
|
-
jsonEntries.push(`${nextIndent} ${JSON.stringify(code)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
|
|
2801
|
-
}
|
|
2802
|
-
parts.push(`${nextIndent}jsonStatusFiles: {\n${jsonEntries.join(",\n")}\n${nextIndent}},`);
|
|
2803
|
-
}
|
|
2804
|
-
if (node.legacyStatusFiles && Object.keys(node.legacyStatusFiles).length > 0) {
|
|
2805
|
-
const legacyEntries = [];
|
|
2806
|
-
for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
|
|
2807
|
-
const v = addImport(file);
|
|
2808
|
-
legacyEntries.push(`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
|
|
2809
|
-
}
|
|
2810
|
-
parts.push(`${nextIndent}legacyStatusFiles: {\n${legacyEntries.join(",\n")}\n${nextIndent}},`);
|
|
2811
|
-
}
|
|
2812
|
-
if (node.metadataRoutes && Object.keys(node.metadataRoutes).length > 0) {
|
|
2813
|
-
const metaEntries = [];
|
|
2814
|
-
for (const [name, file] of Object.entries(node.metadataRoutes)) {
|
|
2815
|
-
const v = addImport(file);
|
|
2816
|
-
metaEntries.push(`${nextIndent} ${JSON.stringify(name)}: { load: ${v}, filePath: ${JSON.stringify(file.filePath)} }`);
|
|
2817
|
-
}
|
|
2818
|
-
parts.push(`${nextIndent}metadataRoutes: {\n${metaEntries.join(",\n")}\n${nextIndent}},`);
|
|
2819
|
-
}
|
|
2820
|
-
if (node.children.length > 0) {
|
|
2821
|
-
const childNodes = node.children.map((c) => serializeNode(c, nextIndent));
|
|
2822
|
-
parts.push(`${nextIndent}children: [\n${childNodes.join(",\n")}\n${nextIndent}],`);
|
|
2823
|
-
} else parts.push(`${nextIndent}children: [],`);
|
|
2824
|
-
const slotKeys = Object.keys(node.slots);
|
|
2825
|
-
if (slotKeys.length > 0) {
|
|
2826
|
-
const slotEntries = [];
|
|
2827
|
-
for (const slotName of slotKeys) {
|
|
2828
|
-
const slotNode = node.slots[slotName];
|
|
2829
|
-
slotEntries.push(`${nextIndent} ${JSON.stringify(slotName)}: ${serializeNode(slotNode, nextIndent + " ")}`);
|
|
2830
|
-
}
|
|
2831
|
-
parts.push(`${nextIndent}slots: {\n${slotEntries.join(",\n")}\n${nextIndent}},`);
|
|
2832
|
-
} else parts.push(`${nextIndent}slots: {},`);
|
|
2833
|
-
return `${indent}{\n${parts.join("\n")}\n${indent}}`;
|
|
2834
|
-
}
|
|
2835
|
-
const rootSerialized = serializeNode(tree.root, " ");
|
|
2836
|
-
let proxyLine = "";
|
|
2837
|
-
if (tree.proxy) proxyLine = ` proxy: { load: ${addImport(tree.proxy)}, filePath: ${JSON.stringify(tree.proxy.filePath)} },`;
|
|
2838
|
-
let globalErrorLine = "";
|
|
2839
|
-
if (tree.globalError) globalErrorLine = ` globalError: { load: ${addImport(tree.globalError)}, filePath: ${JSON.stringify(tree.globalError.filePath)} },`;
|
|
2840
|
-
const rewrites = collectInterceptionRewrites(tree.root);
|
|
2841
|
-
const rewritesLine = rewrites.length > 0 ? ` interceptionRewrites: ${JSON.stringify(rewrites.map((r) => ({
|
|
2842
|
-
interceptedPattern: r.interceptedPattern,
|
|
2843
|
-
interceptingPrefix: r.interceptingPrefix
|
|
2844
|
-
})))},` : "";
|
|
2845
|
-
return [
|
|
2846
|
-
"// Auto-generated route manifest — do not edit.",
|
|
2847
|
-
"// Generated by timber-routing plugin.",
|
|
2848
|
-
"",
|
|
2849
|
-
...imports,
|
|
2850
|
-
"",
|
|
2851
|
-
"const manifest = {",
|
|
2852
|
-
` viteRoot: ${JSON.stringify(viteRoot)},`,
|
|
2853
|
-
proxyLine,
|
|
2854
|
-
globalErrorLine,
|
|
2855
|
-
rewritesLine,
|
|
2856
|
-
` root: ${rootSerialized},`,
|
|
2857
|
-
"};",
|
|
2858
|
-
"",
|
|
2859
|
-
"export default manifest;"
|
|
2860
|
-
].filter((line) => line !== void 0 && line !== "").join("\n");
|
|
2861
|
-
}
|
|
2862
2862
|
//#endregion
|
|
2863
2863
|
//#region src/plugins/shims.ts
|
|
2864
2864
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -3061,6 +3061,58 @@ ${SERVER_STUB_EXPORTS.map((name) => name === "RedirectType" ? `export const Redi
|
|
|
3061
3061
|
};
|
|
3062
3062
|
}
|
|
3063
3063
|
//#endregion
|
|
3064
|
+
//#region src/fonts/paths.ts
|
|
3065
|
+
/**
|
|
3066
|
+
* Shared path/URL helpers for the timber-fonts pipeline.
|
|
3067
|
+
*
|
|
3068
|
+
* Two concerns live here:
|
|
3069
|
+
*
|
|
3070
|
+
* - `fontUrlPrefix` — the public URL prefix for font binaries, derived
|
|
3071
|
+
* from Vite's resolved `base`. Every URL the pipeline emits (@font-face
|
|
3072
|
+
* src, manifest hrefs, dev middleware matching) must go through this so
|
|
3073
|
+
* apps deployed under a non-root base (`base: '/my-app/'`) resolve
|
|
3074
|
+
* fonts correctly. Emitted *file* locations are not base-prefixed —
|
|
3075
|
+
* only URLs are.
|
|
3076
|
+
*
|
|
3077
|
+
* - `fontOutputBasename` / `hashedFontBasename` — basename derivation for
|
|
3078
|
+
* local font sources. Paths are absolutized with `resolve()`, which
|
|
3079
|
+
* produces backslash separators on Windows, so `split('/').pop()` is
|
|
3080
|
+
* not safe. `path.win32.basename` handles both separators.
|
|
3081
|
+
*
|
|
3082
|
+
* Design doc: 24-fonts.md
|
|
3083
|
+
*/
|
|
3084
|
+
/** Directory (relative to the build output / URL root) holding font binaries. */
|
|
3085
|
+
var FONT_OUTPUT_DIR = "_timber/fonts";
|
|
3086
|
+
/**
|
|
3087
|
+
* Build the URL prefix for font binaries from Vite's resolved `base`.
|
|
3088
|
+
*
|
|
3089
|
+
* `fontUrlPrefix('/')` → `/_timber/fonts`
|
|
3090
|
+
* `fontUrlPrefix('/my-app/')` → `/my-app/_timber/fonts`
|
|
3091
|
+
*/
|
|
3092
|
+
function fontUrlPrefix(base = "/") {
|
|
3093
|
+
return `${base.endsWith("/") ? base : `${base}/`}${FONT_OUTPUT_DIR}`;
|
|
3094
|
+
}
|
|
3095
|
+
/**
|
|
3096
|
+
* Derive the output basename for a local font source path.
|
|
3097
|
+
*
|
|
3098
|
+
* Uses `path.win32.basename` because it splits on both `/` and `\`,
|
|
3099
|
+
* handling absolute paths produced by `resolve()` on any platform.
|
|
3100
|
+
*/
|
|
3101
|
+
function fontOutputBasename(srcPath) {
|
|
3102
|
+
return win32.basename(srcPath);
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Derive the content-hashed output basename for a local font source.
|
|
3106
|
+
*
|
|
3107
|
+
* `hashedFontBasename('C:\\fonts\\MyFont-Bold.woff2', 'a1b2c3d4')`
|
|
3108
|
+
* → `MyFont-Bold-a1b2c3d4.woff2`
|
|
3109
|
+
*/
|
|
3110
|
+
function hashedFontBasename(srcPath, hash) {
|
|
3111
|
+
const basename = fontOutputBasename(srcPath);
|
|
3112
|
+
const ext = win32.extname(basename);
|
|
3113
|
+
return `${ext ? basename.slice(0, -ext.length) : basename}-${hash}${ext}`;
|
|
3114
|
+
}
|
|
3115
|
+
//#endregion
|
|
3064
3116
|
//#region src/fonts/google.ts
|
|
3065
3117
|
/**
|
|
3066
3118
|
* Google Fonts download, caching, and dev CDN fallback.
|
|
@@ -3256,12 +3308,14 @@ async function isCacheHit(metaPath, dataPath) {
|
|
|
3256
3308
|
* Generate @font-face descriptors for cached (production) Google Fonts.
|
|
3257
3309
|
*
|
|
3258
3310
|
* Each CachedFont gets a FontFaceDescriptor pointing to the
|
|
3259
|
-
* content-hashed URL under
|
|
3311
|
+
* content-hashed URL under `<base>_timber/fonts/`. The `urlPrefix` comes
|
|
3312
|
+
* from `fontUrlPrefix(viteBase)` so apps deployed under a non-root base
|
|
3313
|
+
* resolve fonts correctly.
|
|
3260
3314
|
*/
|
|
3261
|
-
function generateProductionFontFaces(cachedFonts, display) {
|
|
3315
|
+
function generateProductionFontFaces(cachedFonts, display, urlPrefix = fontUrlPrefix()) {
|
|
3262
3316
|
return cachedFonts.map((cf) => ({
|
|
3263
3317
|
family: cf.face.family,
|
|
3264
|
-
src: `url('
|
|
3318
|
+
src: `url('${urlPrefix}/${cf.hashedFilename}') format('woff2')`,
|
|
3265
3319
|
weight: cf.face.weight,
|
|
3266
3320
|
style: cf.face.style,
|
|
3267
3321
|
display,
|
|
@@ -3528,6 +3582,126 @@ function buildFontStack(family) {
|
|
|
3528
3582
|
return parts.join(", ");
|
|
3529
3583
|
}
|
|
3530
3584
|
//#endregion
|
|
3585
|
+
//#region src/fonts/ast-walk.ts
|
|
3586
|
+
/** Convert a 0-based byte offset into a 1-based line and 0-based column. */
|
|
3587
|
+
function offsetToLineColumn(source, offset) {
|
|
3588
|
+
let line = 1;
|
|
3589
|
+
let lastNewline = -1;
|
|
3590
|
+
for (let i = 0; i < offset; i++) if (source[i] === "\n") {
|
|
3591
|
+
line++;
|
|
3592
|
+
lastNewline = i;
|
|
3593
|
+
}
|
|
3594
|
+
return {
|
|
3595
|
+
line,
|
|
3596
|
+
column: offset - lastNewline - 1
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
/**
|
|
3600
|
+
* Invoke `visit` on every direct child AST node of `node` (array elements
|
|
3601
|
+
* and object-valued properties with a `type` field).
|
|
3602
|
+
*/
|
|
3603
|
+
function walkChildNodes(node, visit) {
|
|
3604
|
+
for (const key of Object.keys(node)) {
|
|
3605
|
+
if (key === "type") continue;
|
|
3606
|
+
const child = node[key];
|
|
3607
|
+
if (Array.isArray(child)) {
|
|
3608
|
+
for (const item of child) if (item && typeof item === "object" && item.type) visit(item);
|
|
3609
|
+
} else if (child && typeof child === "object" && child.type) visit(child);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Node types that introduce a nested scope — any font call inside is an
|
|
3614
|
+
* error (font calls must be module-level, same constraint as Next.js).
|
|
3615
|
+
* Module scope so it isn't rebuilt on every recursive walk call.
|
|
3616
|
+
*/
|
|
3617
|
+
var NESTED_SCOPE_TYPES = new Set([
|
|
3618
|
+
"FunctionDeclaration",
|
|
3619
|
+
"FunctionExpression",
|
|
3620
|
+
"ArrowFunctionExpression",
|
|
3621
|
+
"IfStatement",
|
|
3622
|
+
"ForStatement",
|
|
3623
|
+
"ForInStatement",
|
|
3624
|
+
"ForOfStatement",
|
|
3625
|
+
"WhileStatement",
|
|
3626
|
+
"DoWhileStatement",
|
|
3627
|
+
"SwitchStatement",
|
|
3628
|
+
"TryStatement",
|
|
3629
|
+
"BlockStatement",
|
|
3630
|
+
"ClassDeclaration",
|
|
3631
|
+
"ClassExpression"
|
|
3632
|
+
]);
|
|
3633
|
+
/**
|
|
3634
|
+
* Recursively walk an AST node looking for font CallExpressions in nested
|
|
3635
|
+
* scopes (function bodies, if-blocks, loops, etc.). Font calls must be
|
|
3636
|
+
* module-level; nested calls are collected as errors.
|
|
3637
|
+
*
|
|
3638
|
+
* `isNested` tracks whether we've descended past the top-level statement.
|
|
3639
|
+
* Top-level VariableDeclarations and ExpressionStatements are already
|
|
3640
|
+
* handled by the main loop in `findFontCallSites`, so we only flag calls
|
|
3641
|
+
* found deeper.
|
|
3642
|
+
*/
|
|
3643
|
+
function walkForNestedFontCalls(node, bindingNames, source, out, isNested) {
|
|
3644
|
+
if (!node || typeof node !== "object") return;
|
|
3645
|
+
if (node.type === "CallExpression") {
|
|
3646
|
+
const callee = node.callee;
|
|
3647
|
+
if (isNested && callee.type === "Identifier" && bindingNames.has(callee.name)) {
|
|
3648
|
+
const pos = offsetToLineColumn(source, node.start);
|
|
3649
|
+
out.push({
|
|
3650
|
+
name: callee.name,
|
|
3651
|
+
line: pos.line,
|
|
3652
|
+
column: pos.column
|
|
3653
|
+
});
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3656
|
+
}
|
|
3657
|
+
const enterNested = isNested || NESTED_SCOPE_TYPES.has(node.type);
|
|
3658
|
+
walkChildNodes(node, (child) => walkForNestedFontCalls(child, bindingNames, source, out, enterNested));
|
|
3659
|
+
}
|
|
3660
|
+
/**
|
|
3661
|
+
* Recursively walk an AST node looking for references to font bindings
|
|
3662
|
+
* that were not consumed by a recognized call site.
|
|
3663
|
+
*
|
|
3664
|
+
* Skips:
|
|
3665
|
+
* - recognized CallExpressions (the transform rewrites them wholesale)
|
|
3666
|
+
* - TS type-position nodes (erased at runtime)
|
|
3667
|
+
* - non-computed member properties (`obj.Inter` doesn't reference the binding)
|
|
3668
|
+
* - non-computed object keys (`{ Inter: 1 }` doesn't reference the binding)
|
|
3669
|
+
* - export aliases (`export { inter as Inter }` — only `local` is a reference)
|
|
3670
|
+
*/
|
|
3671
|
+
function walkForUnconsumedReferences(node, bindingNames, consumedCallStarts, source, out) {
|
|
3672
|
+
if (!node || typeof node !== "object") return;
|
|
3673
|
+
if (node.type === "ImportDeclaration" || node.type.startsWith("TS")) return;
|
|
3674
|
+
if (node.type === "Identifier") {
|
|
3675
|
+
const name = node.name;
|
|
3676
|
+
if (bindingNames.has(name)) {
|
|
3677
|
+
const pos = offsetToLineColumn(source, node.start);
|
|
3678
|
+
out.push({
|
|
3679
|
+
name,
|
|
3680
|
+
line: pos.line,
|
|
3681
|
+
column: pos.column
|
|
3682
|
+
});
|
|
3683
|
+
}
|
|
3684
|
+
return;
|
|
3685
|
+
}
|
|
3686
|
+
if (node.type === "CallExpression" && consumedCallStarts.has(node.start)) return;
|
|
3687
|
+
if (node.type === "MemberExpression" && !node.computed) {
|
|
3688
|
+
const object = node.object;
|
|
3689
|
+
if (object) walkForUnconsumedReferences(object, bindingNames, consumedCallStarts, source, out);
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
if (node.type === "Property" && !node.computed) {
|
|
3693
|
+
const value = node.value;
|
|
3694
|
+
if (value) walkForUnconsumedReferences(value, bindingNames, consumedCallStarts, source, out);
|
|
3695
|
+
return;
|
|
3696
|
+
}
|
|
3697
|
+
if (node.type === "ExportSpecifier") {
|
|
3698
|
+
const local = node.local;
|
|
3699
|
+
if (local) walkForUnconsumedReferences(local, bindingNames, consumedCallStarts, source, out);
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
walkChildNodes(node, (child) => walkForUnconsumedReferences(child, bindingNames, consumedCallStarts, source, out));
|
|
3703
|
+
}
|
|
3704
|
+
//#endregion
|
|
3531
3705
|
//#region src/fonts/ast.ts
|
|
3532
3706
|
/**
|
|
3533
3707
|
* AST utilities for extracting static font configs from source code.
|
|
@@ -3546,19 +3720,6 @@ function buildFontStack(family) {
|
|
|
3546
3720
|
*
|
|
3547
3721
|
* Design doc: 24-fonts.md §"Step 1: Static Analysis"
|
|
3548
3722
|
*/
|
|
3549
|
-
/** Convert a 0-based byte offset into a 1-based line and 0-based column. */
|
|
3550
|
-
function offsetToLineColumn(source, offset) {
|
|
3551
|
-
let line = 1;
|
|
3552
|
-
let lastNewline = -1;
|
|
3553
|
-
for (let i = 0; i < offset; i++) if (source[i] === "\n") {
|
|
3554
|
-
line++;
|
|
3555
|
-
lastNewline = i;
|
|
3556
|
-
}
|
|
3557
|
-
return {
|
|
3558
|
-
line,
|
|
3559
|
-
column: offset - lastNewline - 1
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
3723
|
/**
|
|
3563
3724
|
* Extract a static string value from an AST node.
|
|
3564
3725
|
* Returns undefined if the node is not a string literal.
|
|
@@ -3769,6 +3930,7 @@ function findFontCallSites(source) {
|
|
|
3769
3930
|
}
|
|
3770
3931
|
const bindings = [];
|
|
3771
3932
|
const importNodesToRemove = [];
|
|
3933
|
+
const namespaceImports = [];
|
|
3772
3934
|
for (const stmt of program.body) {
|
|
3773
3935
|
if (!isImportDeclaration(stmt)) continue;
|
|
3774
3936
|
const importDecl = stmt;
|
|
@@ -3777,26 +3939,40 @@ function findFontCallSites(source) {
|
|
|
3777
3939
|
if (GOOGLE_FONT_SOURCES.has(specSource)) provider = "google";
|
|
3778
3940
|
else if (LOCAL_FONT_SOURCES.has(specSource)) provider = "local";
|
|
3779
3941
|
if (!provider) continue;
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3942
|
+
let producedBinding = false;
|
|
3943
|
+
for (const spec of importDecl.specifiers) if (spec.type === "ImportSpecifier" && provider === "google") {
|
|
3944
|
+
producedBinding = true;
|
|
3945
|
+
bindings.push({
|
|
3946
|
+
localName: spec.local.name,
|
|
3947
|
+
importedName: spec.imported?.name ?? spec.local.name,
|
|
3948
|
+
provider,
|
|
3949
|
+
importNode: importDecl
|
|
3950
|
+
});
|
|
3951
|
+
} else if (spec.type === "ImportDefaultSpecifier" && provider === "local") {
|
|
3952
|
+
producedBinding = true;
|
|
3953
|
+
bindings.push({
|
|
3954
|
+
localName: spec.local.name,
|
|
3955
|
+
importedName: "default",
|
|
3956
|
+
provider,
|
|
3957
|
+
importNode: importDecl
|
|
3958
|
+
});
|
|
3959
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
3960
|
+
const pos = offsetToLineColumn(source, spec.start);
|
|
3961
|
+
namespaceImports.push({
|
|
3962
|
+
name: spec.local.name,
|
|
3963
|
+
line: pos.line,
|
|
3964
|
+
column: pos.column
|
|
3965
|
+
});
|
|
3966
|
+
}
|
|
3967
|
+
if (producedBinding) importNodesToRemove.push({
|
|
3793
3968
|
start: stmt.start,
|
|
3794
3969
|
end: stmt.end
|
|
3795
3970
|
});
|
|
3796
3971
|
}
|
|
3797
|
-
if (bindings.length === 0) return null;
|
|
3972
|
+
if (bindings.length === 0 && namespaceImports.length === 0) return null;
|
|
3798
3973
|
const bindingsByName = new Map(bindings.map((b) => [b.localName, b]));
|
|
3799
3974
|
const callSites = [];
|
|
3975
|
+
const consumedCallStarts = /* @__PURE__ */ new Set();
|
|
3800
3976
|
let dynamicCall = null;
|
|
3801
3977
|
for (const stmt of program.body) {
|
|
3802
3978
|
const effectiveStmt = isExportNamedDeclaration(stmt) && stmt.declaration ? stmt.declaration : stmt;
|
|
@@ -3807,6 +3983,7 @@ function findFontCallSites(source) {
|
|
|
3807
3983
|
const site = matchCallExpression(call, bindingsByName, source);
|
|
3808
3984
|
if (!site) continue;
|
|
3809
3985
|
if (site === "dynamic") {
|
|
3986
|
+
consumedCallStarts.add(call.start);
|
|
3810
3987
|
if (!dynamicCall && isIdentifier(call.callee)) {
|
|
3811
3988
|
const argStart = call.arguments[0].start;
|
|
3812
3989
|
const argEnd = call.arguments[0].end;
|
|
@@ -3816,6 +3993,7 @@ function findFontCallSites(source) {
|
|
|
3816
3993
|
}
|
|
3817
3994
|
const assignedName = isIdentifier(declarator.id) ? declarator.id.name : null;
|
|
3818
3995
|
const { line, column } = offsetToLineColumn(source, call.start);
|
|
3996
|
+
consumedCallStarts.add(call.start);
|
|
3819
3997
|
callSites.push({
|
|
3820
3998
|
binding: site.binding,
|
|
3821
3999
|
assignedName,
|
|
@@ -3832,6 +4010,7 @@ function findFontCallSites(source) {
|
|
|
3832
4010
|
const site = matchCallExpression(call, bindingsByName, source);
|
|
3833
4011
|
if (site && site !== "dynamic") {
|
|
3834
4012
|
const { line, column } = offsetToLineColumn(source, call.start);
|
|
4013
|
+
consumedCallStarts.add(call.start);
|
|
3835
4014
|
callSites.push({
|
|
3836
4015
|
binding: site.binding,
|
|
3837
4016
|
assignedName: null,
|
|
@@ -3841,10 +4020,13 @@ function findFontCallSites(source) {
|
|
|
3841
4020
|
line,
|
|
3842
4021
|
column
|
|
3843
4022
|
});
|
|
3844
|
-
} else if (site === "dynamic"
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
4023
|
+
} else if (site === "dynamic") {
|
|
4024
|
+
consumedCallStarts.add(call.start);
|
|
4025
|
+
if (!dynamicCall && isIdentifier(call.callee)) {
|
|
4026
|
+
const argStart = call.arguments[0].start;
|
|
4027
|
+
const argEnd = call.arguments[0].end;
|
|
4028
|
+
dynamicCall = `${call.callee.name}(${source.slice(argStart, argEnd)})`;
|
|
4029
|
+
}
|
|
3848
4030
|
}
|
|
3849
4031
|
}
|
|
3850
4032
|
}
|
|
@@ -3854,63 +4036,22 @@ function findFontCallSites(source) {
|
|
|
3854
4036
|
if (stmt.type === "ImportDeclaration") continue;
|
|
3855
4037
|
walkForNestedFontCalls(stmt, bindingsByName, source, nestedCalls, false);
|
|
3856
4038
|
}
|
|
4039
|
+
const unsupportedUsages = [];
|
|
4040
|
+
for (const stmt of program.body) {
|
|
4041
|
+
if (stmt.type === "ImportDeclaration") continue;
|
|
4042
|
+
walkForUnconsumedReferences(stmt, bindingsByName, consumedCallStarts, source, unsupportedUsages);
|
|
4043
|
+
}
|
|
3857
4044
|
return {
|
|
3858
4045
|
bindings,
|
|
3859
4046
|
callSites,
|
|
3860
4047
|
importNodesToRemove,
|
|
3861
4048
|
dynamicCall,
|
|
3862
|
-
nestedCalls
|
|
4049
|
+
nestedCalls,
|
|
4050
|
+
namespaceImports,
|
|
4051
|
+
unsupportedUsages
|
|
3863
4052
|
};
|
|
3864
4053
|
}
|
|
3865
4054
|
/**
|
|
3866
|
-
* Recursively walk an AST node looking for font CallExpressions in nested
|
|
3867
|
-
* scopes (function bodies, if-blocks, loops, etc.). Font calls must be
|
|
3868
|
-
* module-level; nested calls are collected as errors.
|
|
3869
|
-
*
|
|
3870
|
-
* `isNested` tracks whether we've descended past the top-level statement.
|
|
3871
|
-
* Top-level VariableDeclarations and ExpressionStatements are already
|
|
3872
|
-
* handled by the main loop in `findFontCallSites`, so we only flag calls
|
|
3873
|
-
* found deeper.
|
|
3874
|
-
*/
|
|
3875
|
-
function walkForNestedFontCalls(node, bindings, source, out, isNested) {
|
|
3876
|
-
if (!node || typeof node !== "object") return;
|
|
3877
|
-
if (node.type === "CallExpression") {
|
|
3878
|
-
const callee = node.callee;
|
|
3879
|
-
if (isNested && callee.type === "Identifier" && bindings.has(callee.name)) {
|
|
3880
|
-
const pos = offsetToLineColumn(source, node.start);
|
|
3881
|
-
out.push({
|
|
3882
|
-
name: callee.name,
|
|
3883
|
-
line: pos.line,
|
|
3884
|
-
column: pos.column
|
|
3885
|
-
});
|
|
3886
|
-
return;
|
|
3887
|
-
}
|
|
3888
|
-
}
|
|
3889
|
-
const enterNested = isNested || new Set([
|
|
3890
|
-
"FunctionDeclaration",
|
|
3891
|
-
"FunctionExpression",
|
|
3892
|
-
"ArrowFunctionExpression",
|
|
3893
|
-
"IfStatement",
|
|
3894
|
-
"ForStatement",
|
|
3895
|
-
"ForInStatement",
|
|
3896
|
-
"ForOfStatement",
|
|
3897
|
-
"WhileStatement",
|
|
3898
|
-
"DoWhileStatement",
|
|
3899
|
-
"SwitchStatement",
|
|
3900
|
-
"TryStatement",
|
|
3901
|
-
"BlockStatement",
|
|
3902
|
-
"ClassDeclaration",
|
|
3903
|
-
"ClassExpression"
|
|
3904
|
-
]).has(node.type);
|
|
3905
|
-
for (const key of Object.keys(node)) {
|
|
3906
|
-
if (key === "type") continue;
|
|
3907
|
-
const child = node[key];
|
|
3908
|
-
if (Array.isArray(child)) {
|
|
3909
|
-
for (const item of child) if (item && typeof item === "object" && item.type) walkForNestedFontCalls(item, bindings, source, out, enterNested);
|
|
3910
|
-
} else if (child && typeof child === "object" && child.type) walkForNestedFontCalls(child, bindings, source, out, enterNested);
|
|
3911
|
-
}
|
|
3912
|
-
}
|
|
3913
|
-
/**
|
|
3914
4055
|
* Match a CallExpression against the font bindings map.
|
|
3915
4056
|
*
|
|
3916
4057
|
* Returns the binding + config node if the call is a static font call,
|
|
@@ -4001,23 +4142,23 @@ function resolveLocalFontPaths(importerPath, sources) {
|
|
|
4001
4142
|
function generateFamilyName(sources) {
|
|
4002
4143
|
if (sources.length === 0) return "Local Font";
|
|
4003
4144
|
const firstPath = sources[0].path;
|
|
4004
|
-
const stem = (firstPath
|
|
4145
|
+
const stem = fontOutputBasename(firstPath).replace(/\.[^.]+$/, "");
|
|
4005
4146
|
return stem.replace(/[-_]?(Regular|Bold|Italic|Light|Medium|SemiBold|ExtraBold|Thin|Black|Heavy)$/i, "") || stem;
|
|
4006
4147
|
}
|
|
4007
4148
|
/**
|
|
4008
4149
|
* Generate @font-face descriptors for local font sources.
|
|
4009
4150
|
*
|
|
4010
4151
|
* Each source entry produces one @font-face rule. The `src` descriptor
|
|
4011
|
-
* uses a `url()` pointing to the served path under
|
|
4012
|
-
* The `urlPrefix` defaults to
|
|
4013
|
-
*
|
|
4152
|
+
* uses a `url()` pointing to the served path under `<base>_timber/fonts/`.
|
|
4153
|
+
* The `urlPrefix` defaults to `fontUrlPrefix()` (root base) — pass
|
|
4154
|
+
* `fontUrlPrefix(viteBase)` so non-root deployments resolve correctly.
|
|
4014
4155
|
*/
|
|
4015
|
-
function generateLocalFontFaces(family, sources, display, urlPrefix =
|
|
4156
|
+
function generateLocalFontFaces(family, sources, display, urlPrefix = fontUrlPrefix()) {
|
|
4016
4157
|
return sources.map((entry) => {
|
|
4017
4158
|
const format = inferFontFormat(entry.path);
|
|
4018
4159
|
return {
|
|
4019
4160
|
family,
|
|
4020
|
-
src: `url('${urlPrefix}/${entry.hashedBasename ?? entry.path
|
|
4161
|
+
src: `url('${urlPrefix}/${entry.hashedBasename ?? fontOutputBasename(entry.path)}') format('${format}')`,
|
|
4021
4162
|
weight: entry.weight,
|
|
4022
4163
|
style: entry.style,
|
|
4023
4164
|
display
|
|
@@ -4224,9 +4365,18 @@ var FontPipeline = class {
|
|
|
4224
4365
|
_entries = /* @__PURE__ */ new Map();
|
|
4225
4366
|
/** When true, local font sources are content-hashed at registration time. */
|
|
4226
4367
|
_hashOnRegister;
|
|
4368
|
+
/**
|
|
4369
|
+
* Vite's resolved `base`. Set from the plugin's `configResolved` hook so
|
|
4370
|
+
* every @font-face URL the pipeline renders carries the prefix.
|
|
4371
|
+
*/
|
|
4372
|
+
_base = "/";
|
|
4227
4373
|
constructor(opts) {
|
|
4228
4374
|
this._hashOnRegister = opts?.hashOnRegister ?? false;
|
|
4229
4375
|
}
|
|
4376
|
+
/** Set Vite's resolved `base` (from `configResolved`). */
|
|
4377
|
+
setBase(base) {
|
|
4378
|
+
this._base = base;
|
|
4379
|
+
}
|
|
4230
4380
|
/**
|
|
4231
4381
|
* Per-importer virtual CSS module id registry. Populated by the
|
|
4232
4382
|
* transform hook when it inlines a font function call, read by the
|
|
@@ -4296,10 +4446,8 @@ var FontPipeline = class {
|
|
|
4296
4446
|
if (this._hashOnRegister && entry.provider === "local" && entry.localSources) for (const src of entry.localSources) {
|
|
4297
4447
|
const absolutePath = normalize(resolve(src.path));
|
|
4298
4448
|
if (!existsSync(absolutePath)) continue;
|
|
4299
|
-
const
|
|
4300
|
-
|
|
4301
|
-
const ext = extname(basename);
|
|
4302
|
-
src.hashedBasename = `${ext ? basename.slice(0, -ext.length) : basename}-${hash}${ext}`;
|
|
4449
|
+
const data = readFileSync(absolutePath);
|
|
4450
|
+
src.hashedBasename = hashedFontBasename(src.path, contentHash(data));
|
|
4303
4451
|
}
|
|
4304
4452
|
this._entries.set(entry.id, entry);
|
|
4305
4453
|
}
|
|
@@ -4363,8 +4511,9 @@ var FontPipeline = class {
|
|
|
4363
4511
|
* or lazily in `load` for dev).
|
|
4364
4512
|
*/
|
|
4365
4513
|
getCss() {
|
|
4514
|
+
const urlPrefix = fontUrlPrefix(this._base);
|
|
4366
4515
|
const cssParts = [];
|
|
4367
|
-
for (const entry of this._entries.values()) cssParts.push(renderEntryCss(entry));
|
|
4516
|
+
for (const entry of this._entries.values()) cssParts.push(renderEntryCss(entry, urlPrefix));
|
|
4368
4517
|
return cssParts.join("\n\n");
|
|
4369
4518
|
}
|
|
4370
4519
|
/**
|
|
@@ -4384,10 +4533,11 @@ var FontPipeline = class {
|
|
|
4384
4533
|
* pipeline handles it as a valid empty CSS module.
|
|
4385
4534
|
*/
|
|
4386
4535
|
getCssForSegment(importer) {
|
|
4536
|
+
const urlPrefix = fontUrlPrefix(this._base);
|
|
4387
4537
|
const cssParts = [];
|
|
4388
4538
|
for (const entry of this._entries.values()) {
|
|
4389
4539
|
if (entry.importer !== importer) continue;
|
|
4390
|
-
cssParts.push(renderEntryCss(entry));
|
|
4540
|
+
cssParts.push(renderEntryCss(entry, urlPrefix));
|
|
4391
4541
|
}
|
|
4392
4542
|
return cssParts.join("\n\n");
|
|
4393
4543
|
}
|
|
@@ -4395,15 +4545,13 @@ var FontPipeline = class {
|
|
|
4395
4545
|
/**
|
|
4396
4546
|
* Render the combined CSS for a single font entry.
|
|
4397
4547
|
*
|
|
4398
|
-
*
|
|
4399
|
-
*
|
|
4400
|
-
* of from a parallel map. Kept private to this module so the only public
|
|
4401
|
-
* way to render a `FontEntry`'s CSS is through `FontPipeline.getCss()`.
|
|
4548
|
+
* Reads `faces` straight off the entry. Private to this module — the only
|
|
4549
|
+
* public way to render a `FontEntry`'s CSS is through `FontPipeline.getCss()`.
|
|
4402
4550
|
*/
|
|
4403
|
-
function renderEntryCss(entry) {
|
|
4551
|
+
function renderEntryCss(entry, urlPrefix) {
|
|
4404
4552
|
const cssParts = [];
|
|
4405
4553
|
if (entry.provider === "local" && entry.localSources) {
|
|
4406
|
-
const faceCss = generateFontFaces(generateLocalFontFaces(entry.family, entry.localSources, entry.display));
|
|
4554
|
+
const faceCss = generateFontFaces(generateLocalFontFaces(entry.family, entry.localSources, entry.display, urlPrefix));
|
|
4407
4555
|
if (faceCss) cssParts.push(faceCss);
|
|
4408
4556
|
}
|
|
4409
4557
|
if (entry.provider === "google" && entry.faces && entry.faces.length > 0) {
|
|
@@ -5525,6 +5673,14 @@ function runFontsTransform(code, id, pipeline, emitError) {
|
|
|
5525
5673
|
const first = analysis.nestedCalls[0];
|
|
5526
5674
|
emitError(`Font function \`${first.name}()\` must be called at module scope. Found nested call at line ${first.line}, column ${first.column}. Move the call to the top level of the module.`);
|
|
5527
5675
|
}
|
|
5676
|
+
if (analysis.namespaceImports.length > 0) {
|
|
5677
|
+
const first = analysis.namespaceImports[0];
|
|
5678
|
+
emitError(`Namespace imports of font modules are not supported (\`* as ${first.name}\` at line ${first.line}, column ${first.column}). Use a named import instead: \`import { Inter } from 'next/font/google'\`.`);
|
|
5679
|
+
}
|
|
5680
|
+
if (analysis.unsupportedUsages.length > 0) {
|
|
5681
|
+
const first = analysis.unsupportedUsages[0];
|
|
5682
|
+
emitError(`Unsupported usage of font function \`${first.name}\` at line ${first.line}, column ${first.column}. A font call must be \`const x = Font({...})\` at module scope (export const is fine). The call result cannot be nested in other expressions, and the imported font function cannot be aliased or re-exported.`);
|
|
5683
|
+
}
|
|
5528
5684
|
if (analysis.callSites.length === 0 && analysis.bindings.length === 0) return null;
|
|
5529
5685
|
const s = new MagicString(code);
|
|
5530
5686
|
for (const site of analysis.callSites) if (site.binding.provider === "google") {
|
|
@@ -5600,11 +5756,12 @@ var FONT_MIME_TYPES = {
|
|
|
5600
5756
|
* captured by reference so that fonts registered after `configureServer`
|
|
5601
5757
|
* runs (during transform) are still resolvable.
|
|
5602
5758
|
*/
|
|
5603
|
-
function installFontDevMiddleware(server, pipeline) {
|
|
5759
|
+
function installFontDevMiddleware(server, pipeline, base = "/") {
|
|
5760
|
+
const urlPrefix = `${fontUrlPrefix(base)}/`;
|
|
5604
5761
|
server.middlewares.use((req, res, next) => {
|
|
5605
5762
|
const url = req.url;
|
|
5606
|
-
if (!url || !url.startsWith(
|
|
5607
|
-
const requestedFilename = url.slice(
|
|
5763
|
+
if (!url || !url.startsWith(urlPrefix)) return next();
|
|
5764
|
+
const requestedFilename = url.slice(urlPrefix.length);
|
|
5608
5765
|
if (requestedFilename.includes("..") || requestedFilename.includes("/")) {
|
|
5609
5766
|
res.statusCode = 400;
|
|
5610
5767
|
res.end("Bad request");
|
|
@@ -5613,7 +5770,7 @@ function installFontDevMiddleware(server, pipeline) {
|
|
|
5613
5770
|
for (const font of pipeline.fonts()) {
|
|
5614
5771
|
if (font.provider !== "local" || !font.localSources) continue;
|
|
5615
5772
|
for (const src of font.localSources) {
|
|
5616
|
-
if ((src.path
|
|
5773
|
+
if (fontOutputBasename(src.path) !== requestedFilename && src.hashedBasename !== requestedFilename) continue;
|
|
5617
5774
|
const absolutePath = normalize(resolve(src.path));
|
|
5618
5775
|
if (!existsSync(absolutePath)) {
|
|
5619
5776
|
res.statusCode = 404;
|
|
@@ -5623,7 +5780,7 @@ function installFontDevMiddleware(server, pipeline) {
|
|
|
5623
5780
|
const data = readFileSync(absolutePath);
|
|
5624
5781
|
const ext = absolutePath.split(".").pop()?.toLowerCase();
|
|
5625
5782
|
res.setHeader("Content-Type", FONT_MIME_TYPES[ext ?? ""] ?? "application/octet-stream");
|
|
5626
|
-
res.setHeader("Cache-Control", "
|
|
5783
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
5627
5784
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
5628
5785
|
res.end(data);
|
|
5629
5786
|
return;
|
|
@@ -5667,7 +5824,7 @@ function groupCachedFontsByFamily(cachedFonts) {
|
|
|
5667
5824
|
function emitFontAssets(pipeline, emitFile, warn) {
|
|
5668
5825
|
for (const cf of pipeline.uniqueCachedFiles()) emitFile({
|
|
5669
5826
|
type: "asset",
|
|
5670
|
-
fileName:
|
|
5827
|
+
fileName: `${FONT_OUTPUT_DIR}/${cf.hashedFilename}`,
|
|
5671
5828
|
source: cf.data
|
|
5672
5829
|
});
|
|
5673
5830
|
for (const font of pipeline.fonts()) {
|
|
@@ -5678,11 +5835,11 @@ function emitFontAssets(pipeline, emitFile, warn) {
|
|
|
5678
5835
|
warn(`Local font file not found: ${absolutePath}`);
|
|
5679
5836
|
continue;
|
|
5680
5837
|
}
|
|
5681
|
-
const filename = src.hashedBasename ?? src.path
|
|
5838
|
+
const filename = src.hashedBasename ?? fontOutputBasename(src.path);
|
|
5682
5839
|
const data = readFileSync(absolutePath);
|
|
5683
5840
|
emitFile({
|
|
5684
5841
|
type: "asset",
|
|
5685
|
-
fileName:
|
|
5842
|
+
fileName: `${FONT_OUTPUT_DIR}/${filename}`,
|
|
5686
5843
|
source: data
|
|
5687
5844
|
});
|
|
5688
5845
|
}
|
|
@@ -5695,22 +5852,25 @@ function emitFontAssets(pipeline, emitFile, warn) {
|
|
|
5695
5852
|
* `buildManifest.css` keys work after TIM-1014.
|
|
5696
5853
|
*
|
|
5697
5854
|
* All font binaries use content-hashed filenames for immutable caching.
|
|
5855
|
+
* Hrefs are prefixed with Vite's resolved `base` so they match the URLs
|
|
5856
|
+
* the platform serves the emitted binaries under.
|
|
5698
5857
|
*/
|
|
5699
|
-
function writeFontManifest(pipeline, buildManifest) {
|
|
5858
|
+
function writeFontManifest(pipeline, buildManifest, base = "/") {
|
|
5859
|
+
const urlPrefix = fontUrlPrefix(base);
|
|
5700
5860
|
const fontsByImporter = /* @__PURE__ */ new Map();
|
|
5701
5861
|
for (const entry of pipeline.entries()) {
|
|
5702
5862
|
const manifestEntries = fontsByImporter.get(entry.importer) ?? [];
|
|
5703
5863
|
if (entry.provider === "local" && entry.localSources) for (const src of entry.localSources) {
|
|
5704
|
-
const filename = src.hashedBasename ?? src.path
|
|
5864
|
+
const filename = src.hashedBasename ?? fontOutputBasename(src.path);
|
|
5705
5865
|
const format = inferFontFormat(src.path);
|
|
5706
5866
|
manifestEntries.push({
|
|
5707
|
-
href:
|
|
5867
|
+
href: `${urlPrefix}/${filename}`,
|
|
5708
5868
|
format,
|
|
5709
5869
|
crossOrigin: "anonymous"
|
|
5710
5870
|
});
|
|
5711
5871
|
}
|
|
5712
5872
|
else if (entry.cachedFiles) for (const cf of entry.cachedFiles) manifestEntries.push({
|
|
5713
|
-
href:
|
|
5873
|
+
href: `${urlPrefix}/${cf.hashedFilename}`,
|
|
5714
5874
|
format: "woff2",
|
|
5715
5875
|
crossOrigin: "anonymous"
|
|
5716
5876
|
});
|
|
@@ -5725,9 +5885,19 @@ function writeFontManifest(pipeline, buildManifest) {
|
|
|
5725
5885
|
*/
|
|
5726
5886
|
function timberFonts(ctx) {
|
|
5727
5887
|
const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
|
|
5888
|
+
let resolvedBase = "/";
|
|
5728
5889
|
return {
|
|
5729
5890
|
name: "timber-fonts",
|
|
5730
5891
|
/**
|
|
5892
|
+
* Capture Vite's resolved `base` so every font URL the pipeline emits
|
|
5893
|
+
* (@font-face src, manifest hrefs, dev middleware matching) carries
|
|
5894
|
+
* the prefix. Emitted file locations are unaffected — only URLs.
|
|
5895
|
+
*/
|
|
5896
|
+
configResolved(config) {
|
|
5897
|
+
resolvedBase = config.base;
|
|
5898
|
+
pipeline.setBase(config.base);
|
|
5899
|
+
},
|
|
5900
|
+
/**
|
|
5731
5901
|
* `enforce: 'pre'` is load-bearing for the font CSS pipeline (TIM-828).
|
|
5732
5902
|
*
|
|
5733
5903
|
* The transform hook injects a side-effect
|
|
@@ -5811,7 +5981,7 @@ function timberFonts(ctx) {
|
|
|
5811
5981
|
* this middleware.
|
|
5812
5982
|
*/
|
|
5813
5983
|
configureServer(server) {
|
|
5814
|
-
installFontDevMiddleware(server, pipeline);
|
|
5984
|
+
installFontDevMiddleware(server, pipeline, resolvedBase);
|
|
5815
5985
|
},
|
|
5816
5986
|
/**
|
|
5817
5987
|
* Download and cache Google Fonts during production builds.
|
|
@@ -5828,10 +5998,11 @@ function timberFonts(ctx) {
|
|
|
5828
5998
|
const googleFonts = [...pipeline.googleFonts()];
|
|
5829
5999
|
if (googleFonts.length === 0) return;
|
|
5830
6000
|
const cachedByFamily = groupCachedFontsByFamily(await downloadAndCacheFonts(googleFonts, ctx.root));
|
|
6001
|
+
const urlPrefix = fontUrlPrefix(resolvedBase);
|
|
5831
6002
|
for (const font of googleFonts) {
|
|
5832
6003
|
const familyCached = cachedByFamily.get(font.family.toLowerCase()) ?? [];
|
|
5833
6004
|
pipeline.attachCachedFiles(font.id, familyCached);
|
|
5834
|
-
pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display));
|
|
6005
|
+
pipeline.attachFaces(font.id, generateProductionFontFaces(familyCached, font.display, urlPrefix));
|
|
5835
6006
|
}
|
|
5836
6007
|
},
|
|
5837
6008
|
/**
|
|
@@ -5853,7 +6024,7 @@ function timberFonts(ctx) {
|
|
|
5853
6024
|
generateBundle() {
|
|
5854
6025
|
emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
|
|
5855
6026
|
if (!ctx.buildManifest) return;
|
|
5856
|
-
writeFontManifest(pipeline, ctx.buildManifest);
|
|
6027
|
+
writeFontManifest(pipeline, ctx.buildManifest, resolvedBase);
|
|
5857
6028
|
}
|
|
5858
6029
|
};
|
|
5859
6030
|
}
|
|
@@ -6747,13 +6918,21 @@ function timberServerBundle() {
|
|
|
6747
6918
|
* timber-cache-transform — Vite plugin that injects stable callsite IDs
|
|
6748
6919
|
* into `cache()` calls imported from `@timber-js/app/cache`.
|
|
6749
6920
|
*
|
|
6750
|
-
*
|
|
6751
|
-
*
|
|
6752
|
-
*
|
|
6753
|
-
*
|
|
6921
|
+
* Detected callsite forms (TIM-1054):
|
|
6922
|
+
* - named imports, including aliases: `import { cache as c }` + `c(fn, opts)`
|
|
6923
|
+
* - namespace imports: `import * as ns` + `ns.cache(fn, opts)`
|
|
6924
|
+
* Forms a per-module transform cannot trace — local variable aliases
|
|
6925
|
+
* (`const c = cache`), user re-export wrapper modules, computed access
|
|
6926
|
+
* (`ns['cache']`) — fall back to the runtime fnId derived from
|
|
6927
|
+
* fnv1a(fn.toString()) (see cache/timber-cache.ts), which is deterministic
|
|
6928
|
+
* across instances and cold starts of the same build. Without either
|
|
6929
|
+
* mechanism, an ordinal counter would collide across instances with a shared
|
|
6930
|
+
* CacheHandler (Redis/KV) — cache key poisoning, serving one route's cached
|
|
6931
|
+
* data to another.
|
|
6754
6932
|
*
|
|
6755
6933
|
* The plugin transforms:
|
|
6756
|
-
* cache(fn, opts)
|
|
6934
|
+
* cache(fn, opts) → cache(fn, opts, "stable-id")
|
|
6935
|
+
* ns.cache(fn, opts) → ns.cache(fn, opts, "stable-id")
|
|
6757
6936
|
*
|
|
6758
6937
|
* where stable-id =
|
|
6759
6938
|
* fnv1a(relPath + ":cache:" + fnv1a(stmtText) + ":" + fnv1a(callSpanText) + ":" + occurrence)
|
|
@@ -6788,20 +6967,43 @@ function timberServerBundle() {
|
|
|
6788
6967
|
*/
|
|
6789
6968
|
var CACHE_MODULE_SOURCES = new Set(["@timber-js/app/cache", "timber/cache"]);
|
|
6790
6969
|
/**
|
|
6791
|
-
* Find the local binding name(s) for `cache` imported from the cache module
|
|
6970
|
+
* Find the local binding name(s) for `cache` imported from the cache module —
|
|
6971
|
+
* both named imports and namespace imports (TIM-1054).
|
|
6792
6972
|
*/
|
|
6793
6973
|
function findCacheBindings(program) {
|
|
6794
|
-
const
|
|
6974
|
+
const named = /* @__PURE__ */ new Set();
|
|
6975
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
6795
6976
|
for (const stmt of program.body) {
|
|
6796
6977
|
if (stmt.type !== "ImportDeclaration") continue;
|
|
6797
6978
|
const decl = stmt;
|
|
6798
6979
|
if (!CACHE_MODULE_SOURCES.has(decl.source.value)) continue;
|
|
6799
6980
|
for (const spec of decl.specifiers) {
|
|
6981
|
+
if (spec.type === "ImportNamespaceSpecifier") {
|
|
6982
|
+
namespaces.add(spec.local.name);
|
|
6983
|
+
continue;
|
|
6984
|
+
}
|
|
6800
6985
|
if (spec.type !== "ImportSpecifier") continue;
|
|
6801
|
-
if ((spec.imported?.name ?? spec.local.name) === "cache")
|
|
6986
|
+
if ((spec.imported?.name ?? spec.local.name) === "cache") named.add(spec.local.name);
|
|
6802
6987
|
}
|
|
6803
6988
|
}
|
|
6804
|
-
return
|
|
6989
|
+
return {
|
|
6990
|
+
named,
|
|
6991
|
+
namespaces
|
|
6992
|
+
};
|
|
6993
|
+
}
|
|
6994
|
+
/**
|
|
6995
|
+
* Is this callee a reference to the imported `cache` function — either a bare
|
|
6996
|
+
* identifier bound by a named import, or `ns.cache` on a namespace import?
|
|
6997
|
+
* Computed access (`ns['cache']`) is deliberately not matched; it falls back
|
|
6998
|
+
* to the runtime fnId derivation.
|
|
6999
|
+
*/
|
|
7000
|
+
function isCacheCallee(callee, bindings) {
|
|
7001
|
+
if (callee.type === "Identifier") return bindings.named.has(callee.name);
|
|
7002
|
+
if (callee.type === "MemberExpression") {
|
|
7003
|
+
const member = callee;
|
|
7004
|
+
return !member.computed && member.object.type === "Identifier" && bindings.namespaces.has(member.object.name) && member.property.type === "Identifier" && member.property.name === "cache";
|
|
7005
|
+
}
|
|
7006
|
+
return false;
|
|
6805
7007
|
}
|
|
6806
7008
|
/**
|
|
6807
7009
|
* Recursively walk AST nodes to find cache() call expressions.
|
|
@@ -6812,7 +7014,7 @@ function findCacheBindings(program) {
|
|
|
6812
7014
|
function findCacheCallSites(node, cacheBindings, results) {
|
|
6813
7015
|
if (node.type === "CallExpression") {
|
|
6814
7016
|
const call = node;
|
|
6815
|
-
if (call.
|
|
7017
|
+
if (call.arguments.length === 2 && isCacheCallee(call.callee, cacheBindings)) results.push(call);
|
|
6816
7018
|
}
|
|
6817
7019
|
for (const key of Object.keys(node)) {
|
|
6818
7020
|
if (key === "type" || key === "start" || key === "end") continue;
|
|
@@ -6856,7 +7058,7 @@ function timberCacheTransform(ctx) {
|
|
|
6856
7058
|
return;
|
|
6857
7059
|
}
|
|
6858
7060
|
const cacheBindings = findCacheBindings(program);
|
|
6859
|
-
if (cacheBindings.size === 0) return;
|
|
7061
|
+
if (cacheBindings.named.size === 0 && cacheBindings.namespaces.size === 0) return;
|
|
6860
7062
|
const callSites = [];
|
|
6861
7063
|
for (const stmt of program.body) {
|
|
6862
7064
|
const calls = [];
|
|
@@ -7750,14 +7952,15 @@ function timber(config) {
|
|
|
7750
7952
|
ctx.timer.start("config-load");
|
|
7751
7953
|
const fileConfig = loadTimberConfigFile(viteRoot);
|
|
7752
7954
|
if (fileConfig) {
|
|
7753
|
-
mergeFileConfig(ctx, fileConfig);
|
|
7955
|
+
ctx.config = mergeFileConfig(ctx.config, fileConfig);
|
|
7754
7956
|
ctx.clientJavascript = resolveClientJavascript(ctx.config);
|
|
7755
7957
|
}
|
|
7756
7958
|
ctx.config.output ??= "server";
|
|
7757
7959
|
ctx.timer.end("config-load");
|
|
7758
7960
|
const viteOutDir = userConfig.build?.outDir;
|
|
7759
7961
|
const timberBuildDir = ctx.config.buildDir;
|
|
7760
|
-
|
|
7962
|
+
const resolvedRoot = resolve(userConfig.root ?? process.cwd());
|
|
7963
|
+
ctx.buildDir = resolveBuildDir(resolvedRoot, timberBuildDir, viteOutDir);
|
|
7761
7964
|
if (viteRoot !== process.cwd()) {
|
|
7762
7965
|
const hasCompilerInReloaded = !config?.reactCompiler && ctx.config.reactCompiler;
|
|
7763
7966
|
const hadCompilerInEarly = !config?.reactCompiler && earlyFileConfig?.reactCompiler;
|
|
@@ -7784,7 +7987,7 @@ function timber(config) {
|
|
|
7784
7987
|
resolvedDevPort = start.port;
|
|
7785
7988
|
resolvedDevPortExplicit = start.explicit;
|
|
7786
7989
|
}
|
|
7787
|
-
const buildOutDir =
|
|
7990
|
+
const buildOutDir = relative(resolvedRoot, ctx.buildDir);
|
|
7788
7991
|
const envOutDirs = {};
|
|
7789
7992
|
for (const envName of [
|
|
7790
7993
|
"rsc",
|