@timber-js/app 0.2.0-alpha.147 → 0.2.0-alpha.149
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/LICENSE +8 -0
- 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-CQaxupHv.js → cli-schema-sync-DJDdyEae.js} +111 -260
- 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-Ck4Y45XN.js → walkers-DvWDj453.js} +107 -82
- 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/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 +610 -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/plugins/server-bundle.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-renderer.d.ts +1 -1
- package/dist/server/deny-renderer.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 +12 -171
- 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/shared/href-validation.d.ts.map +1 -1
- package/dist/shims/image.d.ts +15 -15
- package/package.json +7 -8
- 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/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/plugins/server-bundle.ts +1 -0
- package/src/routing/codegen.ts +3 -2
- 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 +5 -17
- 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-renderer.ts +6 -0
- 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/rsc-entry/error-renderer.ts +1 -0
- 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/shared/href-validation.ts +8 -0
- package/dist/_chunks/cli-schema-sync-CBiMEtqa.js +0 -2
- package/dist/_chunks/cli-schema-sync-CQaxupHv.js.map +0 -1
- 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-Ck4Y45XN.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/src/fonts/local.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { resolve, dirname, extname } from 'node:path';
|
|
|
17
17
|
import type { LocalFontConfig, LocalFontSrc, FontFaceDescriptor, ExtractedFont } from './types.js';
|
|
18
18
|
import { buildFontStack } from './fallbacks.js';
|
|
19
19
|
import { extractLocalFontConfigAst } from './ast.js';
|
|
20
|
+
import { fontOutputBasename, fontUrlPrefix } from './paths.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Infer the font format from a file extension.
|
|
@@ -85,7 +86,7 @@ export function resolveLocalFontPaths(
|
|
|
85
86
|
export function generateFamilyName(sources: LocalFontSrc[]): string {
|
|
86
87
|
if (sources.length === 0) return 'Local Font';
|
|
87
88
|
const firstPath = sources[0].path;
|
|
88
|
-
const basename = firstPath
|
|
89
|
+
const basename = fontOutputBasename(firstPath);
|
|
89
90
|
// Remove extension and weight/style suffixes for a cleaner family name
|
|
90
91
|
const stem = basename.replace(/\.[^.]+$/, '');
|
|
91
92
|
// Strip common weight/style suffixes to get the family root
|
|
@@ -100,19 +101,19 @@ export function generateFamilyName(sources: LocalFontSrc[]): string {
|
|
|
100
101
|
* Generate @font-face descriptors for local font sources.
|
|
101
102
|
*
|
|
102
103
|
* Each source entry produces one @font-face rule. The `src` descriptor
|
|
103
|
-
* uses a `url()` pointing to the served path under
|
|
104
|
-
* The `urlPrefix` defaults to
|
|
105
|
-
*
|
|
104
|
+
* uses a `url()` pointing to the served path under `<base>_timber/fonts/`.
|
|
105
|
+
* The `urlPrefix` defaults to `fontUrlPrefix()` (root base) — pass
|
|
106
|
+
* `fontUrlPrefix(viteBase)` so non-root deployments resolve correctly.
|
|
106
107
|
*/
|
|
107
108
|
export function generateLocalFontFaces(
|
|
108
109
|
family: string,
|
|
109
110
|
sources: LocalFontSrc[],
|
|
110
111
|
display: string,
|
|
111
|
-
urlPrefix =
|
|
112
|
+
urlPrefix = fontUrlPrefix()
|
|
112
113
|
): FontFaceDescriptor[] {
|
|
113
114
|
return sources.map((entry) => {
|
|
114
115
|
const format = inferFontFormat(entry.path);
|
|
115
|
-
const filename = entry.hashedBasename ?? entry.path
|
|
116
|
+
const filename = entry.hashedBasename ?? fontOutputBasename(entry.path);
|
|
116
117
|
return {
|
|
117
118
|
family,
|
|
118
119
|
src: `url('${urlPrefix}/${filename}') format('${format}')`,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared path/URL helpers for the timber-fonts pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Two concerns live here:
|
|
5
|
+
*
|
|
6
|
+
* - `fontUrlPrefix` — the public URL prefix for font binaries, derived
|
|
7
|
+
* from Vite's resolved `base`. Every URL the pipeline emits (@font-face
|
|
8
|
+
* src, manifest hrefs, dev middleware matching) must go through this so
|
|
9
|
+
* apps deployed under a non-root base (`base: '/my-app/'`) resolve
|
|
10
|
+
* fonts correctly. Emitted *file* locations are not base-prefixed —
|
|
11
|
+
* only URLs are.
|
|
12
|
+
*
|
|
13
|
+
* - `fontOutputBasename` / `hashedFontBasename` — basename derivation for
|
|
14
|
+
* local font sources. Paths are absolutized with `resolve()`, which
|
|
15
|
+
* produces backslash separators on Windows, so `split('/').pop()` is
|
|
16
|
+
* not safe. `path.win32.basename` handles both separators.
|
|
17
|
+
*
|
|
18
|
+
* Design doc: 24-fonts.md
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { win32 } from 'node:path';
|
|
22
|
+
|
|
23
|
+
/** Directory (relative to the build output / URL root) holding font binaries. */
|
|
24
|
+
export const FONT_OUTPUT_DIR = '_timber/fonts';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Build the URL prefix for font binaries from Vite's resolved `base`.
|
|
28
|
+
*
|
|
29
|
+
* `fontUrlPrefix('/')` → `/_timber/fonts`
|
|
30
|
+
* `fontUrlPrefix('/my-app/')` → `/my-app/_timber/fonts`
|
|
31
|
+
*/
|
|
32
|
+
export function fontUrlPrefix(base = '/'): string {
|
|
33
|
+
const normalized = base.endsWith('/') ? base : `${base}/`;
|
|
34
|
+
return `${normalized}${FONT_OUTPUT_DIR}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derive the output basename for a local font source path.
|
|
39
|
+
*
|
|
40
|
+
* Uses `path.win32.basename` because it splits on both `/` and `\`,
|
|
41
|
+
* handling absolute paths produced by `resolve()` on any platform.
|
|
42
|
+
*/
|
|
43
|
+
export function fontOutputBasename(srcPath: string): string {
|
|
44
|
+
return win32.basename(srcPath);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Derive the content-hashed output basename for a local font source.
|
|
49
|
+
*
|
|
50
|
+
* `hashedFontBasename('C:\\fonts\\MyFont-Bold.woff2', 'a1b2c3d4')`
|
|
51
|
+
* → `MyFont-Bold-a1b2c3d4.woff2`
|
|
52
|
+
*/
|
|
53
|
+
export function hashedFontBasename(srcPath: string, hash: string): string {
|
|
54
|
+
const basename = fontOutputBasename(srcPath);
|
|
55
|
+
const ext = win32.extname(basename);
|
|
56
|
+
const stem = ext ? basename.slice(0, -ext.length) : basename;
|
|
57
|
+
return `${stem}-${hash}${ext}`;
|
|
58
|
+
}
|
package/src/fonts/pipeline.ts
CHANGED
|
@@ -27,27 +27,16 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import { readFileSync, existsSync } from 'node:fs';
|
|
30
|
-
import { resolve, normalize
|
|
30
|
+
import { resolve, normalize } from 'node:path';
|
|
31
31
|
import type { ExtractedFont, FontEntry, FontFaceDescriptor } from './types.js';
|
|
32
32
|
import type { CachedFont } from './google.js';
|
|
33
33
|
import { contentHash } from './google.js';
|
|
34
34
|
import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from './css.js';
|
|
35
35
|
import { generateFallbackCss } from './fallbacks.js';
|
|
36
36
|
import { generateLocalFontFaces } from './local.js';
|
|
37
|
+
import { hashedFontBasename, fontUrlPrefix } from './paths.js';
|
|
37
38
|
import { VirtualFontCssIdMap } from './virtual-modules.js';
|
|
38
39
|
|
|
39
|
-
/**
|
|
40
|
-
* Backwards-compatible alias for the registry shape that the standalone
|
|
41
|
-
* `pruneRegistryEntries` and `generateAllFontCss` helpers operate on.
|
|
42
|
-
*
|
|
43
|
-
* The `FontPipeline` itself stores `Map<string, FontEntry>` internally and
|
|
44
|
-
* never exposes its underlying map. Tests and external callers that build
|
|
45
|
-
* their own font registries (e.g. fixtures in `tests/fonts-hmr-prune.test.ts`)
|
|
46
|
-
* still pass plain `Map<string, ExtractedFont>` instances to the standalone
|
|
47
|
-
* helpers, which is why the alias survives.
|
|
48
|
-
*/
|
|
49
|
-
export type FontRegistry = Map<string, ExtractedFont>;
|
|
50
|
-
|
|
51
40
|
export class FontPipeline {
|
|
52
41
|
/**
|
|
53
42
|
* The single per-font store. Keyed by `FontEntry.id` (which is itself
|
|
@@ -62,10 +51,21 @@ export class FontPipeline {
|
|
|
62
51
|
/** When true, local font sources are content-hashed at registration time. */
|
|
63
52
|
private readonly _hashOnRegister: boolean;
|
|
64
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Vite's resolved `base`. Set from the plugin's `configResolved` hook so
|
|
56
|
+
* every @font-face URL the pipeline renders carries the prefix.
|
|
57
|
+
*/
|
|
58
|
+
private _base = '/';
|
|
59
|
+
|
|
65
60
|
constructor(opts?: { hashOnRegister?: boolean }) {
|
|
66
61
|
this._hashOnRegister = opts?.hashOnRegister ?? false;
|
|
67
62
|
}
|
|
68
63
|
|
|
64
|
+
/** Set Vite's resolved `base` (from `configResolved`). */
|
|
65
|
+
setBase(base: string): void {
|
|
66
|
+
this._base = base;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
69
|
/**
|
|
70
70
|
* Per-importer virtual CSS module id registry. Populated by the
|
|
71
71
|
* transform hook when it inlines a font function call, read by the
|
|
@@ -156,11 +156,7 @@ export class FontPipeline {
|
|
|
156
156
|
const absolutePath = normalize(resolve(src.path));
|
|
157
157
|
if (!existsSync(absolutePath)) continue;
|
|
158
158
|
const data = readFileSync(absolutePath);
|
|
159
|
-
|
|
160
|
-
const basename = src.path.split('/').pop() ?? src.path;
|
|
161
|
-
const ext = extname(basename);
|
|
162
|
-
const stem = ext ? basename.slice(0, -ext.length) : basename;
|
|
163
|
-
src.hashedBasename = `${stem}-${hash}${ext}`;
|
|
159
|
+
src.hashedBasename = hashedFontBasename(src.path, contentHash(data));
|
|
164
160
|
}
|
|
165
161
|
}
|
|
166
162
|
this._entries.set(entry.id, entry);
|
|
@@ -247,9 +243,10 @@ export class FontPipeline {
|
|
|
247
243
|
* or lazily in `load` for dev).
|
|
248
244
|
*/
|
|
249
245
|
getCss(): string {
|
|
246
|
+
const urlPrefix = fontUrlPrefix(this._base);
|
|
250
247
|
const cssParts: string[] = [];
|
|
251
248
|
for (const entry of this._entries.values()) {
|
|
252
|
-
cssParts.push(renderEntryCss(entry));
|
|
249
|
+
cssParts.push(renderEntryCss(entry, urlPrefix));
|
|
253
250
|
}
|
|
254
251
|
return cssParts.join('\n\n');
|
|
255
252
|
}
|
|
@@ -271,10 +268,11 @@ export class FontPipeline {
|
|
|
271
268
|
* pipeline handles it as a valid empty CSS module.
|
|
272
269
|
*/
|
|
273
270
|
getCssForSegment(importer: string): string {
|
|
271
|
+
const urlPrefix = fontUrlPrefix(this._base);
|
|
274
272
|
const cssParts: string[] = [];
|
|
275
273
|
for (const entry of this._entries.values()) {
|
|
276
274
|
if (entry.importer !== importer) continue;
|
|
277
|
-
cssParts.push(renderEntryCss(entry));
|
|
275
|
+
cssParts.push(renderEntryCss(entry, urlPrefix));
|
|
278
276
|
}
|
|
279
277
|
return cssParts.join('\n\n');
|
|
280
278
|
}
|
|
@@ -283,16 +281,19 @@ export class FontPipeline {
|
|
|
283
281
|
/**
|
|
284
282
|
* Render the combined CSS for a single font entry.
|
|
285
283
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
* of from a parallel map. Kept private to this module so the only public
|
|
289
|
-
* way to render a `FontEntry`'s CSS is through `FontPipeline.getCss()`.
|
|
284
|
+
* Reads `faces` straight off the entry. Private to this module — the only
|
|
285
|
+
* public way to render a `FontEntry`'s CSS is through `FontPipeline.getCss()`.
|
|
290
286
|
*/
|
|
291
|
-
function renderEntryCss(entry: FontEntry): string {
|
|
287
|
+
function renderEntryCss(entry: FontEntry, urlPrefix: string): string {
|
|
292
288
|
const cssParts: string[] = [];
|
|
293
289
|
|
|
294
290
|
if (entry.provider === 'local' && entry.localSources) {
|
|
295
|
-
const faces = generateLocalFontFaces(
|
|
291
|
+
const faces = generateLocalFontFaces(
|
|
292
|
+
entry.family,
|
|
293
|
+
entry.localSources,
|
|
294
|
+
entry.display,
|
|
295
|
+
urlPrefix
|
|
296
|
+
);
|
|
296
297
|
const faceCss = generateFontFaces(faces);
|
|
297
298
|
if (faceCss) cssParts.push(faceCss);
|
|
298
299
|
}
|
|
@@ -313,25 +314,3 @@ function renderEntryCss(entry: FontEntry): string {
|
|
|
313
314
|
|
|
314
315
|
return cssParts.join('\n\n');
|
|
315
316
|
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Drop every registry entry that originated from `importer` and shares
|
|
319
|
-
* `family`. Standalone helper retained as a public export for the test
|
|
320
|
-
* suite (`tests/fonts-hmr-prune.test.ts`) and any external callers that
|
|
321
|
-
* still build their own `FontRegistry` maps.
|
|
322
|
-
*
|
|
323
|
-
* Prefer `FontPipeline#pruneFor` inside the plugin itself — it also drops
|
|
324
|
-
* attached `faces` and `cachedFiles` in the same operation.
|
|
325
|
-
*/
|
|
326
|
-
export function pruneRegistryEntries(
|
|
327
|
-
registry: FontRegistry,
|
|
328
|
-
importer: string,
|
|
329
|
-
family: string
|
|
330
|
-
): void {
|
|
331
|
-
const familyKey = family.toLowerCase();
|
|
332
|
-
for (const [id, font] of registry) {
|
|
333
|
-
if (font.importer === importer && font.family.toLowerCase() === familyKey) {
|
|
334
|
-
registry.delete(id);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
}
|
package/src/fonts/transform.ts
CHANGED
|
@@ -221,6 +221,33 @@ export function runFontsTransform(
|
|
|
221
221
|
);
|
|
222
222
|
}
|
|
223
223
|
|
|
224
|
+
// Bail on namespace imports of font modules — the transform cannot
|
|
225
|
+
// rewrite `fonts.Inter(...)` member calls, so the font would silently
|
|
226
|
+
// never load.
|
|
227
|
+
if (analysis.namespaceImports.length > 0) {
|
|
228
|
+
const first = analysis.namespaceImports[0];
|
|
229
|
+
emitError(
|
|
230
|
+
`Namespace imports of font modules are not supported ` +
|
|
231
|
+
`(\`* as ${first.name}\` at line ${first.line}, column ${first.column}). ` +
|
|
232
|
+
`Use a named import instead: \`import { Inter } from 'next/font/google'\`.`
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Bail on font binding references the transform cannot rewrite
|
|
237
|
+
// (`export default Inter({...})`, object-literal nesting, member access
|
|
238
|
+
// on the call result, aliasing, re-exports). The import declaration is
|
|
239
|
+
// removed, so leaving these would be a silent runtime ReferenceError.
|
|
240
|
+
if (analysis.unsupportedUsages.length > 0) {
|
|
241
|
+
const first = analysis.unsupportedUsages[0];
|
|
242
|
+
emitError(
|
|
243
|
+
`Unsupported usage of font function \`${first.name}\` at ` +
|
|
244
|
+
`line ${first.line}, column ${first.column}. ` +
|
|
245
|
+
`A font call must be \`const x = Font({...})\` at module scope ` +
|
|
246
|
+
`(export const is fine). The call result cannot be nested in other ` +
|
|
247
|
+
`expressions, and the imported font function cannot be aliased or re-exported.`
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
224
251
|
if (analysis.callSites.length === 0 && analysis.bindings.length === 0) return null;
|
|
225
252
|
|
|
226
253
|
const s = new MagicString(code);
|
|
@@ -30,10 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import { createHash } from 'node:crypto';
|
|
33
|
-
import type { ExtractedFont
|
|
34
|
-
import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from './css.js';
|
|
35
|
-
import { generateFallbackCss } from './fallbacks.js';
|
|
36
|
-
import { generateLocalFontFaces } from './local.js';
|
|
33
|
+
import type { ExtractedFont } from './types.js';
|
|
37
34
|
|
|
38
35
|
export const VIRTUAL_GOOGLE = '@timber/fonts/google';
|
|
39
36
|
export const VIRTUAL_LOCAL = '@timber/fonts/local';
|
|
@@ -180,56 +177,3 @@ export function generateLocalVirtualModule(): string {
|
|
|
180
177
|
'}',
|
|
181
178
|
].join('\n');
|
|
182
179
|
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Generate combined CSS for a single extracted font.
|
|
186
|
-
*
|
|
187
|
-
* Includes `@font-face` rules (local or Google), the size-adjusted fallback
|
|
188
|
-
* `@font-face`, and the scoped class rule. For Google fonts the resolved
|
|
189
|
-
* `FontFaceDescriptor[]` must be passed in (from `generateProductionFontFaces`
|
|
190
|
-
* in production or `resolveDevFontFaces` in dev).
|
|
191
|
-
*/
|
|
192
|
-
export function generateFontCss(font: ExtractedFont, googleFaces?: FontFaceDescriptor[]): string {
|
|
193
|
-
const cssParts: string[] = [];
|
|
194
|
-
|
|
195
|
-
if (font.provider === 'local' && font.localSources) {
|
|
196
|
-
const faces = generateLocalFontFaces(font.family, font.localSources, font.display);
|
|
197
|
-
const faceCss = generateFontFaces(faces);
|
|
198
|
-
if (faceCss) cssParts.push(faceCss);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (font.provider === 'google' && googleFaces && googleFaces.length > 0) {
|
|
202
|
-
const faceCss = generateFontFaces(googleFaces);
|
|
203
|
-
if (faceCss) cssParts.push(faceCss);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
const fallbackCss = generateFallbackCss(font.family);
|
|
207
|
-
if (fallbackCss) cssParts.push(fallbackCss);
|
|
208
|
-
|
|
209
|
-
if (font.variable) {
|
|
210
|
-
cssParts.push(generateVariableClass(font.className, font.variable, font.fontFamily));
|
|
211
|
-
} else {
|
|
212
|
-
cssParts.push(generateFontFamilyClass(font.className, font.fontFamily));
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return cssParts.join('\n\n');
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* Generate the CSS output for all extracted fonts.
|
|
220
|
-
*
|
|
221
|
-
* Includes `@font-face` rules for local and Google fonts, fallback rules,
|
|
222
|
-
* and scoped classes. `googleFontFacesMap` provides pre-resolved
|
|
223
|
-
* `FontFaceDescriptor[]` for each Google font (keyed by `ExtractedFont.id`).
|
|
224
|
-
*/
|
|
225
|
-
export function generateAllFontCss(
|
|
226
|
-
registry: Map<string, ExtractedFont>,
|
|
227
|
-
googleFontFacesMap?: Map<string, FontFaceDescriptor[]>
|
|
228
|
-
): string {
|
|
229
|
-
const cssParts: string[] = [];
|
|
230
|
-
for (const font of registry.values()) {
|
|
231
|
-
const googleFaces = googleFontFacesMap?.get(font.id);
|
|
232
|
-
cssParts.push(generateFontCss(font, googleFaces));
|
|
233
|
-
}
|
|
234
|
-
return cssParts.join('\n\n');
|
|
235
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { Plugin, PluginOption } from 'vite';
|
|
14
|
-
import { join, resolve } from 'node:path';
|
|
14
|
+
import { join, relative, resolve } from 'node:path';
|
|
15
15
|
import { createRequire } from 'node:module';
|
|
16
16
|
import react, { reactCompilerPreset } from '@vitejs/plugin-react';
|
|
17
17
|
import { timberContent } from './plugins/content';
|
|
@@ -46,7 +46,6 @@ import {
|
|
|
46
46
|
resolveAppDir,
|
|
47
47
|
resolveClientJavascript,
|
|
48
48
|
resolveBuildDir,
|
|
49
|
-
DEFAULT_BUILD_DIR,
|
|
50
49
|
} from './plugin-context.js';
|
|
51
50
|
|
|
52
51
|
// ── Public API ────────────────────────────────────────────────────────────
|
|
@@ -251,7 +250,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
251
250
|
ctx.timer.start('config-load');
|
|
252
251
|
const fileConfig = loadTimberConfigFile(viteRoot);
|
|
253
252
|
if (fileConfig) {
|
|
254
|
-
mergeFileConfig(ctx, fileConfig);
|
|
253
|
+
ctx.config = mergeFileConfig(ctx.config, fileConfig);
|
|
255
254
|
ctx.clientJavascript = resolveClientJavascript(ctx.config);
|
|
256
255
|
}
|
|
257
256
|
// Apply defaults AFTER merge so file-based config isn't overridden
|
|
@@ -353,14 +352,9 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
353
352
|
resolvedDevPortExplicit = start.explicit;
|
|
354
353
|
}
|
|
355
354
|
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
const buildOutDir = timberBuildDir
|
|
360
|
-
? timberBuildDir
|
|
361
|
-
: viteOutDir && viteOutDir !== 'dist'
|
|
362
|
-
? viteOutDir
|
|
363
|
-
: DEFAULT_BUILD_DIR;
|
|
355
|
+
// Derive relative buildOutDir from the absolute ctx.buildDir (set above)
|
|
356
|
+
// so Vite's config receives a root-relative path.
|
|
357
|
+
const buildOutDir = relative(resolvedRoot, ctx.buildDir);
|
|
364
358
|
|
|
365
359
|
// Set per-environment build.outDir so Vite's RSC plugin writes
|
|
366
360
|
// each environment to the correct subdirectory under our buildDir.
|
|
@@ -10,39 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Codec } from '../codec.js';
|
|
13
|
-
import { isCodec,
|
|
14
|
-
import type { StandardSchemaV1 } from '../schema-bridge.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Wrap a Standard Schema into a Codec for route params.
|
|
18
|
-
* Parse throws on failure (invalid param → 404). Serialize returns string.
|
|
19
|
-
*/
|
|
20
|
-
function fromParamSchema<T>(fieldName: string, schema: StandardSchemaV1<T>): Codec<T> {
|
|
21
|
-
return {
|
|
22
|
-
parse(value: string | string[] | undefined): T {
|
|
23
|
-
const result = validateSync(schema, value);
|
|
24
|
-
if (!result.issues) {
|
|
25
|
-
return result.value;
|
|
26
|
-
}
|
|
27
|
-
const messages = result.issues.map((i) => i.message).join(', ');
|
|
28
|
-
throw new Error(`[timber] Param '${fieldName}' coercion failed: ${messages}`);
|
|
29
|
-
},
|
|
30
|
-
serialize(value: T): string | null {
|
|
31
|
-
if (value === null || value === undefined) return null;
|
|
32
|
-
if (Array.isArray(value)) return value.join('/');
|
|
33
|
-
return String(value);
|
|
34
|
-
},
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Resolve a schema entry to a Codec. Accepts Codec<T> or StandardSchemaV1<T>.
|
|
40
|
-
*/
|
|
41
|
-
function resolveCodec(fieldName: string, value: unknown): Codec<unknown> {
|
|
42
|
-
if (isCodec(value)) return value;
|
|
43
|
-
if (isStandardSchema(value)) return fromParamSchema(fieldName, value);
|
|
44
|
-
throw new Error(`[timber] Schema field '${fieldName}' is not a valid codec or Standard Schema.`);
|
|
45
|
-
}
|
|
13
|
+
import { isCodec, resolveCodecOrSchema } from '../schema-bridge.js';
|
|
46
14
|
|
|
47
15
|
/**
|
|
48
16
|
* Reconstruct the bracket key for a segment from its type and param name.
|
|
@@ -109,7 +77,7 @@ export function resolveSchemaCodecs(
|
|
|
109
77
|
`(e.g. codec.optionalCatchAll(codec.string)). Got a non-optional catch-all codec.`
|
|
110
78
|
);
|
|
111
79
|
}
|
|
112
|
-
resolved[bracketName] =
|
|
80
|
+
resolved[bracketName] = resolveCodecOrSchema(bracketName, value, 'param');
|
|
113
81
|
}
|
|
114
82
|
|
|
115
83
|
return resolved;
|
package/src/plugin-context.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { existsSync } from 'node:fs';
|
|
12
|
-
import { join } from 'node:path';
|
|
12
|
+
import { join, resolve } from 'node:path';
|
|
13
13
|
import { createRequire } from 'node:module';
|
|
14
14
|
import type { RouteTree } from './routing/types';
|
|
15
15
|
import type { BuildManifest } from './server/build-manifest';
|
|
@@ -168,22 +168,73 @@ export function resolveBuildDir(
|
|
|
168
168
|
viteBuildOutDir?: string
|
|
169
169
|
): string {
|
|
170
170
|
if (timberBuildDir) {
|
|
171
|
-
return
|
|
171
|
+
return resolve(root, timberBuildDir);
|
|
172
172
|
}
|
|
173
173
|
if (viteBuildOutDir && viteBuildOutDir !== 'dist') {
|
|
174
|
-
return
|
|
174
|
+
return resolve(root, viteBuildOutDir);
|
|
175
175
|
}
|
|
176
176
|
return join(root, DEFAULT_BUILD_DIR);
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
// ── Config file loading ───────────────────────────────────────────────────
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Map known timber.config load failures to actionable hints (TIM-1040).
|
|
183
|
+
* Returns null when the error has no known remedy beyond its own message.
|
|
184
|
+
*
|
|
185
|
+
* Known failure modes of loading timber.config.ts via native require():
|
|
186
|
+
* - Node < 22.18 has no default type stripping → ERR_UNKNOWN_FILE_EXTENSION
|
|
187
|
+
* - Non-erasable TS syntax (enums, namespaces, parameter properties)
|
|
188
|
+
* → ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX / ERR_INVALID_TYPESCRIPT_SYNTAX
|
|
189
|
+
* - ESM syntax in a `"type": "commonjs"` package → the stripped .ts is
|
|
190
|
+
* parsed as CJS and throws SyntaxError: "Unexpected token 'export'"
|
|
191
|
+
* (export statements) or "Cannot use import statement outside a
|
|
192
|
+
* module" (import statements)
|
|
193
|
+
*/
|
|
194
|
+
export function configLoadHint(configName: string, err: unknown): string | null {
|
|
195
|
+
const code = (err as NodeJS.ErrnoException | null)?.code;
|
|
196
|
+
const isTs = configName.endsWith('.ts');
|
|
197
|
+
|
|
198
|
+
if (isTs && code === 'ERR_UNKNOWN_FILE_EXTENSION') {
|
|
199
|
+
return (
|
|
200
|
+
'Loading a TypeScript config requires Node.js 22.18+ (built-in type stripping). ' +
|
|
201
|
+
'Upgrade Node.js or rewrite the config as timber.config.js.'
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
if (code === 'ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX' || code === 'ERR_INVALID_TYPESCRIPT_SYNTAX') {
|
|
205
|
+
return (
|
|
206
|
+
'Node.js loads the config with type stripping, which only supports erasable TypeScript syntax — ' +
|
|
207
|
+
'replace enums, namespaces, and constructor parameter properties with plain objects and types.'
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
if (
|
|
211
|
+
isTs &&
|
|
212
|
+
err instanceof SyntaxError &&
|
|
213
|
+
/Unexpected token '(export|import)'|Cannot use import statement outside a module/.test(
|
|
214
|
+
err.message
|
|
215
|
+
)
|
|
216
|
+
) {
|
|
217
|
+
return (
|
|
218
|
+
'The config uses ESM syntax but Node.js parsed it as CommonJS — ' +
|
|
219
|
+
'set "type": "module" in package.json, or use module.exports instead of export default.'
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
|
|
181
225
|
/**
|
|
182
226
|
* Load timber.config.ts (or .js, .mjs) from the project root.
|
|
183
227
|
* Returns the config object or null if no config file is found.
|
|
228
|
+
* Throws a descriptive error naming the file when a config file exists
|
|
229
|
+
* but fails to load — a broken config must fail loudly, never silently
|
|
230
|
+
* degrade (see TIM-1066). Known failure modes get an actionable hint
|
|
231
|
+
* appended (see configLoadHint, TIM-1040).
|
|
184
232
|
*
|
|
185
|
-
* Uses require()
|
|
186
|
-
*
|
|
233
|
+
* Uses require() — Node 22.12+ can require() ESM modules, and Node
|
|
234
|
+
* 22.18+ strips types from .ts files by default (hence the engines
|
|
235
|
+
* field requiring >=22.18.0). This keeps timber() synchronous — no
|
|
236
|
+
* async config loading needed.
|
|
237
|
+
* This is the single config loader — dev, build, and preview all use it.
|
|
187
238
|
*/
|
|
188
239
|
export function loadTimberConfigFile(root: string): TimberUserConfig | null {
|
|
189
240
|
const configNames = ['timber.config.ts', 'timber.config.js', 'timber.config.mjs'];
|
|
@@ -192,8 +243,17 @@ export function loadTimberConfigFile(root: string): TimberUserConfig | null {
|
|
|
192
243
|
for (const name of configNames) {
|
|
193
244
|
const configPath = join(root, name);
|
|
194
245
|
if (existsSync(configPath)) {
|
|
195
|
-
|
|
196
|
-
|
|
246
|
+
try {
|
|
247
|
+
const mod = req(configPath);
|
|
248
|
+
return (mod.default ?? mod) as TimberUserConfig;
|
|
249
|
+
} catch (err) {
|
|
250
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
251
|
+
const hint = configLoadHint(name, err);
|
|
252
|
+
throw new Error(
|
|
253
|
+
`[timber] Failed to load ${configPath}: ${message}${hint ? `\n${hint}` : ''}`,
|
|
254
|
+
{ cause: err }
|
|
255
|
+
);
|
|
256
|
+
}
|
|
197
257
|
}
|
|
198
258
|
}
|
|
199
259
|
return null;
|
|
@@ -228,20 +288,38 @@ export function warnConfigConflicts(
|
|
|
228
288
|
}
|
|
229
289
|
|
|
230
290
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
291
|
+
* Drop own keys whose value is `undefined` so they don't clobber file
|
|
292
|
+
* config during spread merging. `timber({ buildDir: process.env.BUILD_DIR })`
|
|
293
|
+
* with the var unset must not erase the timber.config.ts value.
|
|
294
|
+
*/
|
|
295
|
+
function stripUndefined<T extends object>(obj: T): T {
|
|
296
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined)) as T;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Merge file-based config (timber.config.ts) with inline config (vite.config.ts).
|
|
301
|
+
* Inline config takes precedence — file config only fills in missing fields.
|
|
302
|
+
* Inline keys explicitly set to `undefined` are treated as absent, matching
|
|
303
|
+
* the `!== undefined` check in warnConfigConflicts.
|
|
233
304
|
*/
|
|
234
|
-
export function mergeFileConfig(
|
|
235
|
-
|
|
305
|
+
export function mergeFileConfig(
|
|
306
|
+
inline: TimberUserConfig,
|
|
307
|
+
fileConfig: TimberUserConfig
|
|
308
|
+
): TimberUserConfig {
|
|
236
309
|
warnConfigConflicts(inline, fileConfig);
|
|
310
|
+
const definedInline = stripUndefined(inline);
|
|
237
311
|
|
|
238
|
-
|
|
312
|
+
return {
|
|
239
313
|
...fileConfig,
|
|
240
|
-
...
|
|
241
|
-
...(fileConfig.limits &&
|
|
242
|
-
? { limits: { ...fileConfig.limits, ...
|
|
314
|
+
...definedInline,
|
|
315
|
+
...(fileConfig.limits && definedInline.limits
|
|
316
|
+
? { limits: { ...fileConfig.limits, ...stripUndefined(definedInline.limits) } }
|
|
317
|
+
: {}),
|
|
318
|
+
...(fileConfig.dev && definedInline.dev
|
|
319
|
+
? { dev: { ...fileConfig.dev, ...stripUndefined(definedInline.dev) } }
|
|
320
|
+
: {}),
|
|
321
|
+
...(fileConfig.mdx && definedInline.mdx
|
|
322
|
+
? { mdx: { ...fileConfig.mdx, ...stripUndefined(definedInline.mdx) } }
|
|
243
323
|
: {}),
|
|
244
|
-
...(fileConfig.dev && inline.dev ? { dev: { ...fileConfig.dev, ...inline.dev } } : {}),
|
|
245
|
-
...(fileConfig.mdx && inline.mdx ? { mdx: { ...fileConfig.mdx, ...inline.mdx } } : {}),
|
|
246
324
|
};
|
|
247
325
|
}
|