@timber-js/app 0.2.0-alpha.139 → 0.2.0-alpha.140
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/fonts/bundle.d.ts +5 -5
- package/dist/fonts/bundle.d.ts.map +1 -1
- package/dist/fonts/pipeline.d.ts +5 -0
- package/dist/fonts/pipeline.d.ts.map +1 -1
- package/dist/fonts/types.d.ts +2 -0
- package/dist/fonts/types.d.ts.map +1 -1
- package/dist/index.js +111 -33
- package/dist/index.js.map +1 -1
- package/dist/plugins/client-chunks.d.ts +8 -1
- package/dist/plugins/client-chunks.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/fonts/bundle.ts +12 -23
- package/src/fonts/local.ts +2 -2
- package/src/fonts/pipeline.ts +24 -1
- package/src/fonts/types.ts +2 -0
- package/src/index.ts +1 -1
- package/src/plugins/client-chunks.ts +69 -21
- package/src/plugins/fonts.ts +9 -8
|
@@ -20,6 +20,13 @@
|
|
|
20
20
|
* - `facade:app/layout.tsx` (root layout) → `"client-shared"`
|
|
21
21
|
* - `shared:...` (shared across chunks) → `"client-shared"`
|
|
22
22
|
*
|
|
23
|
+
* When a 'use client' module lives outside the app directory (e.g. in
|
|
24
|
+
* src/posts/ or src/components/), the function falls back to deriving
|
|
25
|
+
* the layout boundary from the serverChunk's facade path. This prevents
|
|
26
|
+
* lazily-loaded client dependencies (e.g. via import.meta.glob) from
|
|
27
|
+
* being hoisted into the shared chunk when they're only used by a
|
|
28
|
+
* specific route.
|
|
29
|
+
*
|
|
23
30
|
* This balances route-scoped code splitting with HTTP request count:
|
|
24
31
|
* fewer chunks than per-serverChunk, but still avoids downloading every
|
|
25
32
|
* client component on every page.
|
|
@@ -28,5 +35,5 @@ export declare function clientChunkGroup(meta: {
|
|
|
28
35
|
id: string;
|
|
29
36
|
normalizedId: string;
|
|
30
37
|
serverChunk: string;
|
|
31
|
-
}, appDir: string): string;
|
|
38
|
+
}, appDir: string, root: string): string;
|
|
32
39
|
//# sourceMappingURL=client-chunks.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client-chunks.d.ts","sourceRoot":"","sources":["../../src/plugins/client-chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;
|
|
1
|
+
{"version":3,"file":"client-chunks.d.ts","sourceRoot":"","sources":["../../src/plugins/client-chunks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAoCH;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAC/D,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,GACX,MAAM,CAwCR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../src/plugins/fonts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA0B1D,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC7F,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAElF;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"fonts.d.ts","sourceRoot":"","sources":["../../src/plugins/fonts.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAClD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AA0B1D,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,KAAK,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAC7F,OAAO,EACL,cAAc,EACd,iBAAiB,EACjB,qBAAqB,EACrB,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,6BAA6B,CAAC;AAElF;;GAEG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAuLtD"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timber-js/app",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.140",
|
|
4
4
|
"description": "Vite-native React framework built for Servers and Serverless Platforms — correct HTTP semantics, real status codes, pages that work without JavaScript",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare-workers",
|
|
@@ -114,11 +114,6 @@
|
|
|
114
114
|
"publishConfig": {
|
|
115
115
|
"access": "public"
|
|
116
116
|
},
|
|
117
|
-
"scripts": {
|
|
118
|
-
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
119
|
-
"typecheck": "tsgo --noEmit",
|
|
120
|
-
"prepublishOnly": "pnpm run build"
|
|
121
|
-
},
|
|
122
117
|
"dependencies": {
|
|
123
118
|
"@opentelemetry/api": "^1.9.1",
|
|
124
119
|
"@opentelemetry/context-async-hooks": "^2.7.1",
|
|
@@ -162,5 +157,9 @@
|
|
|
162
157
|
},
|
|
163
158
|
"engines": {
|
|
164
159
|
"node": ">=22.12.0"
|
|
160
|
+
},
|
|
161
|
+
"scripts": {
|
|
162
|
+
"build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
|
|
163
|
+
"typecheck": "tsgo --noEmit"
|
|
165
164
|
}
|
|
166
|
-
}
|
|
165
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/fonts/bundle.ts
CHANGED
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFileSync, existsSync } from 'node:fs';
|
|
13
|
-
import { resolve, normalize } from 'node:path';
|
|
13
|
+
import { resolve, normalize, extname } from 'node:path';
|
|
14
14
|
import type { ManifestFontEntry, BuildManifest } from '../server/build-manifest.js';
|
|
15
|
+
import { contentHash } from './google.js';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Minimal shape of the asset descriptor accepted by Rollup's `emitFile`.
|
|
@@ -67,7 +68,7 @@ export function emitFontAssets(
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
// Local font files (
|
|
71
|
+
// Local font files (content-hashed for immutable caching)
|
|
71
72
|
for (const font of pipeline.fonts()) {
|
|
72
73
|
if (font.provider !== 'local' || !font.localSources) continue;
|
|
73
74
|
for (const src of font.localSources) {
|
|
@@ -76,11 +77,11 @@ export function emitFontAssets(
|
|
|
76
77
|
warn(`Local font file not found: ${absolutePath}`);
|
|
77
78
|
continue;
|
|
78
79
|
}
|
|
79
|
-
const
|
|
80
|
+
const filename = src.hashedBasename ?? src.path.split('/').pop() ?? src.path;
|
|
80
81
|
const data = readFileSync(absolutePath);
|
|
81
82
|
emitFile({
|
|
82
83
|
type: 'asset',
|
|
83
|
-
fileName: `_timber/fonts/${
|
|
84
|
+
fileName: `_timber/fonts/${filename}`,
|
|
84
85
|
source: data,
|
|
85
86
|
});
|
|
86
87
|
}
|
|
@@ -89,17 +90,13 @@ export function emitFontAssets(
|
|
|
89
90
|
|
|
90
91
|
/**
|
|
91
92
|
* Populate `buildManifest.fonts` with `ManifestFontEntry[]` keyed by the
|
|
92
|
-
* importer
|
|
93
|
-
* `
|
|
93
|
+
* absolute importer path. This matches the runtime route manifest's
|
|
94
|
+
* `filePath` values (which are absolute) and is consistent with how
|
|
95
|
+
* `buildManifest.css` keys work after TIM-1014.
|
|
94
96
|
*
|
|
95
|
-
*
|
|
96
|
-
* `buildStart`. Local fonts use the source basename.
|
|
97
|
+
* All font binaries use content-hashed filenames for immutable caching.
|
|
97
98
|
*/
|
|
98
|
-
export function writeFontManifest(
|
|
99
|
-
pipeline: FontPipeline,
|
|
100
|
-
buildManifest: BuildManifest,
|
|
101
|
-
rootDir: string
|
|
102
|
-
): void {
|
|
99
|
+
export function writeFontManifest(pipeline: FontPipeline, buildManifest: BuildManifest): void {
|
|
103
100
|
const fontsByImporter = new Map<string, ManifestFontEntry[]>();
|
|
104
101
|
|
|
105
102
|
for (const entry of pipeline.entries()) {
|
|
@@ -107,7 +104,7 @@ export function writeFontManifest(
|
|
|
107
104
|
|
|
108
105
|
if (entry.provider === 'local' && entry.localSources) {
|
|
109
106
|
for (const src of entry.localSources) {
|
|
110
|
-
const filename = src.path.split('/').pop() ?? src.path;
|
|
107
|
+
const filename = src.hashedBasename ?? src.path.split('/').pop() ?? src.path;
|
|
111
108
|
const format = inferFontFormat(src.path);
|
|
112
109
|
manifestEntries.push({
|
|
113
110
|
href: `/_timber/fonts/${filename}`,
|
|
@@ -116,9 +113,6 @@ export function writeFontManifest(
|
|
|
116
113
|
});
|
|
117
114
|
}
|
|
118
115
|
} else if (entry.cachedFiles) {
|
|
119
|
-
// Google fonts: use the per-entry cached files attached during
|
|
120
|
-
// buildStart. Each entry owns its own cached binaries (TIM-829),
|
|
121
|
-
// so we no longer need a family-grouped lookup.
|
|
122
116
|
for (const cf of entry.cachedFiles) {
|
|
123
117
|
manifestEntries.push({
|
|
124
118
|
href: `/_timber/fonts/${cf.hashedFilename}`,
|
|
@@ -131,12 +125,7 @@ export function writeFontManifest(
|
|
|
131
125
|
fontsByImporter.set(entry.importer, manifestEntries);
|
|
132
126
|
}
|
|
133
127
|
|
|
134
|
-
// Normalize importer paths to be relative to project root (matching how
|
|
135
|
-
// Vite's manifest.json keys work for css/js).
|
|
136
128
|
for (const [importer, entries] of fontsByImporter) {
|
|
137
|
-
|
|
138
|
-
? importer.slice(rootDir.length + 1)
|
|
139
|
-
: importer;
|
|
140
|
-
buildManifest.fonts[relativePath] = entries;
|
|
129
|
+
buildManifest.fonts[importer] = entries;
|
|
141
130
|
}
|
|
142
131
|
}
|
package/src/fonts/local.ts
CHANGED
|
@@ -112,10 +112,10 @@ export function generateLocalFontFaces(
|
|
|
112
112
|
): FontFaceDescriptor[] {
|
|
113
113
|
return sources.map((entry) => {
|
|
114
114
|
const format = inferFontFormat(entry.path);
|
|
115
|
-
const
|
|
115
|
+
const filename = entry.hashedBasename ?? entry.path.split('/').pop() ?? entry.path;
|
|
116
116
|
return {
|
|
117
117
|
family,
|
|
118
|
-
src: `url('${urlPrefix}/${
|
|
118
|
+
src: `url('${urlPrefix}/${filename}') format('${format}')`,
|
|
119
119
|
weight: entry.weight,
|
|
120
120
|
style: entry.style,
|
|
121
121
|
display,
|
package/src/fonts/pipeline.ts
CHANGED
|
@@ -26,8 +26,11 @@
|
|
|
26
26
|
* Design doc: 24-fonts.md
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
30
|
+
import { resolve, normalize, extname } from 'node:path';
|
|
29
31
|
import type { ExtractedFont, FontEntry, FontFaceDescriptor } from './types.js';
|
|
30
32
|
import type { CachedFont } from './google.js';
|
|
33
|
+
import { contentHash } from './google.js';
|
|
31
34
|
import { generateVariableClass, generateFontFamilyClass, generateFontFaces } from './css.js';
|
|
32
35
|
import { generateFallbackCss } from './fallbacks.js';
|
|
33
36
|
import { generateLocalFontFaces } from './local.js';
|
|
@@ -56,6 +59,13 @@ export class FontPipeline {
|
|
|
56
59
|
*/
|
|
57
60
|
private readonly _entries = new Map<string, FontEntry>();
|
|
58
61
|
|
|
62
|
+
/** When true, local font sources are content-hashed at registration time. */
|
|
63
|
+
private readonly _hashOnRegister: boolean;
|
|
64
|
+
|
|
65
|
+
constructor(opts?: { hashOnRegister?: boolean }) {
|
|
66
|
+
this._hashOnRegister = opts?.hashOnRegister ?? false;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
/**
|
|
60
70
|
* Per-importer virtual CSS module id registry. Populated by the
|
|
61
71
|
* transform hook when it inlines a font function call, read by the
|
|
@@ -140,7 +150,20 @@ export class FontPipeline {
|
|
|
140
150
|
// Wholesale replace — never merge faces/cachedFiles from a previous
|
|
141
151
|
// registration. Re-registration with the same ID always represents the
|
|
142
152
|
// newest authoritative config and must start from a clean slate.
|
|
143
|
-
|
|
153
|
+
const entry: FontEntry = { ...extracted };
|
|
154
|
+
if (this._hashOnRegister && entry.provider === 'local' && entry.localSources) {
|
|
155
|
+
for (const src of entry.localSources) {
|
|
156
|
+
const absolutePath = normalize(resolve(src.path));
|
|
157
|
+
if (!existsSync(absolutePath)) continue;
|
|
158
|
+
const data = readFileSync(absolutePath);
|
|
159
|
+
const hash = contentHash(data);
|
|
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}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
this._entries.set(entry.id, entry);
|
|
144
167
|
}
|
|
145
168
|
|
|
146
169
|
/** Attach pre-resolved `@font-face` descriptors to a font entry. */
|
package/src/fonts/types.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface LocalFontSrc {
|
|
|
26
26
|
path: string;
|
|
27
27
|
weight?: string;
|
|
28
28
|
style?: string;
|
|
29
|
+
/** Content-hashed basename for production builds (e.g. `MyFont-Bold-a1b2c3d4.woff2`). */
|
|
30
|
+
hashedBasename?: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/** Configuration passed to `localFont()`. */
|
package/src/index.ts
CHANGED
|
@@ -157,7 +157,7 @@ function createRscOptions(
|
|
|
157
157
|
// Layout-boundary grouping is the middle ground: components under the same
|
|
158
158
|
// layout segment share a chunk. See design/27-chunking-strategy.md, TIM-440, TIM-499.
|
|
159
159
|
clientChunks: (meta: { id: string; normalizedId: string; serverChunk: string }) =>
|
|
160
|
-
clientChunkGroup(meta, ctx.appDir),
|
|
160
|
+
clientChunkGroup(meta, ctx.appDir, ctx.root),
|
|
161
161
|
};
|
|
162
162
|
|
|
163
163
|
// Bound args encryption — AES-256-GCM authenticated encryption for
|
|
@@ -10,6 +10,40 @@
|
|
|
10
10
|
* See design/27-chunking-strategy.md, TIM-440, TIM-499.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { relative } from 'node:path';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check whether a root-relative path lives under the app directory,
|
|
17
|
+
* and if so return the first non-group route segment.
|
|
18
|
+
*
|
|
19
|
+
* `appPrefix` is the root-relative app directory with a trailing slash
|
|
20
|
+
* (e.g. "app/" or "src/app/"), computed once from the resolved appDir
|
|
21
|
+
* and root. Matching uses `startsWith` — no ambiguity.
|
|
22
|
+
*
|
|
23
|
+
* Returns:
|
|
24
|
+
* { inApp: false } — path is not under app/
|
|
25
|
+
* { inApp: true, segment: null } — root of app/ (e.g. app/layout.tsx)
|
|
26
|
+
* { inApp: true, segment: "dashboard" } — nested route boundary
|
|
27
|
+
*/
|
|
28
|
+
function findAppBoundary(
|
|
29
|
+
path: string,
|
|
30
|
+
appPrefix: string
|
|
31
|
+
): { inApp: false } | { inApp: true; segment: string | null } {
|
|
32
|
+
const relPath = path.replace(/\\/g, '/');
|
|
33
|
+
if (!relPath.startsWith(appPrefix)) return { inApp: false };
|
|
34
|
+
|
|
35
|
+
const withinApp = relPath.slice(appPrefix.length);
|
|
36
|
+
const segments = withinApp.split('/');
|
|
37
|
+
|
|
38
|
+
for (const seg of segments) {
|
|
39
|
+
if (seg.includes('.')) break;
|
|
40
|
+
if (seg.startsWith('(') && seg.endsWith(')')) continue;
|
|
41
|
+
return { inApp: true, segment: seg };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { inApp: true, segment: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
13
47
|
/**
|
|
14
48
|
* Derive a chunk group name for a client reference module.
|
|
15
49
|
*
|
|
@@ -21,42 +55,56 @@
|
|
|
21
55
|
* - `facade:app/layout.tsx` (root layout) → `"client-shared"`
|
|
22
56
|
* - `shared:...` (shared across chunks) → `"client-shared"`
|
|
23
57
|
*
|
|
58
|
+
* When a 'use client' module lives outside the app directory (e.g. in
|
|
59
|
+
* src/posts/ or src/components/), the function falls back to deriving
|
|
60
|
+
* the layout boundary from the serverChunk's facade path. This prevents
|
|
61
|
+
* lazily-loaded client dependencies (e.g. via import.meta.glob) from
|
|
62
|
+
* being hoisted into the shared chunk when they're only used by a
|
|
63
|
+
* specific route.
|
|
64
|
+
*
|
|
24
65
|
* This balances route-scoped code splitting with HTTP request count:
|
|
25
66
|
* fewer chunks than per-serverChunk, but still avoids downloading every
|
|
26
67
|
* client component on every page.
|
|
27
68
|
*/
|
|
28
69
|
export function clientChunkGroup(
|
|
29
70
|
meta: { id: string; normalizedId: string; serverChunk: string },
|
|
30
|
-
appDir: string
|
|
71
|
+
appDir: string,
|
|
72
|
+
root: string
|
|
31
73
|
): string {
|
|
32
74
|
const { normalizedId, serverChunk } = meta;
|
|
33
75
|
|
|
34
76
|
// Shared chunks (not associated with a single route entry) get one group.
|
|
35
77
|
if (serverChunk.startsWith('shared:')) return 'client-shared';
|
|
36
78
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
const
|
|
79
|
+
// Compute the root-relative app directory prefix once. Both normalizedId
|
|
80
|
+
// and serverChunk paths from the RSC plugin are root-relative (via
|
|
81
|
+
// path.relative(root, absoluteId)), so this prefix matches exactly —
|
|
82
|
+
// no ambiguity with content directories that happen to share a name.
|
|
83
|
+
const appPrefix = relative(root, appDir).replace(/\\/g, '/') + '/';
|
|
42
84
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (appIdx === -1) return 'client-shared';
|
|
85
|
+
// Try the client module's own path first.
|
|
86
|
+
const result = findAppBoundary(normalizedId, appPrefix);
|
|
87
|
+
if (result.inApp && result.segment) return `client-${result.segment}`;
|
|
88
|
+
if (result.inApp) return 'client-shared';
|
|
48
89
|
|
|
49
|
-
|
|
50
|
-
|
|
90
|
+
// The client module lives outside the app directory (e.g. src/posts/,
|
|
91
|
+
// src/components/). Fall back to the serverChunk's facade path — the
|
|
92
|
+
// RSC entry point that imported this module — to derive the route's
|
|
93
|
+
// layout boundary. This keeps dependencies of lazily-loaded 'use client'
|
|
94
|
+
// components (e.g. from import.meta.glob) in per-route async chunks
|
|
95
|
+
// instead of hoisting them into client-shared.
|
|
96
|
+
if (serverChunk.startsWith('facade:')) {
|
|
97
|
+
const facadePath = serverChunk.slice('facade:'.length).replace(/\\/g, '/');
|
|
98
|
+
const facadeResult = findAppBoundary(facadePath, appPrefix);
|
|
99
|
+
if (facadeResult.inApp && facadeResult.segment) return `client-${facadeResult.segment}`;
|
|
100
|
+
if (facadeResult.inApp) return 'client-shared';
|
|
51
101
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
// Found a real route segment
|
|
59
|
-
return `client-${seg}`;
|
|
102
|
+
// Both module and facade are outside app/. Derive a chunk name from
|
|
103
|
+
// the full facade path to keep these isolated from client-shared.
|
|
104
|
+
// Uses the full path (minus extension) to avoid collisions between
|
|
105
|
+
// facades with the same basename (e.g. foo/index.mdx vs bar/index.mdx).
|
|
106
|
+
const facadeName = facadePath.replace(/\.[^.]+$/, '').replace(/\//g, '-');
|
|
107
|
+
if (facadeName) return `client-${facadeName}`;
|
|
60
108
|
}
|
|
61
109
|
|
|
62
110
|
// Root-level files (layout.tsx, page.tsx directly in app/) go into the
|
package/src/plugins/fonts.ts
CHANGED
|
@@ -60,7 +60,7 @@ export { generateFontCss, generateAllFontCss } from '../fonts/virtual-modules.js
|
|
|
60
60
|
* Create the timber-fonts Vite plugin.
|
|
61
61
|
*/
|
|
62
62
|
export function timberFonts(ctx: PluginContext): Plugin {
|
|
63
|
-
const pipeline = new FontPipeline();
|
|
63
|
+
const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
|
|
64
64
|
|
|
65
65
|
return {
|
|
66
66
|
name: 'timber-fonts',
|
|
@@ -189,8 +189,10 @@ export function timberFonts(ctx: PluginContext): Plugin {
|
|
|
189
189
|
*
|
|
190
190
|
* In dev mode this is a no-op — fonts point to the Google CDN and are
|
|
191
191
|
* resolved lazily inside `load()` when the side-effect module runs.
|
|
192
|
-
*
|
|
193
|
-
*
|
|
192
|
+
*
|
|
193
|
+
* Local font hashing is handled at registration time by the pipeline
|
|
194
|
+
* (when `hashOnRegister` is set) so hashed filenames are available
|
|
195
|
+
* before the virtual CSS `load()` hook runs.
|
|
194
196
|
*/
|
|
195
197
|
async buildStart() {
|
|
196
198
|
if (ctx.dev) return;
|
|
@@ -226,10 +228,9 @@ export function timberFonts(ctx: PluginContext): Plugin {
|
|
|
226
228
|
/**
|
|
227
229
|
* Emit font binaries and build-manifest entries.
|
|
228
230
|
*
|
|
229
|
-
* Google
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
* binaries here.
|
|
231
|
+
* All font binaries (Google and local) use content-hashed filenames
|
|
232
|
+
* for immutable caching. Font CSS is handled by Vite's CSS pipeline
|
|
233
|
+
* via virtual modules — we only emit binaries here.
|
|
233
234
|
*/
|
|
234
235
|
generateBundle() {
|
|
235
236
|
emitFontAssets(
|
|
@@ -238,7 +239,7 @@ export function timberFonts(ctx: PluginContext): Plugin {
|
|
|
238
239
|
(msg) => this.warn(msg)
|
|
239
240
|
);
|
|
240
241
|
if (!ctx.buildManifest) return;
|
|
241
|
-
writeFontManifest(pipeline, ctx.buildManifest
|
|
242
|
+
writeFontManifest(pipeline, ctx.buildManifest);
|
|
242
243
|
},
|
|
243
244
|
};
|
|
244
245
|
}
|