@timber-js/app 0.1.24 → 0.1.26

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.
Files changed (48) hide show
  1. package/dist/adapters/nitro.d.ts +9 -0
  2. package/dist/adapters/nitro.d.ts.map +1 -1
  3. package/dist/adapters/nitro.js +175 -7
  4. package/dist/adapters/nitro.js.map +1 -1
  5. package/dist/cli.js +2 -2
  6. package/dist/cli.js.map +1 -1
  7. package/dist/client/browser-dev.d.ts +29 -0
  8. package/dist/client/browser-dev.d.ts.map +1 -0
  9. package/dist/client/browser-links.d.ts +32 -0
  10. package/dist/client/browser-links.d.ts.map +1 -0
  11. package/dist/client/index.d.ts +1 -1
  12. package/dist/client/index.d.ts.map +1 -1
  13. package/dist/client/index.js +46 -20
  14. package/dist/client/index.js.map +1 -1
  15. package/dist/client/navigation-context.d.ts +10 -8
  16. package/dist/client/navigation-context.d.ts.map +1 -1
  17. package/dist/client/transition-root.d.ts +54 -0
  18. package/dist/client/transition-root.d.ts.map +1 -0
  19. package/dist/client/use-router.d.ts +14 -0
  20. package/dist/client/use-router.d.ts.map +1 -1
  21. package/dist/server/index.js +264 -218
  22. package/dist/server/index.js.map +1 -1
  23. package/dist/server/metadata-platform.d.ts +34 -0
  24. package/dist/server/metadata-platform.d.ts.map +1 -0
  25. package/dist/server/metadata-render.d.ts.map +1 -1
  26. package/dist/server/metadata-social.d.ts +24 -0
  27. package/dist/server/metadata-social.d.ts.map +1 -0
  28. package/dist/server/pipeline-interception.d.ts +32 -0
  29. package/dist/server/pipeline-interception.d.ts.map +1 -0
  30. package/dist/server/pipeline-metadata.d.ts +31 -0
  31. package/dist/server/pipeline-metadata.d.ts.map +1 -0
  32. package/dist/server/pipeline.d.ts.map +1 -1
  33. package/package.json +1 -1
  34. package/src/adapters/nitro.ts +187 -10
  35. package/src/cli.ts +10 -3
  36. package/src/client/browser-dev.ts +142 -0
  37. package/src/client/browser-entry.ts +32 -222
  38. package/src/client/browser-links.ts +90 -0
  39. package/src/client/index.ts +1 -1
  40. package/src/client/navigation-context.ts +39 -9
  41. package/src/client/transition-root.tsx +86 -0
  42. package/src/client/use-router.ts +17 -15
  43. package/src/server/metadata-platform.ts +229 -0
  44. package/src/server/metadata-render.ts +9 -363
  45. package/src/server/metadata-social.ts +184 -0
  46. package/src/server/pipeline-interception.ts +76 -0
  47. package/src/server/pipeline-metadata.ts +90 -0
  48. package/src/server/pipeline.ts +2 -148
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Platform-specific metadata rendering — icons, Apple Web App, App Links, iTunes.
3
+ *
4
+ * Extracted from metadata-render.ts to keep files under 500 lines.
5
+ *
6
+ * See design/16-metadata.md
7
+ */
8
+ import type { Metadata } from './types.js';
9
+ import type { HeadElement } from './metadata.js';
10
+ /**
11
+ * Render icon link elements (favicon, shortcut, apple-touch-icon, custom).
12
+ */
13
+ export declare function renderIcons(icons: NonNullable<Metadata['icons']>, elements: HeadElement[]): void;
14
+ /**
15
+ * Render alternate link elements (canonical, hreflang, media, types).
16
+ */
17
+ export declare function renderAlternates(alternates: NonNullable<Metadata['alternates']>, elements: HeadElement[]): void;
18
+ /**
19
+ * Render site verification meta tags (Google, Yahoo, Yandex, custom).
20
+ */
21
+ export declare function renderVerification(verification: NonNullable<Metadata['verification']>, elements: HeadElement[]): void;
22
+ /**
23
+ * Render Apple Web App meta tags and startup image links.
24
+ */
25
+ export declare function renderAppleWebApp(appleWebApp: NonNullable<Metadata['appleWebApp']>, elements: HeadElement[]): void;
26
+ /**
27
+ * Render App Links (al:*) meta tags for deep linking across platforms.
28
+ */
29
+ export declare function renderAppLinks(appLinks: NonNullable<Metadata['appLinks']>, elements: HeadElement[]): void;
30
+ /**
31
+ * Render Apple iTunes smart banner meta tag.
32
+ */
33
+ export declare function renderItunes(itunes: NonNullable<Metadata['itunes']>, elements: HeadElement[]): void;
34
+ //# sourceMappingURL=metadata-platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-platform.d.ts","sourceRoot":"","sources":["../../src/server/metadata-platform.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,WAAW,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CA6ChG;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,WAAW,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,EAC/C,QAAQ,EAAE,WAAW,EAAE,GACtB,IAAI,CA+BN;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,WAAW,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,EACnD,QAAQ,EAAE,WAAW,EAAE,GACtB,IAAI,CAkBN;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,WAAW,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,EACjD,QAAQ,EAAE,WAAW,EAAE,GACtB,IAAI,CAmCN;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,WAAW,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,EAC3C,QAAQ,EAAE,WAAW,EAAE,GACtB,IAAI,CAwCN;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,WAAW,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAQnG"}
@@ -1 +1 @@
1
- {"version":3,"file":"metadata-render.d.ts","sourceRoot":"","sources":["../../src/server/metadata-render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAIjD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAkI1E"}
1
+ {"version":3,"file":"metadata-render.d.ts","sourceRoot":"","sources":["../../src/server/metadata-render.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAajD;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,QAAQ,GAAG,WAAW,EAAE,CAkI1E"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Social metadata rendering — Open Graph and Twitter Card meta tags.
3
+ *
4
+ * Extracted from metadata-render.ts to keep files under 500 lines.
5
+ *
6
+ * See design/16-metadata.md
7
+ */
8
+ import type { Metadata } from './types.js';
9
+ import type { HeadElement } from './metadata.js';
10
+ /**
11
+ * Render Open Graph metadata into head element descriptors.
12
+ *
13
+ * Handles og:title, og:description, og:image (with dimensions/alt),
14
+ * og:video, og:audio, og:article:author, and other OG properties.
15
+ */
16
+ export declare function renderOpenGraph(og: NonNullable<Metadata['openGraph']>, elements: HeadElement[]): void;
17
+ /**
18
+ * Render Twitter Card metadata into head element descriptors.
19
+ *
20
+ * Handles twitter:card, twitter:site, twitter:title, twitter:image,
21
+ * twitter:player, and twitter:app (per-platform name/id/url).
22
+ */
23
+ export declare function renderTwitter(tw: NonNullable<Metadata['twitter']>, elements: HeadElement[]): void;
24
+ //# sourceMappingURL=metadata-social.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata-social.d.ts","sourceRoot":"","sources":["../../src/server/metadata-social.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAEjD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CAoErG;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,EAAE,EAAE,WAAW,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EAAE,QAAQ,EAAE,WAAW,EAAE,GAAG,IAAI,CA0FjG"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Interception route matching for the request pipeline.
3
+ *
4
+ * Matches target URLs against interception rewrites to support the
5
+ * modal route pattern (soft navigation intercepts).
6
+ *
7
+ * Extracted from pipeline.ts to keep files under 500 lines.
8
+ *
9
+ * See design/07-routing.md §"Intercepting Routes"
10
+ */
11
+ /** Result of a successful interception match. */
12
+ export interface InterceptionMatchResult {
13
+ /** The pathname to re-match (the source/intercepting route's parent). */
14
+ sourcePathname: string;
15
+ }
16
+ /**
17
+ * Check if an intercepting route applies for this soft navigation.
18
+ *
19
+ * Matches the target pathname against interception rewrites, constrained
20
+ * by the source URL (X-Timber-URL header — where the user navigates FROM).
21
+ *
22
+ * Returns the source pathname to re-match if interception applies, or null.
23
+ */
24
+ export declare function findInterceptionMatch(targetPathname: string, sourceUrl: string, rewrites: import('#/routing/interception.js').InterceptionRewrite[]): InterceptionMatchResult | null;
25
+ /**
26
+ * Check if a pathname matches a URL pattern with dynamic segments.
27
+ *
28
+ * Supports [param] (single segment) and [...param] (one or more segments).
29
+ * Static segments must match exactly.
30
+ */
31
+ export declare function pathnameMatchesPattern(pathname: string, pattern: string): boolean;
32
+ //# sourceMappingURL=pipeline-interception.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline-interception.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-interception.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,iDAAiD;AACjD,MAAM,WAAW,uBAAuB;IACtC,yEAAyE;IACzE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CACnC,cAAc,EAAE,MAAM,EACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,OAAO,2BAA2B,EAAE,mBAAmB,EAAE,GAClE,uBAAuB,GAAG,IAAI,CAYhC;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CA0BjF"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Metadata route helpers for the request pipeline.
3
+ *
4
+ * Handles serving static metadata files and serializing sitemap responses.
5
+ * Extracted from pipeline.ts to keep files under 500 lines.
6
+ *
7
+ * See design/16-metadata.md §"Metadata Routes"
8
+ */
9
+ /**
10
+ * Serve a static metadata file by reading it from disk.
11
+ *
12
+ * Static metadata route files (.xml, .txt, .json, .png, .ico, .svg, etc.)
13
+ * are served as-is with the appropriate Content-Type header.
14
+ * Text files include charset=utf-8; binary files do not.
15
+ *
16
+ * See design/16-metadata.md §"Metadata Routes"
17
+ */
18
+ export declare function serveStaticMetadataFile(metaMatch: import('./route-matcher.js').MetadataRouteMatch): Promise<Response>;
19
+ /**
20
+ * Serialize a sitemap array to XML.
21
+ * Follows the sitemap.org protocol: https://www.sitemaps.org/protocol.html
22
+ */
23
+ export declare function serializeSitemap(entries: Array<{
24
+ url: string;
25
+ lastModified?: string | Date;
26
+ changeFrequency?: string;
27
+ priority?: number;
28
+ }>): string;
29
+ /** Escape special XML characters. */
30
+ export declare function escapeXml(str: string): string;
31
+ //# sourceMappingURL=pipeline-metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pipeline-metadata.d.ts","sourceRoot":"","sources":["../../src/server/pipeline-metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,SAAS,EAAE,OAAO,oBAAoB,EAAE,kBAAkB,GACzD,OAAO,CAAC,QAAQ,CAAC,CAYnB;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,KAAK,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC,GACD,MAAM,CAoBR;AAED,qCAAqC;AACrC,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAkC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAED,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;AAEnE,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,CACjC,QAAQ,EAAE,MAAM,KACb,OAAO,oBAAoB,EAAE,kBAAkB,GAAG,IAAI,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,YAAY,CAAC,EAAE,mBAAmB,KAC/B,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI1B,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACtD,qEAAqE;IACrE,UAAU,EAAE,YAAY,CAAC;IACzB,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,kEAAkE;IAClE,MAAM,EAAE,aAAa,CAAC;IACtB,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzF,kFAAkF;IAClF,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,2BAA2B,EAAE,mBAAmB,EAAE,CAAC;IACjF;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CACpB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuU1F"}
1
+ {"version":3,"file":"pipeline.d.ts","sourceRoot":"","sources":["../../src/server/pipeline.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EAAY,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AACxD,OAAO,EAAiB,KAAK,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAoC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAItD,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC1C,uDAAuD;IACvD,UAAU,CAAC,EAAE,YAAY,CAAC;CAC3B;AAED,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAAC;AAEnE,sEAAsE;AACtE,MAAM,MAAM,oBAAoB,GAAG,CACjC,QAAQ,EAAE,MAAM,KACb,OAAO,oBAAoB,EAAE,kBAAkB,GAAG,IAAI,CAAC;AAE5D,iEAAiE;AACjE,MAAM,WAAW,mBAAmB;IAClC,iEAAiE;IACjE,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,6DAA6D;AAC7D,MAAM,MAAM,aAAa,GAAG,CAC1B,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,UAAU,EACjB,eAAe,EAAE,OAAO,EACxB,oBAAoB,EAAE,OAAO,EAC7B,YAAY,CAAC,EAAE,mBAAmB,KAC/B,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;AAElC,+DAA+D;AAC/D,MAAM,MAAM,iBAAiB,GAAG,CAC9B,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;AAI1B,MAAM,WAAW,cAAc;IAC7B,iFAAiF;IACjF,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAC,CAAC;IACtD,qEAAqE;IACrE,UAAU,EAAE,YAAY,CAAC;IACzB,iGAAiG;IACjG,kBAAkB,CAAC,EAAE,oBAAoB,CAAC;IAC1C,kEAAkE;IAClE,MAAM,EAAE,aAAa,CAAC;IACtB,kEAAkE;IAClE,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACzF,kFAAkF;IAClF,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,gFAAgF;IAChF,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,yGAAyG;IACzG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,2BAA2B,EAAE,mBAAmB,EAAE,CAAC;IACjF;;;;;OAKG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxD;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CACpB,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,OAAO,EACZ,eAAe,EAAE,OAAO,KACrB,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CACnC;AAID;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAuU1F"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
4
4
  "description": "Vite-native React framework for Cloudflare Workers — correct HTTP semantics, real status codes, pages that work without JavaScript",
5
5
  "keywords": [
6
6
  "cloudflare-workers",
@@ -213,9 +213,15 @@ export function nitro(options: NitroAdapterOptions = {}): TimberPlatformAdapter
213
213
  // local runtime — Vite's built-in preview is the fallback.
214
214
  preview: LOCALLY_PREVIEWABLE.has(preset)
215
215
  ? async (_config: TimberConfig, buildDir: string) => {
216
- const cmd = generateNitroPreviewCommand(buildDir, preset);
217
- if (!cmd) return;
218
- await spawnNitroPreview(cmd.command, cmd.args, cmd.cwd);
216
+ // Generate a standalone preview server that uses Node's built-in
217
+ // HTTP server. The Nitro entry.ts can't be run directly because
218
+ // it imports h3 (a Nitro dependency not available at runtime).
219
+ const previewScript = generatePreviewScript(buildDir, preset);
220
+ const scriptPath = join(buildDir, 'nitro', '_preview-server.mjs');
221
+ await writeFile(scriptPath, previewScript);
222
+
223
+ const command = preset === 'bun' ? 'bun' : 'node';
224
+ await spawnNitroPreview(command, [scriptPath], join(buildDir, 'nitro'));
219
225
  }
220
226
  : undefined,
221
227
 
@@ -239,7 +245,13 @@ export function generateNitroEntry(
239
245
  preset: NitroPreset,
240
246
  hasManifestInit = false
241
247
  ): string {
242
- const serverEntryRelative = relative(outDir, join(buildDir, 'server', 'entry.js'));
248
+ // The RSC entry is the main request handler — it exports the fetch handler as default.
249
+ // The Vite RSC plugin outputs it to rsc/index.js.
250
+ let serverEntryRelative = relative(outDir, join(buildDir, 'rsc', 'index.js'));
251
+ // Ensure the import path starts with ./ for ESM compatibility
252
+ if (!serverEntryRelative.startsWith('.')) {
253
+ serverEntryRelative = './' + serverEntryRelative;
254
+ }
243
255
  const runtimeName = PRESET_CONFIGS[preset].runtimeName;
244
256
  const earlyHints = PRESET_CONFIGS[preset].supportsEarlyHints;
245
257
 
@@ -250,10 +262,6 @@ export function generateNitroEntry(
250
262
  // On node-server and bun, wrap the handler with ALS so the pipeline
251
263
  // can send 103 Early Hints via res.writeEarlyHints(). Other presets
252
264
  // either don't support 103 or handle it at the CDN level.
253
- const earlyHintsImport = earlyHints
254
- ? `import { runWithEarlyHintsSender } from '${serverEntryRelative}'\n`
255
- : '';
256
-
257
265
  const handlerCall = earlyHints
258
266
  ? ` const nodeRes = event.node?.res
259
267
  const earlyHintsSender = (typeof nodeRes?.writeEarlyHints === 'function')
@@ -268,8 +276,8 @@ export function generateNitroEntry(
268
276
  return `// Generated by @timber-js/app/adapters/nitro
269
277
  // Do not edit — this file is regenerated on each build.
270
278
 
271
- ${manifestImport}${earlyHintsImport}import { defineEventHandler, toWebRequest, sendWebResponse } from 'h3'
272
- import { handler } from '${serverEntryRelative}'
279
+ ${manifestImport}import { defineEventHandler, toWebRequest, sendWebResponse } from 'h3'
280
+ import handler, { runWithEarlyHintsSender } from '${serverEntryRelative}'
273
281
 
274
282
  // Set TIMBER_RUNTIME for instrumentation.ts conditional SDK initialization.
275
283
  // See design/25-production-deployments.md §"TIMBER_RUNTIME".
@@ -318,6 +326,175 @@ export default defineNitroConfig(${configJson})
318
326
  /** Presets that produce a locally-runnable server entry. */
319
327
  const LOCALLY_PREVIEWABLE = new Set<NitroPreset>(['node-server', 'bun']);
320
328
 
329
+ /**
330
+ * Generate a standalone preview server script that uses Node's built-in
331
+ * HTTP server. This bypasses Nitro entirely — the Nitro entry.ts imports
332
+ * h3 which isn't available outside a Nitro build. For local preview we
333
+ * just need to serve static files and route requests to the RSC handler.
334
+ *
335
+ * @internal Exported for testing.
336
+ */
337
+ export function generatePreviewScript(buildDir: string, preset: NitroPreset): string {
338
+ const rscEntryRelative = relative(join(buildDir, 'nitro'), join(buildDir, 'rsc', 'index.js'));
339
+ const rscEntry = rscEntryRelative.startsWith('.') ? rscEntryRelative : './' + rscEntryRelative;
340
+ const publicDir = './public';
341
+ const manifestInitPath = './_timber-manifest-init.js';
342
+ const runtimeName = PRESET_CONFIGS[preset].runtimeName;
343
+
344
+ return `// Generated by @timber-js/app — standalone preview server.
345
+ // Uses Node's built-in HTTP server to serve static assets and route
346
+ // dynamic requests through the RSC handler. No Nitro/h3 dependency.
347
+
348
+ import { createServer } from 'node:http';
349
+ import { readFile, stat } from 'node:fs/promises';
350
+ import { join, extname } from 'node:path';
351
+ import { fileURLToPath } from 'node:url';
352
+ import { existsSync } from 'node:fs';
353
+
354
+ // Set runtime before importing the handler.
355
+ process.env.TIMBER_RUNTIME = '${runtimeName}';
356
+
357
+ // Load the build manifest if it exists.
358
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
359
+ const manifestPath = join(__dirname, '${manifestInitPath}');
360
+ if (existsSync(manifestPath)) {
361
+ await import('${manifestInitPath}');
362
+ }
363
+
364
+ // Import the RSC handler (default export is the fetch-like handler).
365
+ const { default: handler, runWithEarlyHintsSender } = await import('${rscEntry}');
366
+
367
+ const MIME_TYPES = {
368
+ '.html': 'text/html',
369
+ '.js': 'application/javascript',
370
+ '.mjs': 'application/javascript',
371
+ '.css': 'text/css',
372
+ '.json': 'application/json',
373
+ '.png': 'image/png',
374
+ '.jpg': 'image/jpeg',
375
+ '.jpeg': 'image/jpeg',
376
+ '.gif': 'image/gif',
377
+ '.svg': 'image/svg+xml',
378
+ '.ico': 'image/x-icon',
379
+ '.woff': 'font/woff',
380
+ '.woff2': 'font/woff2',
381
+ '.ttf': 'font/ttf',
382
+ '.otf': 'font/otf',
383
+ '.webp': 'image/webp',
384
+ '.avif': 'image/avif',
385
+ '.webm': 'video/webm',
386
+ '.mp4': 'video/mp4',
387
+ '.txt': 'text/plain',
388
+ '.xml': 'application/xml',
389
+ '.wasm': 'application/wasm',
390
+ };
391
+
392
+ const publicDir = join(__dirname, '${publicDir}');
393
+ const port = parseInt(process.env.PORT || '3000', 10);
394
+
395
+ const server = createServer(async (req, res) => {
396
+ const url = new URL(req.url || '/', \`http://localhost:\${port}\`);
397
+
398
+ // Try serving static files from the public directory first.
399
+ const filePath = join(publicDir, url.pathname);
400
+ // Prevent path traversal.
401
+ if (filePath.startsWith(publicDir)) {
402
+ try {
403
+ const fileStat = await stat(filePath);
404
+ if (fileStat.isFile()) {
405
+ const ext = extname(filePath);
406
+ const contentType = MIME_TYPES[ext] || 'application/octet-stream';
407
+ const body = await readFile(filePath);
408
+ // Hashed assets get immutable cache, others get short cache.
409
+ const cacheControl = url.pathname.startsWith('/assets/')
410
+ ? 'public, max-age=31536000, immutable'
411
+ : 'public, max-age=3600, must-revalidate';
412
+ res.writeHead(200, {
413
+ 'Content-Type': contentType,
414
+ 'Content-Length': body.length,
415
+ 'Cache-Control': cacheControl,
416
+ });
417
+ res.end(body);
418
+ return;
419
+ }
420
+ } catch {
421
+ // File not found — fall through to the RSC handler.
422
+ }
423
+ }
424
+
425
+ // Convert Node request to Web Request.
426
+ const headers = new Headers();
427
+ for (const [key, value] of Object.entries(req.headers)) {
428
+ if (value) {
429
+ if (Array.isArray(value)) {
430
+ for (const v of value) headers.append(key, v);
431
+ } else {
432
+ headers.set(key, value);
433
+ }
434
+ }
435
+ }
436
+
437
+ let body = undefined;
438
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
439
+ body = await new Promise((resolve) => {
440
+ const chunks = [];
441
+ req.on('data', (chunk) => chunks.push(chunk));
442
+ req.on('end', () => resolve(Buffer.concat(chunks)));
443
+ });
444
+ }
445
+
446
+ const webRequest = new Request(url.href, {
447
+ method: req.method,
448
+ headers,
449
+ body,
450
+ duplex: body ? 'half' : undefined,
451
+ });
452
+
453
+ try {
454
+ // Support 103 Early Hints when available.
455
+ const earlyHintsSender = (typeof res.writeEarlyHints === 'function')
456
+ ? (links) => { try { res.writeEarlyHints({ link: links }); } catch {} }
457
+ : undefined;
458
+
459
+ const webResponse = earlyHintsSender && runWithEarlyHintsSender
460
+ ? await runWithEarlyHintsSender(earlyHintsSender, () => handler(webRequest))
461
+ : await handler(webRequest);
462
+
463
+ // Write the response back to the Node response.
464
+ res.writeHead(webResponse.status, Object.fromEntries(webResponse.headers.entries()));
465
+
466
+ if (webResponse.body) {
467
+ const reader = webResponse.body.getReader();
468
+ const pump = async () => {
469
+ while (true) {
470
+ const { done, value } = await reader.read();
471
+ if (done) { res.end(); return; }
472
+ res.write(value);
473
+ }
474
+ };
475
+ await pump();
476
+ } else {
477
+ res.end();
478
+ }
479
+ } catch (err) {
480
+ console.error('[timber preview] Request error:', err);
481
+ if (!res.headersSent) {
482
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
483
+ }
484
+ res.end('Internal Server Error');
485
+ }
486
+ });
487
+
488
+ server.listen(port, () => {
489
+ console.log();
490
+ console.log(' ⚡ timber preview server running at:');
491
+ console.log();
492
+ console.log(\` ➜ http://localhost:\${port}\`);
493
+ console.log();
494
+ });
495
+ `;
496
+ }
497
+
321
498
  /** Command descriptor for Nitro preview — testable without spawning. */
322
499
  export interface NitroPreviewCommand {
323
500
  command: string;
package/src/cli.ts CHANGED
@@ -132,7 +132,7 @@ export async function runPreview(options: CommandOptions): Promise<void> {
132
132
  const adapter = config?.adapter as import('./adapters/types').TimberPlatformAdapter | undefined;
133
133
 
134
134
  if (resolvePreviewStrategy(adapter) === 'adapter') {
135
- const buildDir = join(root, '.timber', 'build');
135
+ const buildDir = join(root, 'dist');
136
136
  const timberConfig = { output: (config?.output ?? 'server') as 'server' | 'static' };
137
137
  await adapter!.preview!(timberConfig, buildDir);
138
138
  return;
@@ -189,9 +189,16 @@ async function main(): Promise<void> {
189
189
  }
190
190
  }
191
191
 
192
- // Only run main when executed directly (not imported in tests)
192
+ // Run main when executed as a CLI (not imported in tests).
193
+ // The bin shim (bin/timber.mjs) does `import '../dist/cli.js'`, so
194
+ // process.argv[1] points to the shim, not this file. We check both:
195
+ // direct execution AND being imported by the timber bin shim.
193
196
  const isDirectExecution =
194
- typeof process !== 'undefined' && process.argv[1] && import.meta.url.endsWith(process.argv[1]);
197
+ typeof process !== 'undefined' &&
198
+ process.argv[1] &&
199
+ (import.meta.url.endsWith(process.argv[1]) ||
200
+ process.argv[1].endsWith('bin/timber.mjs') ||
201
+ process.argv[1].endsWith('bin/timber'));
195
202
 
196
203
  if (isDirectExecution) {
197
204
  main().catch((err) => {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Dev-only browser helpers — server log replay and client error forwarding.
3
+ *
4
+ * These are only active when import.meta.hot is available (Vite dev mode).
5
+ * Extracted from browser-entry.ts to keep files under 500 lines.
6
+ *
7
+ * See design/21-dev-server.md §"HMR Wiring"
8
+ */
9
+
10
+ import { isPageUnloading } from './unload-guard.js';
11
+
12
+ // ─── HMR Hot Interface ──────────────────────────────────────────────
13
+
14
+ /** Minimal interface for Vite's HMR channel. */
15
+ export interface HotInterface {
16
+ on(event: string, cb: (...args: unknown[]) => void): void;
17
+ send(event: string, data: unknown): void;
18
+ }
19
+
20
+ // ─── Server Log Replay ──────────────────────────────────────────────
21
+
22
+ /** Payload shape from plugins/dev-logs.ts */
23
+ interface ServerLogPayload {
24
+ level: 'log' | 'warn' | 'error' | 'debug' | 'info';
25
+ args: unknown[];
26
+ location: string | null;
27
+ timestamp: number;
28
+ }
29
+
30
+ /**
31
+ * Deserialize a serialized arg back into a console-friendly value.
32
+ *
33
+ * Handles Error objects (serialized as { __type: 'Error', ... }),
34
+ * Maps, Sets, and passes everything else through.
35
+ */
36
+ function deserializeArg(arg: unknown): unknown {
37
+ if (arg === '[undefined]') return undefined;
38
+ if (arg === null || typeof arg !== 'object') return arg;
39
+
40
+ const obj = arg as Record<string, unknown>;
41
+
42
+ if (obj.__type === 'Error') {
43
+ const err = new Error(obj.message as string);
44
+ err.name = obj.name as string;
45
+ if (obj.stack) err.stack = obj.stack as string;
46
+ return err;
47
+ }
48
+
49
+ if (obj.__type === 'Map') {
50
+ return new Map(
51
+ Object.entries(obj.entries as Record<string, unknown>).map(([k, v]) => [k, deserializeArg(v)])
52
+ );
53
+ }
54
+
55
+ if (obj.__type === 'Set') {
56
+ return new Set((obj.values as unknown[]).map(deserializeArg));
57
+ }
58
+
59
+ if (Array.isArray(arg)) {
60
+ return arg.map(deserializeArg);
61
+ }
62
+
63
+ // Plain object — recurse
64
+ const result: Record<string, unknown> = {};
65
+ for (const [key, value] of Object.entries(obj)) {
66
+ result[key] = deserializeArg(value);
67
+ }
68
+ return result;
69
+ }
70
+
71
+ /**
72
+ * Set up the HMR listener that replays server console output in the browser.
73
+ *
74
+ * Each message arrives with a log level and serialized args. We prepend
75
+ * a styled "[SERVER]" badge and call the matching console method.
76
+ */
77
+ export function setupServerLogReplay(hot: Pick<HotInterface, 'on'>): void {
78
+ /** CSS styles for the [SERVER] badge in browser console. */
79
+ const BADGE_STYLES: Record<string, string> = {
80
+ log: 'background: #0070f3; color: white; padding: 1px 5px; border-radius: 3px; font-weight: bold;',
81
+ info: 'background: #0070f3; color: white; padding: 1px 5px; border-radius: 3px; font-weight: bold;',
82
+ warn: 'background: #f5a623; color: white; padding: 1px 5px; border-radius: 3px; font-weight: bold;',
83
+ error:
84
+ 'background: #e00; color: white; padding: 1px 5px; border-radius: 3px; font-weight: bold;',
85
+ debug:
86
+ 'background: #666; color: white; padding: 1px 5px; border-radius: 3px; font-weight: bold;',
87
+ };
88
+
89
+ hot.on('timber:server-log', (data: unknown) => {
90
+ const payload = data as ServerLogPayload;
91
+ const level = payload.level;
92
+ const fn = console[level] ?? console.log;
93
+ const args = payload.args.map(deserializeArg);
94
+
95
+ const badge = `%cSERVER`;
96
+ const style = BADGE_STYLES[level] ?? BADGE_STYLES.log;
97
+ const locationSuffix = payload.location ? ` (${payload.location})` : '';
98
+
99
+ fn.call(console, badge, style, ...args, locationSuffix ? `\n → ${payload.location}` : '');
100
+ });
101
+ }
102
+
103
+ // ─── Client Error Forwarding ────────────────────────────────────────
104
+
105
+ /**
106
+ * Set up global error handlers that forward uncaught client-side
107
+ * errors to the dev server via Vite's HMR channel.
108
+ *
109
+ * The server receives 'timber:client-error' events, and echoes them
110
+ * back as Vite '{ type: "error" }' payloads to trigger the overlay.
111
+ */
112
+ export function setupClientErrorForwarding(hot: Pick<HotInterface, 'send'>): void {
113
+ window.addEventListener('error', (event: ErrorEvent) => {
114
+ // Skip errors without useful information
115
+ if (!event.error && !event.message) return;
116
+ // Skip errors during page unload — these are abort-related, not application errors
117
+ if (isPageUnloading()) return;
118
+
119
+ const error = event.error;
120
+ hot.send('timber:client-error', {
121
+ message: error?.message ?? event.message,
122
+ stack: error?.stack ?? '',
123
+ componentStack: error?.componentStack ?? null,
124
+ });
125
+ });
126
+
127
+ window.addEventListener('unhandledrejection', (event: PromiseRejectionEvent) => {
128
+ const reason = event.reason;
129
+ if (!reason) return;
130
+ // Skip rejections during page unload — aborted fetches/streams cause these
131
+ if (isPageUnloading()) return;
132
+
133
+ const message = reason instanceof Error ? reason.message : String(reason);
134
+ const stack = reason instanceof Error ? (reason.stack ?? '') : '';
135
+
136
+ hot.send('timber:client-error', {
137
+ message,
138
+ stack,
139
+ componentStack: null,
140
+ });
141
+ });
142
+ }