@timber-js/app 0.2.0-alpha.97 → 0.2.0-alpha.98

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 (102) hide show
  1. package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
  2. package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
  3. package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
  4. package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
  5. package/dist/_chunks/{interception-BbqMCVXa.js → walkers-VOXgavMF.js} +61 -85
  6. package/dist/_chunks/walkers-VOXgavMF.js.map +1 -0
  7. package/dist/adapters/nitro.d.ts.map +1 -1
  8. package/dist/adapters/nitro.js +55 -5
  9. package/dist/adapters/nitro.js.map +1 -1
  10. package/dist/client/index.js +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +189 -62
  13. package/dist/index.js.map +1 -1
  14. package/dist/plugins/build-report.d.ts +6 -4
  15. package/dist/plugins/build-report.d.ts.map +1 -1
  16. package/dist/plugins/dev-404-page.d.ts +8 -18
  17. package/dist/plugins/dev-404-page.d.ts.map +1 -1
  18. package/dist/routing/index.d.ts +5 -3
  19. package/dist/routing/index.d.ts.map +1 -1
  20. package/dist/routing/index.js +3 -3
  21. package/dist/routing/scanner.d.ts +1 -10
  22. package/dist/routing/scanner.d.ts.map +1 -1
  23. package/dist/routing/segment-classify.d.ts +37 -8
  24. package/dist/routing/segment-classify.d.ts.map +1 -1
  25. package/dist/routing/types.d.ts +63 -23
  26. package/dist/routing/types.d.ts.map +1 -1
  27. package/dist/routing/walkers.d.ts +51 -0
  28. package/dist/routing/walkers.d.ts.map +1 -0
  29. package/dist/server/action-handler.d.ts.map +1 -1
  30. package/dist/server/dev-holding-server.d.ts +4 -2
  31. package/dist/server/dev-holding-server.d.ts.map +1 -1
  32. package/dist/server/html-injector-core.d.ts +212 -0
  33. package/dist/server/html-injector-core.d.ts.map +1 -0
  34. package/dist/server/html-injectors.d.ts +59 -59
  35. package/dist/server/html-injectors.d.ts.map +1 -1
  36. package/dist/server/internal.js +710 -563
  37. package/dist/server/internal.js.map +1 -1
  38. package/dist/server/node-stream-transforms.d.ts +46 -49
  39. package/dist/server/node-stream-transforms.d.ts.map +1 -1
  40. package/dist/server/pipeline-helpers.d.ts +88 -0
  41. package/dist/server/pipeline-helpers.d.ts.map +1 -0
  42. package/dist/server/pipeline-phases.d.ts +97 -0
  43. package/dist/server/pipeline-phases.d.ts.map +1 -0
  44. package/dist/server/pipeline.d.ts +53 -32
  45. package/dist/server/pipeline.d.ts.map +1 -1
  46. package/dist/server/port-resolution.d.ts +117 -0
  47. package/dist/server/port-resolution.d.ts.map +1 -0
  48. package/dist/server/route-matcher.d.ts +20 -47
  49. package/dist/server/route-matcher.d.ts.map +1 -1
  50. package/dist/server/rsc-entry/index.d.ts.map +1 -1
  51. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +74 -0
  52. package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
  53. package/dist/server/status-code-resolver.d.ts +16 -11
  54. package/dist/server/status-code-resolver.d.ts.map +1 -1
  55. package/dist/server/tree-builder.d.ts.map +1 -1
  56. package/dist/utils/directive-parser.d.ts +0 -45
  57. package/dist/utils/directive-parser.d.ts.map +1 -1
  58. package/package.json +7 -6
  59. package/src/adapters/nitro.ts +55 -5
  60. package/src/cli.ts +0 -0
  61. package/src/index.ts +84 -31
  62. package/src/plugins/build-report.ts +13 -22
  63. package/src/plugins/dev-404-page.ts +15 -41
  64. package/src/plugins/routing.ts +14 -12
  65. package/src/routing/codegen.ts +1 -1
  66. package/src/routing/convention-lint.ts +4 -4
  67. package/src/routing/index.ts +5 -3
  68. package/src/routing/interception.ts +1 -1
  69. package/src/routing/scanner.ts +17 -93
  70. package/src/routing/segment-classify.ts +107 -8
  71. package/src/routing/status-file-lint.ts +3 -3
  72. package/src/routing/types.ts +63 -23
  73. package/src/routing/walkers.ts +90 -0
  74. package/src/server/action-handler.ts +6 -0
  75. package/src/server/deny-renderer.ts +5 -5
  76. package/src/server/dev-holding-server.ts +4 -2
  77. package/src/server/fallback-error.ts +1 -1
  78. package/src/server/html-injector-core.ts +403 -0
  79. package/src/server/html-injectors.ts +158 -297
  80. package/src/server/node-stream-transforms.ts +108 -248
  81. package/src/server/pipeline-helpers.ts +180 -0
  82. package/src/server/pipeline-phases.ts +591 -0
  83. package/src/server/pipeline.ts +76 -539
  84. package/src/server/port-resolution.ts +215 -0
  85. package/src/server/route-element-builder.ts +1 -1
  86. package/src/server/route-matcher.ts +28 -60
  87. package/src/server/rsc-entry/api-handler.ts +2 -2
  88. package/src/server/rsc-entry/error-renderer.ts +1 -1
  89. package/src/server/rsc-entry/index.ts +52 -98
  90. package/src/server/rsc-entry/wrap-action-dispatch.ts +156 -0
  91. package/src/server/sitemap-generator.ts +1 -1
  92. package/src/server/slot-resolver.ts +1 -1
  93. package/src/server/status-code-resolver.ts +112 -128
  94. package/src/server/tree-builder.ts +6 -4
  95. package/src/utils/directive-parser.ts +0 -392
  96. package/LICENSE +0 -8
  97. package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
  98. package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
  99. package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
  100. package/dist/server/manifest-status-resolver.d.ts +0 -58
  101. package/dist/server/manifest-status-resolver.d.ts.map +0 -1
  102. package/src/server/manifest-status-resolver.ts +0 -215
