@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.
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js → metadata-routes-BU684ls2.js} +1 -1
- package/dist/_chunks/{metadata-routes-DS3eKNmf.js.map → metadata-routes-BU684ls2.js.map} +1 -1
- package/dist/_chunks/segment-classify-BjfuctV2.js +137 -0
- package/dist/_chunks/segment-classify-BjfuctV2.js.map +1 -0
- package/dist/_chunks/{interception-BbqMCVXa.js → walkers-VOXgavMF.js} +61 -85
- package/dist/_chunks/walkers-VOXgavMF.js.map +1 -0
- package/dist/adapters/nitro.d.ts.map +1 -1
- package/dist/adapters/nitro.js +55 -5
- package/dist/adapters/nitro.js.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -62
- package/dist/index.js.map +1 -1
- package/dist/plugins/build-report.d.ts +6 -4
- package/dist/plugins/build-report.d.ts.map +1 -1
- package/dist/plugins/dev-404-page.d.ts +8 -18
- package/dist/plugins/dev-404-page.d.ts.map +1 -1
- package/dist/routing/index.d.ts +5 -3
- package/dist/routing/index.d.ts.map +1 -1
- package/dist/routing/index.js +3 -3
- package/dist/routing/scanner.d.ts +1 -10
- package/dist/routing/scanner.d.ts.map +1 -1
- package/dist/routing/segment-classify.d.ts +37 -8
- package/dist/routing/segment-classify.d.ts.map +1 -1
- package/dist/routing/types.d.ts +63 -23
- package/dist/routing/types.d.ts.map +1 -1
- package/dist/routing/walkers.d.ts +51 -0
- package/dist/routing/walkers.d.ts.map +1 -0
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/dev-holding-server.d.ts +4 -2
- package/dist/server/dev-holding-server.d.ts.map +1 -1
- package/dist/server/html-injector-core.d.ts +212 -0
- package/dist/server/html-injector-core.d.ts.map +1 -0
- package/dist/server/html-injectors.d.ts +59 -59
- package/dist/server/html-injectors.d.ts.map +1 -1
- package/dist/server/internal.js +710 -563
- package/dist/server/internal.js.map +1 -1
- package/dist/server/node-stream-transforms.d.ts +46 -49
- package/dist/server/node-stream-transforms.d.ts.map +1 -1
- package/dist/server/pipeline-helpers.d.ts +88 -0
- package/dist/server/pipeline-helpers.d.ts.map +1 -0
- package/dist/server/pipeline-phases.d.ts +97 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -0
- package/dist/server/pipeline.d.ts +53 -32
- package/dist/server/pipeline.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +117 -0
- package/dist/server/port-resolution.d.ts.map +1 -0
- package/dist/server/route-matcher.d.ts +20 -47
- package/dist/server/route-matcher.d.ts.map +1 -1
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts +74 -0
- package/dist/server/rsc-entry/wrap-action-dispatch.d.ts.map +1 -0
- package/dist/server/status-code-resolver.d.ts +16 -11
- package/dist/server/status-code-resolver.d.ts.map +1 -1
- package/dist/server/tree-builder.d.ts.map +1 -1
- package/dist/utils/directive-parser.d.ts +0 -45
- package/dist/utils/directive-parser.d.ts.map +1 -1
- package/package.json +7 -6
- package/src/adapters/nitro.ts +55 -5
- package/src/cli.ts +0 -0
- package/src/index.ts +84 -31
- package/src/plugins/build-report.ts +13 -22
- package/src/plugins/dev-404-page.ts +15 -41
- package/src/plugins/routing.ts +14 -12
- package/src/routing/codegen.ts +1 -1
- package/src/routing/convention-lint.ts +4 -4
- package/src/routing/index.ts +5 -3
- package/src/routing/interception.ts +1 -1
- package/src/routing/scanner.ts +17 -93
- package/src/routing/segment-classify.ts +107 -8
- package/src/routing/status-file-lint.ts +3 -3
- package/src/routing/types.ts +63 -23
- package/src/routing/walkers.ts +90 -0
- package/src/server/action-handler.ts +6 -0
- package/src/server/deny-renderer.ts +5 -5
- package/src/server/dev-holding-server.ts +4 -2
- package/src/server/fallback-error.ts +1 -1
- package/src/server/html-injector-core.ts +403 -0
- package/src/server/html-injectors.ts +158 -297
- package/src/server/node-stream-transforms.ts +108 -248
- package/src/server/pipeline-helpers.ts +180 -0
- package/src/server/pipeline-phases.ts +591 -0
- package/src/server/pipeline.ts +76 -539
- package/src/server/port-resolution.ts +215 -0
- package/src/server/route-element-builder.ts +1 -1
- package/src/server/route-matcher.ts +28 -60
- package/src/server/rsc-entry/api-handler.ts +2 -2
- package/src/server/rsc-entry/error-renderer.ts +1 -1
- package/src/server/rsc-entry/index.ts +52 -98
- package/src/server/rsc-entry/wrap-action-dispatch.ts +156 -0
- package/src/server/sitemap-generator.ts +1 -1
- package/src/server/slot-resolver.ts +1 -1
- package/src/server/status-code-resolver.ts +112 -128
- package/src/server/tree-builder.ts +6 -4
- package/src/utils/directive-parser.ts +0 -392
- package/LICENSE +0 -8
- package/dist/_chunks/interception-BbqMCVXa.js.map +0 -1
- package/dist/_chunks/segment-classify-BDNn6EzD.js +0 -65
- package/dist/_chunks/segment-classify-BDNn6EzD.js.map +0 -1
- package/dist/server/manifest-status-resolver.d.ts +0 -58
- package/dist/server/manifest-status-resolver.d.ts.map +0 -1
- 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
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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,
|
|
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;
|
|
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 →
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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.
|
|
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
|
+
}
|
package/src/adapters/nitro.ts
CHANGED
|
@@ -486,11 +486,25 @@ const MIME_TYPES = {
|
|
|
486
486
|
};
|
|
487
487
|
|
|
488
488
|
const publicDir = join(__dirname, '${publicDir}');
|
|
489
|
-
|
|
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}:\${
|
|
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
|
-
|
|
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}:\${
|
|
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
|
-
// ──
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
353
|
-
|
|
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;
|