@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 ADDED
@@ -0,0 +1,8 @@
1
+ DONTFUCKINGUSE LICENSE
2
+
3
+ Copyright (c) 2025 Daniel Saewitz
4
+
5
+ This software may not be used, copied, modified, merged, published,
6
+ distributed, sublicensed, or sold by anyone other than the copyright holder.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND.
@@ -37,12 +37,12 @@ export declare function groupCachedFontsByFamily(cachedFonts: readonly CachedFon
37
37
  export declare function emitFontAssets(pipeline: FontPipeline, emitFile: EmitFile, warn: EmitWarning): void;
38
38
  /**
39
39
  * Populate `buildManifest.fonts` with `ManifestFontEntry[]` keyed by the
40
- * importer module path (relative to the project root, matching how Vite's
41
- * `manifest.json` keys css/js).
40
+ * absolute importer path. This matches the runtime route manifest's
41
+ * `filePath` values (which are absolute) and is consistent with how
42
+ * `buildManifest.css` keys work after TIM-1014.
42
43
  *
43
- * Google fonts use the content-hashed filenames produced during
44
- * `buildStart`. Local fonts use the source basename.
44
+ * All font binaries use content-hashed filenames for immutable caching.
45
45
  */
46
- export declare function writeFontManifest(pipeline: FontPipeline, buildManifest: BuildManifest, rootDir: string): void;
46
+ export declare function writeFontManifest(pipeline: FontPipeline, buildManifest: BuildManifest): void;
47
47
  export {};
48
48
  //# sourceMappingURL=bundle.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/fonts/bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAqB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEpF;;;;GAIG;AACH,UAAU,YAAY;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CAC7B;AACD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,KAAK,QAAQ,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC;AAChD,KAAK,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAEzC,kEAAkE;AAClE,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,SAAS,UAAU,EAAE,GACjC,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAS3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,WAAW,GAChB,IAAI,CA4BN;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,YAAY,EACtB,aAAa,EAAE,aAAa,EAC5B,OAAO,EAAE,MAAM,GACd,IAAI,CAwCN"}
1
+ {"version":3,"file":"bundle.d.ts","sourceRoot":"","sources":["../../src/fonts/bundle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAqB,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAGpF;;;;GAIG;AACH,UAAU,YAAY;IACpB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,GAAG,UAAU,CAAC;CAC7B;AACD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C,KAAK,QAAQ,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC;AAChD,KAAK,WAAW,GAAG,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;AAEzC,kEAAkE;AAClE,wBAAgB,wBAAwB,CACtC,WAAW,EAAE,SAAS,UAAU,EAAE,GACjC,GAAG,CAAC,MAAM,EAAE,UAAU,EAAE,CAAC,CAS3B;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,WAAW,GAChB,IAAI,CA4BN;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,aAAa,GAAG,IAAI,CAgC5F"}
@@ -49,6 +49,11 @@ export declare class FontPipeline {
49
49
  * the map contents are mutable through the methods below.
50
50
  */
51
51
  private readonly _entries;
52
+ /** When true, local font sources are content-hashed at registration time. */
53
+ private readonly _hashOnRegister;
54
+ constructor(opts?: {
55
+ hashOnRegister?: boolean;
56
+ });
52
57
  /**
53
58
  * Per-importer virtual CSS module id registry. Populated by the
54
59
  * transform hook when it inlines a font function call, read by the
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/fonts/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAI9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAEtD,qBAAa,YAAY;IACvB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IAEzD;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,sBAA6B;IAI9C,kCAAkC;IAClC,IAAI,IAAI,MAAM;IAId,2EAA2E;IAC3E,OAAO,IAAI,gBAAgB,CAAC,SAAS,CAAC;IAItC,uEAAuE;IACvE,KAAK,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAIxC,2CAA2C;IAC1C,WAAW,IAAI,gBAAgB,CAAC,SAAS,CAAC;IAM3C,kCAAkC;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI3C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKjC;;;;;OAKG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAOvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAOxC,oEAAoE;IACpE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAW9D,8DAA8D;IAC9D,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAW5D;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAShD,mEAAmE;IACnE,KAAK,IAAI,IAAI;IAOb;;;;;OAKG;IACF,iBAAiB,IAAI,gBAAgB,CAAC,UAAU,CAAC;IAYlD;;;;;;;OAOG;IACH,MAAM,IAAI,MAAM;IAQhB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAQ3C;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,IAAI,CAON"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/fonts/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC/E,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAK9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAE3D;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;AAEtD,qBAAa,YAAY;IACvB;;;;;;;OAOG;IACH,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAgC;IAEzD,6EAA6E;IAC7E,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAU;gBAE9B,IAAI,CAAC,EAAE;QAAE,cAAc,CAAC,EAAE,OAAO,CAAA;KAAE;IAI/C;;;;;;;;;OASG;IACH,QAAQ,CAAC,QAAQ,sBAA6B;IAI9C,kCAAkC;IAClC,IAAI,IAAI,MAAM;IAId,2EAA2E;IAC3E,OAAO,IAAI,gBAAgB,CAAC,SAAS,CAAC;IAItC,uEAAuE;IACvE,KAAK,IAAI,gBAAgB,CAAC,aAAa,CAAC;IAIxC,2CAA2C;IAC1C,WAAW,IAAI,gBAAgB,CAAC,SAAS,CAAC;IAM3C,kCAAkC;IAClC,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS;IAI3C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAKjC;;;;;OAKG;IACH,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAOvC;;;;;;;;OAQG;IACH,QAAQ,CAAC,SAAS,EAAE,aAAa,GAAG,IAAI;IAoBxC,oEAAoE;IACpE,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAW9D,8DAA8D;IAC9D,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI;IAW5D;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAShD,mEAAmE;IACnE,KAAK,IAAI,IAAI;IAOb;;;;;OAKG;IACF,iBAAiB,IAAI,gBAAgB,CAAC,UAAU,CAAC;IAYlD;;;;;;;OAOG;IACH,MAAM,IAAI,MAAM;IAQhB;;;;;;;;;;;;;;;OAeG;IACH,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM;CAQ3C;AAoCD;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,IAAI,CAON"}
@@ -21,6 +21,8 @@ export interface LocalFontSrc {
21
21
  path: string;
22
22
  weight?: string;
23
23
  style?: string;
24
+ /** Content-hashed basename for production builds (e.g. `MyFont-Bold-a1b2c3d4.woff2`). */
25
+ hashedBasename?: string;
24
26
  }
25
27
  /** Configuration passed to `localFont()`. */
26
28
  export interface LocalFontConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fonts/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC7B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oDAAoD;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,SAAU,SAAQ,aAAa;IAC9C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAE7B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/fonts/types.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,8EAA8E;AAC9E,MAAM,WAAW,gBAAgB;IAC/B,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC1B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;IAC7B,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,mEAAmE;IACnE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,aAAa;IAC5B,gFAAgF;IAChF,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,MAAM,EAAE,MAAM,CAAC;IACf,qCAAqC;IACrC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC7B,+CAA+C;IAC/C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,oDAAoD;IACpD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,6DAA6D;IAC7D,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,0BAA0B;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yCAAyC;IACzC,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,oEAAoE;IACpE,QAAQ,EAAE,MAAM,CAAC;IACjB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,SAAU,SAAQ,aAAa;IAC9C;;;;;OAKG;IACH,KAAK,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAE7B;;;;;;;;OAQG;IACH,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;CAC5B"}
package/dist/index.js CHANGED
@@ -3927,7 +3927,7 @@ function generateLocalFontFaces(family, sources, display, urlPrefix = "/_timber/
3927
3927
  const format = inferFontFormat(entry.path);
3928
3928
  return {
3929
3929
  family,
3930
- src: `url('${urlPrefix}/${entry.path.split("/").pop() ?? entry.path}') format('${format}')`,
3930
+ src: `url('${urlPrefix}/${entry.hashedBasename ?? entry.path.split("/").pop() ?? entry.path}') format('${format}')`,
3931
3931
  weight: entry.weight,
3932
3932
  style: entry.style,
3933
3933
  display
@@ -4095,6 +4095,33 @@ function generateLocalVirtualModule() {
4095
4095
  }
4096
4096
  //#endregion
4097
4097
  //#region src/fonts/pipeline.ts
4098
+ /**
4099
+ * FontPipeline — owns all per-font state for the timber-fonts plugin.
4100
+ *
4101
+ * Before TIM-829, font state was spread across three parallel mutable maps:
4102
+ *
4103
+ * - `registry: Map<string, ExtractedFont>`
4104
+ * - `googleFontFacesMap: Map<string, FontFaceDescriptor[]>`
4105
+ * - `cachedFonts: CachedFont[]`
4106
+ *
4107
+ * Every plugin hook had to remember which map(s) it owned and which it
4108
+ * read. The TIM-824 bug existed because `transform` could write to
4109
+ * `registry` multiple times for the same `(importer, family)` pair without
4110
+ * a corresponding cleanup of the entries that `buildStart` / `load` would
4111
+ * later use. The fix in TIM-824 added prune calls in `transform` for the
4112
+ * registry, but the parallel maps still had the same hazard latent in
4113
+ * them: any future code path that ran `buildStart`-equivalent logic on
4114
+ * HMR could re-introduce a stale-entry leak in a different map.
4115
+ *
4116
+ * TIM-829 collapses the three maps into a single `Map<string, FontEntry>`
4117
+ * keyed by font ID. Mutation goes through a small set of methods that
4118
+ * maintain the core invariant: **every piece of state belonging to a font
4119
+ * lives on its `FontEntry`, and `pruneFor` removes the entry along with
4120
+ * everything attached to it in one call.** No code path inside the plugin
4121
+ * touches the underlying map directly.
4122
+ *
4123
+ * Design doc: 24-fonts.md
4124
+ */
4098
4125
  var FontPipeline = class {
4099
4126
  /**
4100
4127
  * The single per-font store. Keyed by `FontEntry.id` (which is itself
@@ -4105,6 +4132,11 @@ var FontPipeline = class {
4105
4132
  * the map contents are mutable through the methods below.
4106
4133
  */
4107
4134
  _entries = /* @__PURE__ */ new Map();
4135
+ /** When true, local font sources are content-hashed at registration time. */
4136
+ _hashOnRegister;
4137
+ constructor(opts) {
4138
+ this._hashOnRegister = opts?.hashOnRegister ?? false;
4139
+ }
4108
4140
  /**
4109
4141
  * Per-importer virtual CSS module id registry. Populated by the
4110
4142
  * transform hook when it inlines a font function call, read by the
@@ -4170,7 +4202,16 @@ var FontPipeline = class {
4170
4202
  * dropped (TIM-824).
4171
4203
  */
4172
4204
  register(extracted) {
4173
- this._entries.set(extracted.id, { ...extracted });
4205
+ const entry = { ...extracted };
4206
+ if (this._hashOnRegister && entry.provider === "local" && entry.localSources) for (const src of entry.localSources) {
4207
+ const absolutePath = normalize(resolve(src.path));
4208
+ if (!existsSync(absolutePath)) continue;
4209
+ const hash = contentHash(readFileSync(absolutePath));
4210
+ const basename = src.path.split("/").pop() ?? src.path;
4211
+ const ext = extname(basename);
4212
+ src.hashedBasename = `${ext ? basename.slice(0, -ext.length) : basename}-${hash}${ext}`;
4213
+ }
4214
+ this._entries.set(entry.id, entry);
4174
4215
  }
4175
4216
  /** Attach pre-resolved `@font-face` descriptors to a font entry. */
4176
4217
  attachFaces(fontId, faces) {
@@ -5547,11 +5588,11 @@ function emitFontAssets(pipeline, emitFile, warn) {
5547
5588
  warn(`Local font file not found: ${absolutePath}`);
5548
5589
  continue;
5549
5590
  }
5550
- const basename = src.path.split("/").pop() ?? src.path;
5591
+ const filename = src.hashedBasename ?? src.path.split("/").pop() ?? src.path;
5551
5592
  const data = readFileSync(absolutePath);
5552
5593
  emitFile({
5553
5594
  type: "asset",
5554
- fileName: `_timber/fonts/${basename}`,
5595
+ fileName: `_timber/fonts/${filename}`,
5555
5596
  source: data
5556
5597
  });
5557
5598
  }
@@ -5559,18 +5600,18 @@ function emitFontAssets(pipeline, emitFile, warn) {
5559
5600
  }
5560
5601
  /**
5561
5602
  * Populate `buildManifest.fonts` with `ManifestFontEntry[]` keyed by the
5562
- * importer module path (relative to the project root, matching how Vite's
5563
- * `manifest.json` keys css/js).
5603
+ * absolute importer path. This matches the runtime route manifest's
5604
+ * `filePath` values (which are absolute) and is consistent with how
5605
+ * `buildManifest.css` keys work after TIM-1014.
5564
5606
  *
5565
- * Google fonts use the content-hashed filenames produced during
5566
- * `buildStart`. Local fonts use the source basename.
5607
+ * All font binaries use content-hashed filenames for immutable caching.
5567
5608
  */
5568
- function writeFontManifest(pipeline, buildManifest, rootDir) {
5609
+ function writeFontManifest(pipeline, buildManifest) {
5569
5610
  const fontsByImporter = /* @__PURE__ */ new Map();
5570
5611
  for (const entry of pipeline.entries()) {
5571
5612
  const manifestEntries = fontsByImporter.get(entry.importer) ?? [];
5572
5613
  if (entry.provider === "local" && entry.localSources) for (const src of entry.localSources) {
5573
- const filename = src.path.split("/").pop() ?? src.path;
5614
+ const filename = src.hashedBasename ?? src.path.split("/").pop() ?? src.path;
5574
5615
  const format = inferFontFormat(src.path);
5575
5616
  manifestEntries.push({
5576
5617
  href: `/_timber/fonts/${filename}`,
@@ -5585,10 +5626,7 @@ function writeFontManifest(pipeline, buildManifest, rootDir) {
5585
5626
  });
5586
5627
  fontsByImporter.set(entry.importer, manifestEntries);
5587
5628
  }
5588
- for (const [importer, entries] of fontsByImporter) {
5589
- const relativePath = importer.startsWith(rootDir) ? importer.slice(rootDir.length + 1) : importer;
5590
- buildManifest.fonts[relativePath] = entries;
5591
- }
5629
+ for (const [importer, entries] of fontsByImporter) buildManifest.fonts[importer] = entries;
5592
5630
  }
5593
5631
  //#endregion
5594
5632
  //#region src/plugins/fonts.ts
@@ -5596,7 +5634,7 @@ function writeFontManifest(pipeline, buildManifest, rootDir) {
5596
5634
  * Create the timber-fonts Vite plugin.
5597
5635
  */
5598
5636
  function timberFonts(ctx) {
5599
- const pipeline = new FontPipeline();
5637
+ const pipeline = new FontPipeline({ hashOnRegister: !ctx.dev });
5600
5638
  return {
5601
5639
  name: "timber-fonts",
5602
5640
  /**
@@ -5690,8 +5728,10 @@ function timberFonts(ctx) {
5690
5728
  *
5691
5729
  * In dev mode this is a no-op — fonts point to the Google CDN and are
5692
5730
  * resolved lazily inside `load()` when the side-effect module runs.
5693
- * The registry is populated by the transform hook which runs before
5694
- * `buildStart` in the build pipeline, so all fonts are known here.
5731
+ *
5732
+ * Local font hashing is handled at registration time by the pipeline
5733
+ * (when `hashOnRegister` is set) so hashed filenames are available
5734
+ * before the virtual CSS `load()` hook runs.
5695
5735
  */
5696
5736
  async buildStart() {
5697
5737
  if (ctx.dev) return;
@@ -5716,15 +5756,14 @@ function timberFonts(ctx) {
5716
5756
  /**
5717
5757
  * Emit font binaries and build-manifest entries.
5718
5758
  *
5719
- * Google fonts use the content-hashed filenames produced during
5720
- * `buildStart`. Local fonts are emitted by basename. Font CSS is
5721
- * handled by Vite's CSS pipeline via virtual modules — we only emit
5722
- * binaries here.
5759
+ * All font binaries (Google and local) use content-hashed filenames
5760
+ * for immutable caching. Font CSS is handled by Vite's CSS pipeline
5761
+ * via virtual modules — we only emit binaries here.
5723
5762
  */
5724
5763
  generateBundle() {
5725
5764
  emitFontAssets(pipeline, (asset) => this.emitFile(asset), (msg) => this.warn(msg));
5726
5765
  if (!ctx.buildManifest) return;
5727
- writeFontManifest(pipeline, ctx.buildManifest, ctx.root);
5766
+ writeFontManifest(pipeline, ctx.buildManifest);
5728
5767
  }
5729
5768
  };
5730
5769
  }
@@ -6445,6 +6484,36 @@ function timberChunks() {
6445
6484
  * See design/27-chunking-strategy.md, TIM-440, TIM-499.
6446
6485
  */
6447
6486
  /**
6487
+ * Check whether a root-relative path lives under the app directory,
6488
+ * and if so return the first non-group route segment.
6489
+ *
6490
+ * `appPrefix` is the root-relative app directory with a trailing slash
6491
+ * (e.g. "app/" or "src/app/"), computed once from the resolved appDir
6492
+ * and root. Matching uses `startsWith` — no ambiguity.
6493
+ *
6494
+ * Returns:
6495
+ * { inApp: false } — path is not under app/
6496
+ * { inApp: true, segment: null } — root of app/ (e.g. app/layout.tsx)
6497
+ * { inApp: true, segment: "dashboard" } — nested route boundary
6498
+ */
6499
+ function findAppBoundary(path, appPrefix) {
6500
+ const relPath = path.replace(/\\/g, "/");
6501
+ if (!relPath.startsWith(appPrefix)) return { inApp: false };
6502
+ const segments = relPath.slice(appPrefix.length).split("/");
6503
+ for (const seg of segments) {
6504
+ if (seg.includes(".")) break;
6505
+ if (seg.startsWith("(") && seg.endsWith(")")) continue;
6506
+ return {
6507
+ inApp: true,
6508
+ segment: seg
6509
+ };
6510
+ }
6511
+ return {
6512
+ inApp: true,
6513
+ segment: null
6514
+ };
6515
+ }
6516
+ /**
6448
6517
  * Derive a chunk group name for a client reference module.
6449
6518
  *
6450
6519
  * Groups by the first non-group route segment under appDir so that all
@@ -6455,22 +6524,31 @@ function timberChunks() {
6455
6524
  * - `facade:app/layout.tsx` (root layout) → `"client-shared"`
6456
6525
  * - `shared:...` (shared across chunks) → `"client-shared"`
6457
6526
  *
6527
+ * When a 'use client' module lives outside the app directory (e.g. in
6528
+ * src/posts/ or src/components/), the function falls back to deriving
6529
+ * the layout boundary from the serverChunk's facade path. This prevents
6530
+ * lazily-loaded client dependencies (e.g. via import.meta.glob) from
6531
+ * being hoisted into the shared chunk when they're only used by a
6532
+ * specific route.
6533
+ *
6458
6534
  * This balances route-scoped code splitting with HTTP request count:
6459
6535
  * fewer chunks than per-serverChunk, but still avoids downloading every
6460
6536
  * client component on every page.
6461
6537
  */
6462
- function clientChunkGroup(meta, appDir) {
6538
+ function clientChunkGroup(meta, appDir, root) {
6463
6539
  const { normalizedId, serverChunk } = meta;
6464
6540
  if (serverChunk.startsWith("shared:")) return "client-shared";
6465
- const relPath = normalizedId.replace(/\\/g, "/");
6466
- const appDirName = appDir.replace(/\\/g, "/").split("/").pop() || "app";
6467
- const appIdx = relPath.indexOf(appDirName + "/");
6468
- if (appIdx === -1) return "client-shared";
6469
- const segments = relPath.slice(appIdx + appDirName.length + 1).split("/");
6470
- for (const seg of segments) {
6471
- if (seg.includes(".")) break;
6472
- if (seg.startsWith("(") && seg.endsWith(")")) continue;
6473
- return `client-${seg}`;
6541
+ const appPrefix = relative(root, appDir).replace(/\\/g, "/") + "/";
6542
+ const result = findAppBoundary(normalizedId, appPrefix);
6543
+ if (result.inApp && result.segment) return `client-${result.segment}`;
6544
+ if (result.inApp) return "client-shared";
6545
+ if (serverChunk.startsWith("facade:")) {
6546
+ const facadePath = serverChunk.slice(7).replace(/\\/g, "/");
6547
+ const facadeResult = findAppBoundary(facadePath, appPrefix);
6548
+ if (facadeResult.inApp && facadeResult.segment) return `client-${facadeResult.segment}`;
6549
+ if (facadeResult.inApp) return "client-shared";
6550
+ const facadeName = facadePath.replace(/\.[^.]+$/, "").replace(/\//g, "-");
6551
+ if (facadeName) return `client-${facadeName}`;
6474
6552
  }
6475
6553
  return "client-shared";
6476
6554
  }
@@ -7454,7 +7532,7 @@ function createRscOptions(ctx, encryptionKeyExpr) {
7454
7532
  ssr: "virtual:timber-ssr-entry",
7455
7533
  client: "virtual:timber-browser-entry"
7456
7534
  },
7457
- clientChunks: (meta) => clientChunkGroup(meta, ctx.appDir)
7535
+ clientChunks: (meta) => clientChunkGroup(meta, ctx.appDir, ctx.root)
7458
7536
  };
7459
7537
  Object.defineProperty(options, "enableActionEncryption", {
7460
7538
  get() {