@@ -8,58 +8,32 @@
8
8
  * See design/07-routing.md §"Request Lifecycle"
9
9
  */
10
10
  import type { RouteMatch } from './pipeline.js';
11
+ import type { SegmentNode, RouteTree } from '../routing/types.js';
11
12
  import { type MetadataRouteType } from './metadata-routes.js';
12
13
  /** A file reference in the manifest (lazy import + path). */
13
- interface ManifestFile {
14
+ export interface ManifestFile {
14
15
  load: () => Promise<unknown>;
15
16
  filePath: string;
16
17
  }
17
- /** A segment node as it appears in the virtual:timber-route-manifest module. */
18
- export interface ManifestSegmentNode {
19
- segmentName: string;
20
- segmentType: 'static' | 'dynamic' | 'catch-all' | 'optional-catch-all' | 'group' | 'slot' | 'intercepting';
21
- urlPath: string;
22
- paramName?: string;
23
- /** For intercepting segments: the marker used, e.g. "(.)". */
24
- interceptionMarker?: '(.)' | '(..)' | '(...)' | '(..)(..)';
25
- /** For intercepting segments: the segment name after stripping the marker. */
26
- interceptedSegmentName?: string;
27
- page?: ManifestFile;
28
- layout?: ManifestFile;
29
- middleware?: ManifestFile;
30
- access?: ManifestFile;
31
- route?: ManifestFile;
32
- /** params.ts isomorphic convention file for segmentParams + searchParams definitions. */
33
- params?: ManifestFile;
34
- error?: ManifestFile;
35
- default?: ManifestFile;
36
- denied?: ManifestFile;
37
- statusFiles?: Record<string, ManifestFile>;
38
- jsonStatusFiles?: Record<string, ManifestFile>;
39
- legacyStatusFiles?: Record<string, ManifestFile>;
40
- /** Metadata route files (sitemap.ts, robots.ts, icon.tsx, etc.) keyed by base name */
41
- metadataRoutes?: Record<string, ManifestFile>;
42
- children: ManifestSegmentNode[];
43
- slots: Record<string, ManifestSegmentNode>;
44
- }
45
- /** The manifest shape from virtual:timber-route-manifest. */
46
- export interface ManifestRoot {
47
- root: ManifestSegmentNode;
48
- /**
49
- * Absolute path to the Vite project root, captured at build/load time.
50
- * Used by dev-only features (e.g., dev error page frame classification)
51
- * that need a correct project root even when CWD differs (e.g., monorepo
52
- * custom root). See TIM-807 / TIM-808.
53
- */
18
+ /**
19
+ * A segment node as it appears in the virtual:timber-route-manifest module.
20
+ *
21
+ * Type alias for `SegmentNode<ManifestFile>` the same shape used by the
22
+ * scanner, only with lazy `load` functions instead of build-time extension
23
+ * metadata.
24
+ */
25
+ export type ManifestSegmentNode = SegmentNode<ManifestFile>;
26
+ /**
27
+ * The manifest shape from virtual:timber-route-manifest.
28
+ *
29
+ * Extends `RouteTree<ManifestFile>` with `viteRoot`, the absolute path
30
+ * to the Vite project root captured at build/load time. Used by dev-only
31
+ * features (e.g., dev error page frame classification) that need a
32
+ * correct project root even when CWD differs (e.g., monorepo custom root).
33
+ * See TIM-807 / TIM-808.
34
+ */
35
+ export interface ManifestRoot extends RouteTree<ManifestFile> {
54
36
  viteRoot: string;
55
- proxy?: ManifestFile;
56
- /**
57
- * Global error page: app/global-error.{tsx,ts,jsx,js}
58
- * Tier 2 — standalone full-page replacement (no layouts) when no
59
- * segment-level error file is found. SSR-only render.
60
- * See design/10-error-handling.md §"Tier 2"
61
- */
62
- globalError?: ManifestFile;
63
37
  }
64
38
  /**
65
39
  * Create a route matcher function from a manifest.
@@ -93,5 +67,4 @@ export interface MetadataRouteMatch {
93
67
  * See design/16-metadata.md §"Metadata Routes"
94
68
  */
95
69
  export declare function createMetadataRouteMatcher(manifest: ManifestRoot): (pathname: string) => MetadataRouteMatch | null;
96
- export {};
97
70
  //# sourceMappingURL=route-matcher.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAM9B,6DAA6D;AAC7D,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,gFAAgF;AAChF,MAAM,WAAW,mBAAmB;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EACP,QAAQ,GACR,SAAS,GACT,WAAW,GACX,oBAAoB,GACpB,OAAO,GACP,MAAM,GACN,cAAc,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,UAAU,CAAC;IAC3D,8EAA8E;IAC9E,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAEhC,IAAI,CAAC,EAAE,YAAY,CAAC;IACpB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,UAAU,CAAC,EAAE,YAAY,CAAC;IAC1B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,2FAA2F;IAC3F,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC3C,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACjD,sFAAsF;IACtF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAE9C,QAAQ,EAAE,mBAAmB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;CAC5C;AAED,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,mBAAmB,CAAC;IAC1B;;;;;OAKG;IACH,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,YAAY,CAAC;CAC5B;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AAyMD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../../src/server/route-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAClE,OAAO,EAIL,KAAK,iBAAiB,EACvB,MAAM,sBAAsB,CAAC;AAW9B,6DAA6D;AAC7D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC;AAE5D;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAa,SAAQ,SAAS,CAAC,YAAY,CAAC;IAC3D,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,CAEzC;AAuMD,2CAA2C;AAC3C,MAAM,WAAW,kBAAkB;IACjC,4DAA4D;IAC5D,IAAI,EAAE,iBAAiB,CAAC;IACxB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,YAAY,CAAC;IACnB,0DAA0D;IAC1D,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;OAGG;IACH,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACxC,QAAQ,EAAE,YAAY,GACrB,CAAC,QAAQ,EAAE,MAAM,KAAK,kBAAkB,GAAG,IAAI,CAMjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAuBA,OAAO,uCAAuC,CAAC;AA0C/C,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AAmnBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BA7UrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA+UhD,wBAAiE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AAuBA,OAAO,uCAAuC,CAAC;AAwC/C,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAoCtB;;;;;;;;;GASG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,eAAe,CAAC,EAAE,mBAAmB,EAAE,KAAK,IAAI,GACtF,IAAI,CAEN;AAED;;;;;;;;;GASG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,IAAI,CAE5E;AAukBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;;AAE1D,wBAAiE"}
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Action-dispatch wrapper around the route pipeline.
3
+ *
4
+ * Extracted from rsc-entry/index.ts so the wiring can be unit-tested in
5
+ * isolation from Vite's virtual modules. The wrapper is responsible for
6
+ * three things, in order:
7
+ *
8
+ * 1. **Pipeline-boundary CSRF validation** — runs on EVERY unsafe-method
9
+ * request, before any dispatch decision. This is the only line of
10
+ * defense for `route.ts` API handlers, which never see the action
11
+ * handler. See LOCAL-773.
12
+ *
13
+ * 2. **Server action interception** — POST requests carrying an
14
+ * `x-rsc-action` header or React's `$ACTION_REF` form fields are
15
+ * handed to `handleActionRequest`, which executes the action and
16
+ * returns either an RSC response or a no-JS rerender signal.
17
+ *
18
+ * 3. **No-JS validation rerender** — when an action returns flash data
19
+ * instead of a redirect, the wrapper re-runs the page render via the
20
+ * pipeline with the post-action cookie state and `runWithFormFlash`
21
+ * so server components can read the flash. See TIM-836 / TIM-837.
22
+ *
23
+ * Anything else falls through to `pipeline(req)` for normal route handling.
24
+ *
25
+ * The wrapper takes its dependencies as parameters (no module-level
26
+ * imports of virtual modules) so tests can construct it with stub
27
+ * pipelines and stub revalidate renderers.
28
+ */
29
+ import type { BodyLimitsConfig } from '../body-limits.js';
30
+ import { type CsrfConfig } from '../csrf.js';
31
+ import type { SensitiveFieldsOption } from '../sensitive-fields.js';
32
+ import type { RevalidateRenderer } from '../actions.js';
33
+ /**
34
+ * Build a `RevalidateRenderer` for a specific request.
35
+ *
36
+ * The renderer needs to forward cookies/headers from the originating
37
+ * request when fetching the revalidated route. We pass the request in
38
+ * via a builder so the wrapper doesn't need to know about route matching,
39
+ * segment param coercion, or element building — those concerns stay in
40
+ * `rsc-entry/index.ts`.
41
+ */
42
+ export type RevalidateRendererFactory = (req: Request) => RevalidateRenderer;
43
+ /** Dependencies for the action-dispatch wrapper. */
44
+ export interface ActionDispatchDeps {
45
+ /** CSRF configuration (Origin allow-list, on/off switch). */
46
+ csrfConfig: CsrfConfig;
47
+ /** Body size limits forwarded to `handleActionRequest`. */
48
+ bodyLimits?: BodyLimitsConfig['limits'];
49
+ /** Sensitive-field deny-list forwarded to `handleActionRequest`. */
50
+ sensitiveFields?: SensitiveFieldsOption;
51
+ /** Per-request factory that builds a `RevalidateRenderer`. */
52
+ buildRevalidateRenderer: RevalidateRendererFactory;
53
+ }
54
+ /**
55
+ * Wrap a pipeline function with CSRF validation and server-action dispatch.
56
+ *
57
+ * The returned handler is the framework's outermost request entry point.
58
+ * Its responsibilities, in order:
59
+ *
60
+ * 1. CSRF (Origin/Host) validation on every unsafe-method request.
61
+ * Safe methods (GET/HEAD/OPTIONS) short-circuit inside `validateCsrf`,
62
+ * so this is a no-op for reads.
63
+ * 2. Server-action interception for POSTs that match `isActionRequest`.
64
+ * 3. No-JS validation rerender when an action returns a `FormRerender`
65
+ * signal instead of a redirect.
66
+ * 4. Otherwise, delegate to `pipeline(req)`.
67
+ *
68
+ * The duplicate `validateCsrf` call inside `handleActionRequest` is left in
69
+ * place as defense-in-depth (no-op on the happy path) so the action handler
70
+ * remains safe to call from any future entry point that bypasses this
71
+ * wrapper. See LOCAL-773.
72
+ */
73
+ export declare function wrapPipelineWithActionDispatch(pipeline: (req: Request) => Promise<Response>, deps: ActionDispatchDeps): (req: Request) => Promise<Response>;
74
+ //# sourceMappingURL=wrap-action-dispatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wrap-action-dispatch.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/wrap-action-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAC1D,OAAO,EAAgB,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE3D,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAIxD;;;;;;;;GAQG;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,GAAG,EAAE,OAAO,KAAK,kBAAkB,CAAC;AAE7E,oDAAoD;AACpD,MAAM,WAAW,kBAAkB;IACjC,6DAA6D;IAC7D,UAAU,EAAE,UAAU,CAAC;IACvB,2DAA2D;IAC3D,UAAU,CAAC,EAAE,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IACxC,oEAAoE;IACpE,eAAe,CAAC,EAAE,qBAAqB,CAAC;IACxC,8DAA8D;IAC9D,uBAAuB,EAAE,yBAAyB,CAAC;CACpD;AAID;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,8BAA8B,CAC5C,QAAQ,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,EAC7C,IAAI,EAAE,kBAAkB,GACvB,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAqErC"}
@@ -5,6 +5,12 @@
5
5
  * correct file to render by walking the fallback chain described in
6
6
  * design/10-error-handling.md §"Status-Code Files".
7
7
  *
8
+ * **Generic over `TFile`** (TIM-848). Walks `SegmentNode<TFile>` trees
9
+ * regardless of whether `TFile` is the build-time `RouteFile` or the
10
+ * runtime `ManifestFile`. Before TIM-848 there were two near-identical
11
+ * resolvers — one for the Map-based scanner output and one for the
12
+ * object-based runtime manifest. Now there is one.
13
+ *
8
14
  * Supports two format families:
9
15
  * - 'component' (default): .tsx/.jsx/.mdx status files → React rendering pipeline
10
16
  * - 'json': .json status files → raw JSON response, no React
@@ -17,24 +23,23 @@
17
23
  * Pass 3 — error.tsx (leaf → root)
18
24
  * Pass 4 — framework default (returns null)
19
25
  *
20
- * **JSON chain (4xx):**
21
- * Pass 1 — json status files (leaf → root): {status}.json → 4xx.json
26
+ * **JSON chain (4xx and 5xx):**
27
+ * Pass 1 — json status files (leaf → root): {status}.json → {category}.json
22
28
  * Pass 2 — framework default JSON (returns null, caller provides bare JSON)
23
29
  *
24
- * **5xx (component only):**
30
+ * **5xx component:**
25
31
  * Per-segment (leaf → root): {status}.tsx → 5xx.tsx → error.tsx
26
- * Then global-error.tsx (future)
27
32
  * Then framework default (returns null)
28
33
  */
29
- import type { SegmentNode, RouteFile } from '../routing/types.js';
34
+ import type { SegmentNode } from '../routing/types.js';
30
35
  /** How the status-code file was matched. */
31
36
  export type StatusFileKind = 'exact' | 'category' | 'legacy' | 'error';
32
37
  /** Response format family for status-code resolution. */
33
38
  export type StatusFileFormat = 'component' | 'json';
34
39
  /** Result of resolving a status-code file for a segment chain. */
35
- export interface StatusFileResolution {
40
+ export interface StatusFileResolution<TFile> {
36
41
  /** The matched route file. */
37
- file: RouteFile;
42
+ file: TFile;
38
43
  /** The HTTP status code (always the original status, not the file's code). */
39
44
  status: number;
40
45
  /** How the file was matched. */
@@ -45,9 +50,9 @@ export interface StatusFileResolution {
45
50
  /** How a slot denial file was matched. */
46
51
  export type SlotDeniedKind = 'denied' | 'default';
47
52
  /** Result of resolving a slot denied file. */
48
- export interface SlotDeniedResolution {
53
+ export interface SlotDeniedResolution<TFile> {
49
54
  /** The matched route file (denied.tsx or default.tsx). */
50
- file: RouteFile;
55
+ file: TFile;
51
56
  /** Slot name without @ prefix. */
52
57
  slotName: string;
53
58
  /** How the file was matched. */
@@ -64,7 +69,7 @@ export interface SlotDeniedResolution {
64
69
  * @param segments - The matched segment chain from root (index 0) to leaf (last).
65
70
  * @param format - The response format family ('component' or 'json'). Defaults to 'component'.
66
71
  */
67
- export declare function resolveStatusFile(status: number, segments: ReadonlyArray<SegmentNode>, format?: StatusFileFormat): StatusFileResolution | null;
72
+ export declare function resolveStatusFile<TFile>(status: number, segments: ReadonlyArray<SegmentNode<TFile>>, format?: StatusFileFormat): StatusFileResolution<TFile> | null;
68
73
  /**
69
74
  * Resolve the denial file for a parallel route slot.
70
75
  *
@@ -73,5 +78,5 @@ export declare function resolveStatusFile(status: number, segments: ReadonlyArra
73
78
  *
74
79
  * @param slotNode - The segment node for the slot (segmentType === 'slot').
75
80
  */
76
- export declare function resolveSlotDenied(slotNode: SegmentNode): SlotDeniedResolution | null;
81
+ export declare function resolveSlotDenied<TFile>(slotNode: SegmentNode<TFile>): SlotDeniedResolution<TFile> | null;
77
82
  //# sourceMappingURL=status-code-resolver.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"status-code-resolver.d.ts","sourceRoot":"","sources":["../../src/server/status-code-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIlE,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,CAAC;AAEZ,yDAAyD;AACzD,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEpD,kEAAkE;AAClE,MAAM,WAAW,oBAAoB;IACnC,8BAA8B;IAC9B,IAAI,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElD,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,IAAI,EAAE,SAAS,CAAC;IAChB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,IAAI,EAAE,cAAc,CAAC;CACtB;AAgBD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,EACpC,MAAM,GAAE,gBAA8B,GACrC,oBAAoB,GAAG,IAAI,CAS7B;AA2JD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,WAAW,GAAG,oBAAoB,GAAG,IAAI,CAYpF"}
1
+ {"version":3,"file":"status-code-resolver.d.ts","sourceRoot":"","sources":["../../src/server/status-code-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAIvD,4CAA4C;AAC5C,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,UAAU,GACV,QAAQ,GACR,OAAO,CAAC;AAEZ,yDAAyD;AACzD,MAAM,MAAM,gBAAgB,GAAG,WAAW,GAAG,MAAM,CAAC;AAEpD,kEAAkE;AAClE,MAAM,WAAW,oBAAoB,CAAC,KAAK;IACzC,8BAA8B;IAC9B,IAAI,EAAE,KAAK,CAAC;IACZ,8EAA8E;IAC9E,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,0CAA0C;AAC1C,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAC;AAElD,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB,CAAC,KAAK;IACzC,0DAA0D;IAC1D,IAAI,EAAE,KAAK,CAAC;IACZ,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,gCAAgC;IAChC,IAAI,EAAE,cAAc,CAAC;CACtB;AA8DD;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,aAAa,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAC3C,MAAM,GAAE,gBAA8B,GACrC,oBAAoB,CAAC,KAAK,CAAC,GAAG,IAAI,CAKpC;AA0FD;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EACrC,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,GAC3B,oBAAoB,CAAC,KAAK,CAAC,GAAG,IAAI,CAYpC"}
@@ -1 +1 @@
1
- {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIlE,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAErF,gFAAgF;AAEhF,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC;AAE/B,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACrC,GAAG,QAAQ,EAAE,OAAO,EAAE,KACnB,YAAY,CAAC;AAElB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAErD,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,YAAY,CAAC;IACzB,yCAAyC;IACzC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EACJ,MAAM,GACN,OAAO,iBAAiB,EAAE,UAAU,GACpC,OAAO,iBAAiB,EAAE,cAAc,CAAC;IAC7C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,yBAAyB,EAAE,aAAa,EAAE,CAAC;IAC9D,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wFAAwF;IACxF,eAAe,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,YAAY,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC,eAAe,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAID;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,YAAY,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAkF1F"}
1
+ {"version":3,"file":"tree-builder.d.ts","sourceRoot":"","sources":["../../src/server/tree-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIlE,mDAAmD;AACnD,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mEAAmE;IACnE,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,iDAAiD;AACjD,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,SAAS,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;AAErF,gFAAgF;AAEhF,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC;AAE/B,oFAAoF;AACpF,MAAM,MAAM,aAAa,GAAG,CAC1B,IAAI,EAAE,OAAO,EACb,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,EACrC,GAAG,QAAQ,EAAE,OAAO,EAAE,KACnB,YAAY,CAAC;AAElB;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;AAErD,0CAA0C;AAC1C,MAAM,WAAW,iBAAiB;IAChC,mDAAmD;IACnD,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,mCAAmC;IACnC,UAAU,EAAE,YAAY,CAAC;IACzB,yCAAyC;IACzC,aAAa,EAAE,aAAa,CAAC;IAC7B;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAID;;;;;;;GAOG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wEAAwE;IACxE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;;OAKG;IACH,OAAO,CAAC,EACJ,MAAM,GACN,OAAO,iBAAiB,EAAE,UAAU,GACpC,OAAO,iBAAiB,EAAE,cAAc,CAAC;IAC7C;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,yBAAyB,EAAE,aAAa,EAAE,CAAC;IAC9D,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,mBAAmB;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC;IACxB,wFAAwF;IACxF,eAAe,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,GAAG,IAAI,CAAC;IAC1D,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,aAAa,EAAE,aAAa,CAAC;IAC7B,eAAe,EAAE,YAAY,GAAG,IAAI,CAAC;IACrC,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED;;;GAGG;AACH,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACxC,eAAe,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAID;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,oEAAoE;IACpE,IAAI,EAAE,YAAY,CAAC;IACnB,gFAAgF;IAChF,UAAU,EAAE,OAAO,CAAC;CACrB;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,eAAe,CAAC,CAoF1F"}
@@ -5,10 +5,6 @@
5
5
  * avoiding false positives from regex matching inside string literals,
6
6
  * comments, or template expressions.
7
7
  *
8
- * The function-body directive detection (findFunctionsWithDirective) is a
9
- * general-purpose utility kept for future use. Custom directives like
10
- * 'use cache' are not currently implemented — see design/06-caching.md.
11
- *
12
8
  * @module
13
9
  */
14
10
  export interface FileDirective {
@@ -17,30 +13,6 @@ export interface FileDirective {
17
13
  /** 1-based line number where the directive appears */
18
14
  line: number;
19
15
  }
20
- export interface FunctionWithDirective {
21
- /** Function name (or 'default' for anonymous default exports) */
22
- name: string;
23
- /** The directive found in the function body */
24
- directive: string;
25
- /** 1-based line number of the directive */
26
- directiveLine: number;
27
- /** Start offset of the function in the source */
28
- start: number;
29
- /** End offset of the function in the source */
30
- end: number;
31
- /** Start offset of the function body block (after the '{') */
32
- bodyStart: number;
33
- /** End offset of the function body block (the '}') */
34
- bodyEnd: number;
35
- /** Content between { and } of the function body */
36
- bodyContent: string;
37
- /** 'export ', 'export default ', or '' */
38
- prefix: string;
39
- /** Whether this is an arrow function */
40
- isArrow: boolean;
41
- /** The function signature (everything before the body '{') */
42
- declaration: string;
43
- }
44
16
  /**
45
17
  * Detect a file-level directive ('use client', 'use server', etc.).
46
18
  *
@@ -53,21 +25,4 @@ export interface FunctionWithDirective {
53
25
  * Returns the first matching directive, or null if none found.
54
26
  */
55
27
  export declare function detectFileDirective(code: string, directives?: string[]): FileDirective | null;
56
- /**
57
- * Find all functions in the source code that contain a directive
58
- * (e.g. 'use cache', 'use dynamic') as their first body statement.
59
- *
60
- * Parses the source with acorn and walks the AST looking for function
61
- * declarations and arrow function expressions whose body is a
62
- * BlockStatement with a directive prologue.
63
- *
64
- * Returns an array of function info objects, sorted by position
65
- * (descending) for safe end-to-start replacement.
66
- */
67
- export declare function findFunctionsWithDirective(code: string, directive: string): FunctionWithDirective[];
68
- /**
69
- * Quick regex check for whether code contains a directive string.
70
- * Use as a fast bail-out before calling the AST-based functions.
71
- */
72
- export declare function containsDirective(code: string, directive: string): boolean;
73
28
  //# sourceMappingURL=directive-parser.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"directive-parser.d.ts","sourceRoot":"","sources":["../../src/utils/directive-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAYH,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,qBAAqB;IACpC,iEAAiE;IACjE,IAAI,EAAE,MAAM,CAAC;IACb,+CAA+C;IAC/C,SAAS,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,aAAa,EAAE,MAAM,CAAC;IACtB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,GAAG,EAAE,MAAM,CAAC;IACZ,8DAA8D;IAC9D,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,OAAO,EAAE,MAAM,CAAC;IAChB,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAC;IACpB,0CAA0C;IAC1C,MAAM,EAAE,MAAM,CAAC;IACf,wCAAwC;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,8DAA8D;IAC9D,WAAW,EAAE,MAAM,CAAC;CACrB;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAM,EAAiC,GAClD,aAAa,GAAG,IAAI,CAmCtB;AAiED;;;;;;;;;;GAUG;AACH,wBAAgB,0BAA0B,CACxC,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,GAChB,qBAAqB,EAAE,CAqBzB;AA4TD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO,CAE1E"}
1
+ {"version":3,"file":"directive-parser.d.ts","sourceRoot":"","sources":["../../src/utils/directive-parser.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,MAAM,WAAW,aAAa;IAC5B,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,IAAI,EAAE,MAAM,CAAC;CACd;AAMD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,UAAU,GAAE,MAAM,EAAiC,GAClD,aAAa,GAAG,IAAI,CAmCtB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timber-js/app",
3
- "version": "0.2.0-alpha.97",
3
+ "version": "0.2.0-alpha.98",
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",
@@ -110,6 +110,11 @@
110
110
  "publishConfig": {
111
111
  "access": "public"
112
112
  },
113
+ "scripts": {
114
+ "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
115
+ "typecheck": "tsgo --noEmit",
116
+ "prepublishOnly": "pnpm run build"
117
+ },
113
118
  "dependencies": {
114
119
  "@opentelemetry/api": "^1.9.1",
115
120
  "@opentelemetry/context-async-hooks": "^2.6.1",
@@ -152,9 +157,5 @@
152
157
  },
153
158
  "engines": {
154
159
  "node": ">=22.12.0"
155
- },
156
- "scripts": {
157
- "build": "vite build --config vite.lib.config.ts && tsc --emitDeclarationOnly --project tsconfig.json --outDir dist",
158
- "typecheck": "tsgo --noEmit"
159
160
  }
160
- }
161
+ }
@@ -486,11 +486,25 @@ const MIME_TYPES = {
486
486
  };
487
487
 
488
488
  const publicDir = join(__dirname, '${publicDir}');
489
- const port = parseInt(process.env.PORT || '3000', 10);
489
+
490
+ // Port resolution (TIM-842):
491
+ // - Default to 3000
492
+ // - If PORT env var is set, honor it strictly (fail loudly on conflict)
493
+ // - Otherwise auto-bump from 3000 until a free port is found
494
+ // Mirrors the dev server behavior so timber dev/preview/production all
495
+ // behave the same way around port selection.
496
+ const envPort = process.env.PORT ? parseInt(process.env.PORT, 10) : null;
497
+ const portIsExplicit = envPort != null && Number.isFinite(envPort) && envPort > 0;
498
+ const startPort = portIsExplicit ? envPort : 3000;
490
499
  const host = process.env.HOST || process.env.HOSTNAME || 'localhost';
491
500
 
501
+ // Set after listenWithBump() resolves so request handlers can build
502
+ // absolute URLs from the actual bound port (which may differ from
503
+ // startPort after an auto-bump).
504
+ let boundPort = startPort;
505
+
492
506
  const server = createServer(async (req, res) => {
493
- const url = new URL(req.url || '/', \`http://\${host}:\${port}\`);
507
+ const url = new URL(req.url || '/', \`http://\${host}:\${boundPort}\`);
494
508
 
495
509
  // Try serving static files from the public directory first.
496
510
  const filePath = join(publicDir, url.pathname);
@@ -631,13 +645,49 @@ const server = createServer(async (req, res) => {
631
645
  }
632
646
  });
633
647
 
634
- server.listen(port, host, () => {
648
+ function listenWithBump(port, attempt = 0) {
649
+ return new Promise((resolveListen, rejectListen) => {
650
+ const onError = (err) => {
651
+ server.removeListener('listening', onListening);
652
+ if (err && err.code === 'EADDRINUSE' && !portIsExplicit && attempt < 100) {
653
+ if (attempt === 0) {
654
+ console.log(\` [timber] Port \${port} in use, trying \${port + 1}...\`);
655
+ }
656
+ listenWithBump(port + 1, attempt + 1).then(resolveListen, rejectListen);
657
+ return;
658
+ }
659
+ rejectListen(err);
660
+ };
661
+ const onListening = () => {
662
+ server.removeListener('error', onError);
663
+ resolveListen(port);
664
+ };
665
+ server.once('error', onError);
666
+ server.once('listening', onListening);
667
+ server.listen(port, host);
668
+ });
669
+ }
670
+
671
+ try {
672
+ boundPort = await listenWithBump(startPort);
673
+ if (boundPort !== startPort) {
674
+ console.log();
675
+ console.log(\` [timber] Port \${startPort} in use, using \${boundPort}\`);
676
+ }
635
677
  console.log();
636
678
  console.log(' ⚡ timber preview server running at:');
637
679
  console.log();
638
- console.log(\` ➜ http://\${host}:\${port}\`);
680
+ console.log(\` ➜ http://\${host}:\${boundPort}\`);
639
681
  console.log();
640
- });
682
+ } catch (err) {
683
+ if (err && err.code === 'EADDRINUSE') {
684
+ console.error(\`[timber] Port \${startPort} is already in use. \` +
685
+ 'Set PORT to a free port, or unset PORT to auto-pick.');
686
+ } else {
687
+ console.error('[timber] Failed to start preview server:', err);
688
+ }
689
+ process.exit(1);
690
+ }
641
691
  `;
642
692
  }
643
693
 
package/src/cli.ts CHANGED
File without changes
package/src/index.ts CHANGED
@@ -35,6 +35,7 @@ import { timberBuildReport } from './plugins/build-report';
35
35
  import { createNoopTimer } from './utils/startup-timer';
36
36
  import { resolveEncryptionKeyExpression, shouldEnableEncryption } from './server/action-encryption';
37
37
  import { createHoldingServer } from './server/dev-holding-server.js';
38
+ import { resolveStartPort, startDevServerPort } from './server/port-resolution.js';
38
39
  import type { TimberUserConfig } from './config-types.js';
39
40
  import type { PluginContext } from './plugin-context.js';
40
41
  import {
@@ -232,7 +233,7 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
232
233
  // config() hook must NOT return a `plugins` field (Vite ignores it).
233
234
  const rootSync: Plugin = {
234
235
  name: 'timber-root-sync',
235
- config(userConfig, { command }) {
236
+ async config(userConfig, { command, isPreview }) {
236
237
  // ── Load timber.config.ts from the correct root ───────────────
237
238
  // Vite's `config` hook fires before `configResolved`. The user's
238
239
  // `root` option (if set) tells us where the project lives.
@@ -287,36 +288,60 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
287
288
  // We explicitly set `oxc.jsx.development: false` for builds so
288
289
  // the client bundle always uses jsx/jsxs from react/jsx-runtime,
289
290
  // regardless of the ambient NODE_ENV value.
290
- // ── Start holding server for dev mode ───────────────────────
291
- // Bind the port immediately so browsers see a loading page
292
- // instead of ERR_CONNECTION_REFUSED during the ~6-8s startup.
293
- // The holding server is closed in timber-dev-server's
294
- // configureServer hook (the last plugin), right before Vite
295
- // calls server.listen().
296
- // See design/21-dev-server.md §"Startup Holding Server", TIM-665.
297
- if (command === 'serve') {
298
- const port = (userConfig.server?.port as number) ?? 5173;
299
- try {
300
- ctx.holdingServer = createHoldingServer();
301
- // listen() is async but we fire-and-forget the server
302
- // starts binding immediately and will be ready well before
303
- // any browser request arrives. We can't await here because
304
- // config() must return synchronously.
305
- ctx.holdingServer.listen(port).then(
306
- (boundPort) => {
307
- const url = `http://localhost:${boundPort}`;
308
- console.log(
309
- `\n \x1b[2m🪵 timber.js dev server starting at\x1b[0m \x1b[36m${url}\x1b[0m\n`
310
- );
311
- },
312
- () => {
313
- // Port already in use — skip gracefully.
314
- ctx.holdingServer = null;
315
- }
316
- );
317
- } catch {
291
+ // ── Resolve dev/preview port (TIM-842) ───────────────────────
292
+ // Default port is 3000 with auto-bump on conflict. Explicit user
293
+ // overrides (`--port`, `PORT` env, `vite.config.ts` server.port)
294
+ // are honored as-is and fail loudly via `strictPort: true`.
295
+ //
296
+ // For dev (`command === 'serve' && !isPreview`) we start the
297
+ // holding server and use its `listen()` as the probe, pairing
298
+ // the probe with the real bind to eliminate TOCTOU races.
299
+ //
300
+ // For `vite preview` (`command === 'serve' && isPreview`) we
301
+ // resolve the config/env port but delegate the actual bind +
302
+ // auto-bump to Vite's own preview server. Starting the holding
303
+ // server in preview mode would self-conflict because the dev
304
+ // plugin's `configureServer` handoff does not run for preview;
305
+ // the holding server would keep the port occupied while Vite's
306
+ // preview tried to bind the same port.
307
+ //
308
+ // See design/21-dev-server.md §"Default Port and Auto-Bump".
309
+ let resolvedDevPort: number | null = null;
310
+ let resolvedDevPortExplicit = false;
311
+ if (command === 'serve' && !isPreview) {
312
+ ctx.holdingServer = createHoldingServer();
313
+ const holdingRef = ctx.holdingServer;
314
+ const result = await startDevServerPort({
315
+ configPort:
316
+ typeof userConfig.server?.port === 'number' ? userConfig.server.port : undefined,
317
+ envPort: process.env.PORT,
318
+ listen: (p) => holdingRef.listen(p),
319
+ });
320
+ resolvedDevPort = result.port;
321
+ resolvedDevPortExplicit = result.explicit;
322
+ if (!result.bound) {
323
+ // Holding server failed to bind. Drop the reference so the
324
+ // dev-server plugin doesn't try to close a server that was
325
+ // never listening, and let Vite surface the conflict.
318
326
  ctx.holdingServer = null;
319
327
  }
328
+ } else if (command === 'serve' && isPreview) {
329
+ // Preview mode: resolve the starting port from config/env/
330
+ // default but do NOT probe. Vite's preview server handles the
331
+ // bind itself, with `preview.strictPort` controlling whether
332
+ // an in-use port auto-bumps (implicit) or errors (explicit).
333
+ const previewConfigPort =
334
+ typeof userConfig.preview?.port === 'number'
335
+ ? userConfig.preview.port
336
+ : typeof userConfig.server?.port === 'number'
337
+ ? userConfig.server.port
338
+ : undefined;
339
+ const start = resolveStartPort({
340
+ configPort: previewConfigPort,
341
+ envPort: process.env.PORT,
342
+ });
343
+ resolvedDevPort = start.port;
344
+ resolvedDevPortExplicit = start.explicit;
320
345
  }
321
346
 
322
347
  // Return the resolved buildDir as Vite's build.outDir so the RSC
@@ -349,8 +374,36 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
349
374
  };
350
375
  }
351
376
 
352
- // Dev mode: set outDir so dev-time references are consistent
353
- return { build: { outDir: buildOutDir }, environments: envOutDirs };
377
+ // Dev/preview mode: set outDir so dev-time references are
378
+ // consistent, and surface the resolved port to Vite.
379
+ //
380
+ // For dev, we set strictPort: true so Vite uses exactly the port
381
+ // the holding server probed for — without strictPort, Vite would
382
+ // run its own auto-bump starting from `port`, which would skip
383
+ // straight past whatever holding-server-bumped port we picked.
384
+ //
385
+ // For `vite preview`, we set `preview.strictPort` based on
386
+ // whether the user explicitly set the port. Implicit (default)
387
+ // preview uses strictPort: false so Vite's preview server can
388
+ // auto-bump from 3000. Explicit preview uses strictPort: true
389
+ // so conflicts fail loudly, matching the dev behavior and the
390
+ // TIM-842 spec.
391
+ const serverConfig: { port?: number; strictPort?: boolean } = {};
392
+ const previewConfig: { port?: number; strictPort?: boolean } = {};
393
+ if (resolvedDevPort != null) {
394
+ if (!isPreview) {
395
+ serverConfig.port = resolvedDevPort;
396
+ serverConfig.strictPort = true;
397
+ }
398
+ previewConfig.port = resolvedDevPort;
399
+ previewConfig.strictPort = resolvedDevPortExplicit;
400
+ }
401
+ return {
402
+ build: { outDir: buildOutDir },
403
+ environments: envOutDirs,
404
+ server: serverConfig,
405
+ preview: previewConfig,
406
+ };
354
407
  },
355
408
  configResolved(resolved) {
356
409
  ctx.root = resolved.root;