@timber-js/app 0.2.0-alpha.197 → 0.2.0-alpha.199
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/{actions-BS-m5SLv.js → actions-d1hCqnU3.js} +35 -8
- package/dist/_chunks/actions-d1hCqnU3.js.map +1 -0
- package/dist/_chunks/als-registry-C6kcfprT.js.map +1 -1
- package/dist/_chunks/{cache-api-DqzgTEqk.js → cache-api-ByagcC-J.js} +2 -2
- package/dist/_chunks/{cache-api-DqzgTEqk.js.map → cache-api-ByagcC-J.js.map} +1 -1
- package/dist/_chunks/canonicalize-CgHoscYO.js +66 -0
- package/dist/_chunks/canonicalize-CgHoscYO.js.map +1 -0
- package/dist/_chunks/{chains-CZG7E5zg.js → chains-Bpb0W4ax.js} +3 -3
- package/dist/_chunks/{chains-CZG7E5zg.js.map → chains-Bpb0W4ax.js.map} +1 -1
- package/dist/_chunks/{cli-check-bE3H5Bjr.js → cli-check-D6VolrDV.js} +3 -3
- package/dist/_chunks/{cli-check-bE3H5Bjr.js.map → cli-check-D6VolrDV.js.map} +1 -1
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js → cli-schema-sync-D6rO-VcS.js} +2 -2
- package/dist/_chunks/{cli-schema-sync-DTy_-Msq.js.map → cli-schema-sync-D6rO-VcS.js.map} +1 -1
- package/dist/_chunks/{convention-lint-n3RJLgww.js → convention-lint-fRkwVwEH.js} +27 -4
- package/dist/_chunks/convention-lint-fRkwVwEH.js.map +1 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js +1050 -0
- package/dist/_chunks/error-boundary-BfPHZjm0.js.map +1 -0
- package/dist/_chunks/{live-graph-BXDsdzBv.js → live-graph-D_2D32Ad.js} +3 -3
- package/dist/_chunks/{live-graph-BXDsdzBv.js.map → live-graph-D_2D32Ad.js.map} +1 -1
- package/dist/_chunks/{logger-DDirEsn7.js → logger-uLBuGKDI.js} +471 -440
- package/dist/_chunks/logger-uLBuGKDI.js.map +1 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js → navigation-context-D0TU0Jog.js} +3 -101
- package/dist/_chunks/navigation-context-D0TU0Jog.js.map +1 -0
- package/dist/_chunks/navigation-root-mHSK9psY.js +126 -0
- package/dist/_chunks/{navigation-root-B00jjGd5.js.map → navigation-root-mHSK9psY.js.map} +1 -1
- package/dist/_chunks/{poison-scan-BoDLgbix.js → poison-scan-Bm9Yyqk9.js} +2 -2
- package/dist/_chunks/{poison-scan-BoDLgbix.js.map → poison-scan-Bm9Yyqk9.js.map} +1 -1
- package/dist/_chunks/{scanner-tdFPvDYi.js → scanner-AiazgH_f.js} +6 -5
- package/dist/_chunks/scanner-AiazgH_f.js.map +1 -0
- package/dist/_chunks/{segment-keys-BhqoHiLc.js → segment-keys-lqtdookO.js} +2 -65
- package/dist/_chunks/segment-keys-lqtdookO.js.map +1 -0
- package/dist/_chunks/{ssr-data-BQGhTPAK.js → ssr-data-D6T6Y3ef.js} +4 -26
- package/dist/_chunks/ssr-data-D6T6Y3ef.js.map +1 -0
- package/dist/_chunks/state-FippDgxN.js +52 -0
- package/dist/_chunks/state-FippDgxN.js.map +1 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js +496 -0
- package/dist/_chunks/status-page-marker-DwQBrLBz.js.map +1 -0
- package/dist/_chunks/{walkers-DNX05dC0.js → walkers-B6XUtmqK.js} +2 -2
- package/dist/_chunks/{walkers-DNX05dC0.js.map → walkers-B6XUtmqK.js.map} +1 -1
- package/dist/analyze/crawl-entry.js +2 -2
- package/dist/analyze/graph-command.js +2 -2
- package/dist/cache/index.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/client/browser-entry/action-dispatch.d.ts +6 -4
- package/dist/client/browser-entry/action-dispatch.d.ts.map +1 -1
- package/dist/client/browser-entry/action-queue.d.ts +44 -0
- package/dist/client/browser-entry/action-queue.d.ts.map +1 -0
- package/dist/client/browser-entry/router-init.d.ts.map +1 -1
- package/dist/client/deny-last-resort.d.ts +29 -0
- package/dist/client/deny-last-resort.d.ts.map +1 -0
- package/dist/client/error-boundary.d.ts +48 -2
- package/dist/client/error-boundary.d.ts.map +1 -1
- package/dist/client/error-boundary.js +2 -2
- package/dist/client/history.d.ts +21 -2
- package/dist/client/history.d.ts.map +1 -1
- package/dist/client/index.js +34 -14
- package/dist/client/index.js.map +1 -1
- package/dist/client/internal.d.ts +1 -0
- package/dist/client/internal.d.ts.map +1 -1
- package/dist/client/internal.js +272 -1225
- package/dist/client/internal.js.map +1 -1
- package/dist/client/link.d.ts.map +1 -1
- package/dist/client/navigation-commit.d.ts +12 -19
- package/dist/client/navigation-commit.d.ts.map +1 -1
- package/dist/client/navigation-transition.d.ts +62 -11
- package/dist/client/navigation-transition.d.ts.map +1 -1
- package/dist/client/router-effects.d.ts +9 -8
- package/dist/client/router-effects.d.ts.map +1 -1
- package/dist/client/router-lifecycle.d.ts +60 -17
- package/dist/client/router-lifecycle.d.ts.map +1 -1
- package/dist/client/router-pipeline.d.ts +7 -4
- package/dist/client/router-pipeline.d.ts.map +1 -1
- package/dist/client/router-types.d.ts +63 -8
- package/dist/client/router-types.d.ts.map +1 -1
- package/dist/client/router.d.ts.map +1 -1
- package/dist/client/rsc-fetch.d.ts +0 -9
- package/dist/client/rsc-fetch.d.ts.map +1 -1
- package/dist/client/segment-cache.d.ts +23 -8
- package/dist/client/segment-cache.d.ts.map +1 -1
- package/dist/client/state.d.ts +16 -0
- package/dist/client/state.d.ts.map +1 -1
- package/dist/client/status-page-marker.d.ts +25 -0
- package/dist/client/status-page-marker.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/dev-tools/holding-server.d.ts +4 -17
- package/dist/dev-tools/holding-server.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +74 -175
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/routing/codegen-write.d.ts.map +1 -1
- package/dist/routing/index.js +2 -2
- package/dist/routing/interception.d.ts +2 -2
- package/dist/routing/slot-placement.d.ts +2 -2
- package/dist/server/access-gate.d.ts +73 -1
- package/dist/server/access-gate.d.ts.map +1 -1
- package/dist/server/action-handler.d.ts.map +1 -1
- package/dist/server/actions.d.ts +16 -1
- package/dist/server/actions.d.ts.map +1 -1
- package/dist/server/als-registry.d.ts +3 -9
- package/dist/server/als-registry.d.ts.map +1 -1
- package/dist/server/children-interception.d.ts +1 -1
- package/dist/server/default-status-page.d.ts +2 -2
- package/dist/server/default-status-page.d.ts.map +1 -1
- package/dist/server/deny-boundary.d.ts +15 -9
- package/dist/server/deny-boundary.d.ts.map +1 -1
- package/dist/server/deny-renderer.d.ts.map +1 -1
- package/dist/server/error-boundary-wrapper.d.ts +21 -4
- package/dist/server/error-boundary-wrapper.d.ts.map +1 -1
- package/dist/server/error-response-headers.d.ts +3 -0
- package/dist/server/error-response-headers.d.ts.map +1 -0
- package/dist/server/index.js +3 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.d.ts +1 -2
- package/dist/server/internal.d.ts.map +1 -1
- package/dist/server/internal.js +2339 -2506
- package/dist/server/internal.js.map +1 -1
- package/dist/server/metadata-collector.d.ts +2 -5
- package/dist/server/metadata-collector.d.ts.map +1 -1
- package/dist/server/param-coercion.d.ts +10 -3
- package/dist/server/param-coercion.d.ts.map +1 -1
- package/dist/server/pipeline-outcome.d.ts.map +1 -1
- package/dist/server/pipeline-phases.d.ts +11 -0
- package/dist/server/pipeline-phases.d.ts.map +1 -1
- package/dist/server/port-resolution.d.ts +3 -89
- package/dist/server/port-resolution.d.ts.map +1 -1
- package/dist/server/primitives.d.ts +38 -10
- package/dist/server/primitives.d.ts.map +1 -1
- package/dist/server/response-cache-policy.d.ts +3 -0
- package/dist/server/response-cache-policy.d.ts.map +1 -0
- package/dist/server/route-element-builder.d.ts +11 -41
- package/dist/server/route-element-builder.d.ts.map +1 -1
- package/dist/server/route-element-helpers.d.ts +12 -0
- package/dist/server/route-element-helpers.d.ts.map +1 -0
- package/dist/server/route-module-loader.d.ts +37 -0
- package/dist/server/route-module-loader.d.ts.map +1 -0
- package/dist/server/rsc-cache-key-guard.d.ts.map +1 -1
- package/dist/server/rsc-entry/action-middleware-runner.d.ts.map +1 -1
- package/dist/server/rsc-entry/error-renderer.d.ts.map +1 -1
- package/dist/server/rsc-entry/render-route.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts +22 -1
- package/dist/server/rsc-entry/rsc-payload.d.ts.map +1 -1
- package/dist/server/rsc-entry/rsc-stream.d.ts +4 -11
- package/dist/server/rsc-entry/rsc-stream.d.ts.map +1 -1
- package/dist/server/rsc-entry/ssr-renderer.d.ts.map +1 -1
- package/dist/server/rsc-error-envelope.d.ts +11 -0
- package/dist/server/rsc-error-envelope.d.ts.map +1 -0
- package/dist/server/skippable-prefix.d.ts +18 -15
- package/dist/server/skippable-prefix.d.ts.map +1 -1
- package/dist/server/slot-resolver.d.ts.map +1 -1
- package/dist/server/slot-subtree-contain.d.ts +54 -0
- package/dist/server/slot-subtree-contain.d.ts.map +1 -0
- package/dist/server/stream-utils.d.ts.map +1 -1
- package/dist/server/utils/element-type.d.ts +10 -0
- package/dist/server/utils/element-type.d.ts.map +1 -1
- package/dist/shared/rsc-error-envelope.d.ts +0 -9
- package/dist/shared/rsc-error-envelope.d.ts.map +1 -1
- package/dist/shared/status-reason-phrase.d.ts +26 -0
- package/dist/shared/status-reason-phrase.d.ts.map +1 -0
- package/docs/api/30-api-server.mdx +4 -2
- package/docs/api/31-api-client.mdx +5 -1
- package/docs/api/36-cli.mdx +5 -3
- package/docs/learn/12-error-handling.mdx +5 -1
- package/package.json +10 -10
- package/src/client/browser-entry/action-dispatch.ts +166 -99
- package/src/client/browser-entry/action-queue.ts +90 -0
- package/src/client/browser-entry/router-init.ts +60 -35
- package/src/client/deny-last-resort.tsx +54 -0
- package/src/client/error-boundary.tsx +144 -42
- package/src/client/history.ts +52 -3
- package/src/client/internal.ts +1 -0
- package/src/client/link.tsx +70 -35
- package/src/client/navigation-commit.ts +79 -27
- package/src/client/navigation-transition.ts +176 -127
- package/src/client/router-effects.ts +14 -17
- package/src/client/router-lifecycle.ts +181 -115
- package/src/client/router-pipeline.ts +94 -71
- package/src/client/router-types.ts +61 -7
- package/src/client/router.ts +147 -74
- package/src/client/rsc-fetch.ts +0 -13
- package/src/client/segment-cache.ts +43 -10
- package/src/client/state.ts +26 -0
- package/src/client/status-page-marker.tsx +32 -0
- package/src/dev-tools/holding-server.ts +4 -17
- package/src/index.ts +18 -34
- package/src/plugins/dev-server.ts +2 -1
- package/src/react-canary.d.ts +2 -0
- package/src/routing/codegen-write.ts +2 -0
- package/src/routing/interception.ts +2 -2
- package/src/routing/slot-placement.ts +2 -2
- package/src/server/access-gate.tsx +89 -21
- package/src/server/action-client.ts +2 -2
- package/src/server/action-handler.ts +23 -10
- package/src/server/actions.ts +81 -34
- package/src/server/als-registry.ts +3 -9
- package/src/server/children-interception.ts +1 -1
- package/src/server/default-status-page.ts +7 -47
- package/src/server/deny-boundary.ts +45 -28
- package/src/server/deny-renderer.ts +6 -2
- package/src/server/error-boundary-wrapper.ts +23 -4
- package/src/server/error-response-headers.ts +18 -0
- package/src/server/internal.ts +2 -10
- package/src/server/metadata-collector.ts +3 -18
- package/src/server/param-coercion.ts +13 -4
- package/src/server/pipeline-outcome.ts +35 -13
- package/src/server/pipeline-phases.ts +22 -16
- package/src/server/port-resolution.ts +3 -165
- package/src/server/prebuilt-builder.ts +4 -4
- package/src/server/primitives.ts +75 -11
- package/src/server/response-cache-policy.ts +45 -0
- package/src/server/route-element-builder.ts +149 -412
- package/src/server/route-element-helpers.ts +37 -0
- package/src/server/route-handler.ts +2 -2
- package/src/server/route-module-loader.ts +161 -0
- package/src/server/rsc-cache-key-guard.ts +2 -42
- package/src/server/rsc-entry/action-middleware-runner.ts +4 -4
- package/src/server/rsc-entry/api-handler.ts +5 -5
- package/src/server/rsc-entry/error-renderer.ts +3 -4
- package/src/server/rsc-entry/helpers.ts +1 -1
- package/src/server/rsc-entry/index.ts +3 -3
- package/src/server/rsc-entry/render-route.ts +4 -8
- package/src/server/rsc-entry/rsc-payload.ts +59 -42
- package/src/server/rsc-entry/rsc-stream.ts +48 -27
- package/src/server/rsc-entry/ssr-renderer.ts +6 -10
- package/src/server/rsc-error-envelope.ts +18 -0
- package/src/server/skippable-prefix.ts +105 -7
- package/src/server/slot-resolver.ts +43 -12
- package/src/server/slot-subtree-contain.ts +255 -0
- package/src/server/stream-utils.ts +12 -8
- package/src/server/utils/element-type.ts +18 -2
- package/src/shared/rsc-error-envelope.ts +0 -15
- package/src/shared/status-reason-phrase.ts +61 -0
- package/dist/_chunks/actions-BS-m5SLv.js.map +0 -1
- package/dist/_chunks/convention-lint-n3RJLgww.js.map +0 -1
- package/dist/_chunks/error-boundary-BvRCCmbN.js +0 -353
- package/dist/_chunks/error-boundary-BvRCCmbN.js.map +0 -1
- package/dist/_chunks/logger-DDirEsn7.js.map +0 -1
- package/dist/_chunks/mdx-file-CXyHGUpS.js +0 -25
- package/dist/_chunks/mdx-file-CXyHGUpS.js.map +0 -1
- package/dist/_chunks/router-ref-8gr8qsxN.js +0 -28
- package/dist/_chunks/router-ref-8gr8qsxN.js.map +0 -1
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js +0 -40
- package/dist/_chunks/rsc-error-envelope-tT5PJs4q.js.map +0 -1
- package/dist/_chunks/scanner-tdFPvDYi.js.map +0 -1
- package/dist/_chunks/segment-keys-BhqoHiLc.js.map +0 -1
- package/dist/_chunks/ssr-data-BQGhTPAK.js.map +0 -1
- package/dist/server/tree-builder.d.ts +0 -150
- package/dist/server/tree-builder.d.ts.map +0 -1
- package/src/server/tree-builder.ts +0 -313
|
@@ -1,34 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Port resolution for the dev server, Vite preview, and the Node
|
|
3
|
-
* production preview server.
|
|
4
|
-
*
|
|
5
|
-
* Behavior (see TIM-842):
|
|
6
|
-
*
|
|
7
|
-
* 1. Default port is **3000** for both dev and prod.
|
|
8
|
-
* 2. If the user did NOT set an explicit port, auto-bump from 3000
|
|
9
|
-
* until a free port is found (3000 → 3001 → 3002 → …).
|
|
10
|
-
* 3. If the user DID set an explicit port (via `--port`, `PORT` env
|
|
11
|
-
* var, or `vite.config.ts` `server.port`), use it as-is and let
|
|
12
|
-
* the bind fail loudly on conflict (`strictPort: true`).
|
|
13
|
-
*
|
|
14
|
-
* The port-bump probe is performed by binding the **actual** server
|
|
15
|
-
* (e.g. the dev holding server) — not a throwaway probe — so there is
|
|
16
|
-
* no time-of-check / time-of-use race between probing and listening.
|
|
17
|
-
*
|
|
18
|
-
* Design doc: 21-dev-server.md §"Default Port and Auto-Bump".
|
|
19
|
-
*/
|
|
1
|
+
/** Port input precedence for dev and preview. Vite owns binding and restart reuse. */
|
|
20
2
|
|
|
21
3
|
/** Default port used by `timber dev` and `timber preview`. */
|
|
22
4
|
export const DEFAULT_PORT = 3000;
|
|
23
5
|
|
|
24
|
-
/**
|
|
25
|
-
* A function that attempts to bind to a given port.
|
|
26
|
-
*
|
|
27
|
-
* Resolves with the bound port on success. Rejects with an error
|
|
28
|
-
* (typically with `code === 'EADDRINUSE'`) on failure.
|
|
29
|
-
*/
|
|
30
|
-
export type ListenFn = (port: number) => Promise<number>;
|
|
31
|
-
|
|
32
6
|
// ── Pure resolution ────────────────────────────────────────────────────
|
|
33
7
|
|
|
34
8
|
export interface ResolvePortInput {
|
|
@@ -49,8 +23,7 @@ export interface ResolvedPortInput {
|
|
|
49
23
|
port: number;
|
|
50
24
|
/**
|
|
51
25
|
* `true` if the port came from an explicit user override (config or
|
|
52
|
-
* env).
|
|
53
|
-
* `EADDRINUSE` to the user.
|
|
26
|
+
* env). Preview uses this to default to strict port binding.
|
|
54
27
|
*/
|
|
55
28
|
explicit: boolean;
|
|
56
29
|
}
|
|
@@ -58,7 +31,7 @@ export interface ResolvedPortInput {
|
|
|
58
31
|
/**
|
|
59
32
|
* Pure: compute the starting port from config / env / defaults.
|
|
60
33
|
*
|
|
61
|
-
* Performs no I/O
|
|
34
|
+
* Performs no I/O.
|
|
62
35
|
*/
|
|
63
36
|
export function resolveStartPort(input: ResolvePortInput): ResolvedPortInput {
|
|
64
37
|
const defaultPort = input.defaultPort ?? DEFAULT_PORT;
|
|
@@ -78,138 +51,3 @@ export function resolveStartPort(input: ResolvePortInput): ResolvedPortInput {
|
|
|
78
51
|
|
|
79
52
|
return { port: defaultPort, explicit: false };
|
|
80
53
|
}
|
|
81
|
-
|
|
82
|
-
// ── Bind with auto-bump ────────────────────────────────────────────────
|
|
83
|
-
|
|
84
|
-
export interface BindWithBumpOptions {
|
|
85
|
-
/** First port to attempt. */
|
|
86
|
-
startPort: number;
|
|
87
|
-
/**
|
|
88
|
-
* If `true`, increment the port and retry on `EADDRINUSE`. If
|
|
89
|
-
* `false`, attempt once and let the error propagate.
|
|
90
|
-
*/
|
|
91
|
-
autoBump: boolean;
|
|
92
|
-
/** Maximum number of port attempts when `autoBump` is `true`. */
|
|
93
|
-
maxAttempts?: number;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface BindWithBumpResult {
|
|
97
|
-
/** Port that was actually bound. */
|
|
98
|
-
port: number;
|
|
99
|
-
/** `true` if the bound port differs from `startPort`. */
|
|
100
|
-
bumped: boolean;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Bind a server starting at `startPort`, optionally bumping the port
|
|
105
|
-
* on `EADDRINUSE` until a free one is found.
|
|
106
|
-
*
|
|
107
|
-
* Use this with the actual server you intend to keep listening (e.g.
|
|
108
|
-
* the dev holding server). Pairing the probe with the real listen
|
|
109
|
-
* eliminates the TOCTOU race that a throwaway probe would introduce.
|
|
110
|
-
*/
|
|
111
|
-
export async function bindWithBump(
|
|
112
|
-
listen: ListenFn,
|
|
113
|
-
options: BindWithBumpOptions
|
|
114
|
-
): Promise<BindWithBumpResult> {
|
|
115
|
-
const maxAttempts = options.autoBump ? (options.maxAttempts ?? 100) : 1;
|
|
116
|
-
let lastErr: unknown = null;
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
119
|
-
const port = options.startPort + i;
|
|
120
|
-
try {
|
|
121
|
-
const bound = await listen(port);
|
|
122
|
-
return { port: bound, bumped: i > 0 };
|
|
123
|
-
} catch (err) {
|
|
124
|
-
lastErr = err;
|
|
125
|
-
// Only retry on EADDRINUSE — other errors (EACCES, etc.) are
|
|
126
|
-
// permanent and must surface immediately.
|
|
127
|
-
if (!isAddrInUse(err)) throw err;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
throw lastErr ?? new Error(`Could not bind to a free port starting at ${options.startPort}`);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** True if `err` is a Node `EADDRINUSE` error from `server.listen()`. */
|
|
135
|
-
export function isAddrInUse(err: unknown): boolean {
|
|
136
|
-
return (
|
|
137
|
-
typeof err === 'object' && err !== null && (err as { code?: string }).code === 'EADDRINUSE'
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// ── High-level helper used by the rootSync plugin ────────────────────
|
|
142
|
-
|
|
143
|
-
export interface StartDevServerPortInput {
|
|
144
|
-
/** Resolved port from `userConfig.server?.port` (or `--port`), or undefined. */
|
|
145
|
-
configPort: number | undefined;
|
|
146
|
-
/** Raw `process.env.PORT` value. */
|
|
147
|
-
envPort: string | undefined;
|
|
148
|
-
/** ListenFn for the holding server (or any pre-bind probe target). */
|
|
149
|
-
listen: ListenFn;
|
|
150
|
-
/** Logger — defaults to `console`. Injected for tests. */
|
|
151
|
-
log?: (msg: string) => void;
|
|
152
|
-
warn?: (msg: string) => void;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface StartDevServerPortResult {
|
|
156
|
-
/** Port chosen for both the holding server and Vite's dev server. */
|
|
157
|
-
port: number;
|
|
158
|
-
/** True if the user explicitly set the port (=> Vite must `strictPort: true`). */
|
|
159
|
-
explicit: boolean;
|
|
160
|
-
/** True if the holding server actually bound the port. */
|
|
161
|
-
bound: boolean;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Run the full dev-server port resolution + holding-server bind sequence.
|
|
166
|
-
*
|
|
167
|
-
* Resolves the port from config / env / default, attempts to bind the
|
|
168
|
-
* holding server (auto-bumping when the port came from the default),
|
|
169
|
-
* and logs the chosen URL. On a clean failure for an explicit port, it
|
|
170
|
-
* warns and falls back to the requested port so Vite can surface the
|
|
171
|
-
* conflict via `strictPort: true`.
|
|
172
|
-
*
|
|
173
|
-
* Extracted from `index.ts` so the rootSync `config()` hook stays
|
|
174
|
-
* focused on plugin assembly.
|
|
175
|
-
*/
|
|
176
|
-
export async function startDevServerPort(
|
|
177
|
-
input: StartDevServerPortInput
|
|
178
|
-
): Promise<StartDevServerPortResult> {
|
|
179
|
-
const log = input.log ?? ((msg: string) => console.log(msg));
|
|
180
|
-
const warn = input.warn ?? ((msg: string) => console.warn(msg));
|
|
181
|
-
|
|
182
|
-
const start = resolveStartPort({
|
|
183
|
-
configPort: input.configPort,
|
|
184
|
-
envPort: input.envPort,
|
|
185
|
-
defaultPort: DEFAULT_PORT,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
try {
|
|
189
|
-
const result = await bindWithBump(input.listen, {
|
|
190
|
-
startPort: start.port,
|
|
191
|
-
autoBump: !start.explicit,
|
|
192
|
-
});
|
|
193
|
-
if (result.bumped) {
|
|
194
|
-
log(`\n \x1b[33m[timber]\x1b[0m Port ${start.port} in use, using ${result.port}\n`);
|
|
195
|
-
}
|
|
196
|
-
log(
|
|
197
|
-
`\n \x1b[2m\u{1FAB5} timber.js dev server starting at\x1b[0m ` +
|
|
198
|
-
`\x1b[36mhttp://localhost:${result.port}\x1b[0m\n`
|
|
199
|
-
);
|
|
200
|
-
return { port: result.port, explicit: start.explicit, bound: true };
|
|
201
|
-
} catch (err) {
|
|
202
|
-
// Holding server failed to bind. For explicit ports we leave the
|
|
203
|
-
// bound state false and let Vite fail loudly via strictPort: true.
|
|
204
|
-
// For implicit ports this is essentially unreachable (auto-bump
|
|
205
|
-
// tries 100 ports), but if we hit it we still want Vite to surface
|
|
206
|
-
// the error.
|
|
207
|
-
if (start.explicit && isAddrInUse(err)) {
|
|
208
|
-
warn(
|
|
209
|
-
`\n \x1b[33m[timber]\x1b[0m Port ${start.port} is already in use. ` +
|
|
210
|
-
`Set PORT (or remove the override) to pick another port.\n`
|
|
211
|
-
);
|
|
212
|
-
}
|
|
213
|
-
return { port: start.port, explicit: start.explicit, bound: false };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
@@ -46,10 +46,10 @@ import { createElement } from 'react';
|
|
|
46
46
|
|
|
47
47
|
import { requestContextAls } from './als-registry.ts';
|
|
48
48
|
import type { CoercedParams } from '../shared/param-value.ts';
|
|
49
|
-
import { ParamCoercionError } from './
|
|
49
|
+
import { ParamCoercionError } from './param-coercion.ts';
|
|
50
50
|
import { SlotOutlet } from '../client/slot-outlet.tsx';
|
|
51
51
|
import { coerceSegmentParams } from './param-coercion.ts';
|
|
52
|
-
import {
|
|
52
|
+
import { isDenySignal, isRedirectSignal } from './primitives.ts';
|
|
53
53
|
import {
|
|
54
54
|
beginCapture,
|
|
55
55
|
endCapture,
|
|
@@ -395,8 +395,8 @@ async function renderEntry(
|
|
|
395
395
|
|
|
396
396
|
/** Human-readable classification for a failed entry. */
|
|
397
397
|
function describeFailure(error: unknown): string {
|
|
398
|
-
if (error
|
|
399
|
-
if (error
|
|
398
|
+
if (isDenySignal(error)) return `component called deny(${error.status})`;
|
|
399
|
+
if (isRedirectSignal(error)) return `component called redirect("${error.location}")`;
|
|
400
400
|
if (error instanceof Error) return error.message;
|
|
401
401
|
return String(error);
|
|
402
402
|
}
|
package/src/server/primitives.ts
CHANGED
|
@@ -103,31 +103,85 @@ function warnIfNotSerializable(data: unknown, callerName: string): void {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
// ─── Signal branding ────────────────────────────────────────────────────────
|
|
107
|
+
//
|
|
108
|
+
// Catch sites must NOT use `instanceof` on these classes. In dev, an HMR
|
|
109
|
+
// program reload clears the RSC module cache; a request that is mid-render
|
|
110
|
+
// still holds the catch sites from the old evaluation of this module while
|
|
111
|
+
// user code loaded after the reload throws from the new one. The two
|
|
112
|
+
// evaluations are distinct classes, so `instanceof` fails and a correct
|
|
113
|
+
// deny() falls through every catch site as an unhandled 500 (TIM-1499).
|
|
114
|
+
//
|
|
115
|
+
// Each class stamps a `Symbol.for` brand on its instances in the constructor.
|
|
116
|
+
// `Symbol.for` keys live in the realm-wide symbol registry, so every
|
|
117
|
+
// evaluation of this module — and every module graph in the same process —
|
|
118
|
+
// agrees on the key. The `is*` predicates below check the brand and are the
|
|
119
|
+
// only supported way to detect a signal. The classes themselves are NOT
|
|
120
|
+
// anchored on globalThis: a re-evaluated module must hand out its own
|
|
121
|
+
// (possibly updated) class, not a stale one.
|
|
122
|
+
//
|
|
123
|
+
// See design/18-build-system.md §"Dev Mode: Signal Identity Across Program Reloads".
|
|
124
|
+
|
|
125
|
+
const DENY_BRAND: unique symbol = Symbol.for('timber:signal.deny');
|
|
126
|
+
const REDIRECT_BRAND: unique symbol = Symbol.for('timber:signal.redirect');
|
|
127
|
+
const RENDER_ERROR_BRAND: unique symbol = Symbol.for('timber:signal.render-error');
|
|
128
|
+
const SSR_STREAM_ERROR_BRAND: unique symbol = Symbol.for('timber:signal.ssr-stream-error');
|
|
129
|
+
|
|
130
|
+
function hasBrand(value: unknown, brand: symbol): boolean {
|
|
131
|
+
return typeof value === 'object' && value !== null && brand in value;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** True for any `DenySignal`, regardless of which module evaluation created it. */
|
|
135
|
+
export function isDenySignal(error: unknown): error is DenySignal {
|
|
136
|
+
return hasBrand(error, DENY_BRAND);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** True for any `RedirectSignal`, regardless of which module evaluation created it. */
|
|
140
|
+
export function isRedirectSignal(error: unknown): error is RedirectSignal {
|
|
141
|
+
return hasBrand(error, REDIRECT_BRAND);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** True for any `RenderError`, regardless of which module evaluation created it. */
|
|
145
|
+
export function isRenderError(error: unknown): error is RenderError {
|
|
146
|
+
return hasBrand(error, RENDER_ERROR_BRAND);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* True for any `SsrStreamError`. This one also crosses Vite environments:
|
|
151
|
+
* it is thrown in SSR and caught in RSC, which are separate module graphs in
|
|
152
|
+
* the same process — the brand is the only identity the two share.
|
|
153
|
+
*/
|
|
154
|
+
export function isSsrStreamError(error: unknown): error is SsrStreamError {
|
|
155
|
+
return hasBrand(error, SSR_STREAM_ERROR_BRAND);
|
|
156
|
+
}
|
|
157
|
+
|
|
106
158
|
// ─── DenySignal ─────────────────────────────────────────────────────────────
|
|
107
159
|
|
|
108
160
|
/**
|
|
109
161
|
* Render-phase signal thrown by `deny()`. Caught by the framework to produce
|
|
110
162
|
* the correct HTTP status code (segment context) or graceful degradation (slot context).
|
|
163
|
+
*
|
|
164
|
+
* Detect with `isDenySignal()`, never `instanceof` — see "Signal branding" above.
|
|
111
165
|
*/
|
|
112
166
|
export class DenySignal extends Error {
|
|
167
|
+
readonly [DENY_BRAND] = true;
|
|
113
168
|
readonly status: number;
|
|
114
169
|
readonly data: JsonSerializable | undefined;
|
|
115
170
|
|
|
116
171
|
/**
|
|
117
|
-
*
|
|
118
|
-
* signal
|
|
119
|
-
* render
|
|
120
|
-
* segment renders it instead.
|
|
172
|
+
* Ordered list of segment keys that own matching deny pages for this
|
|
173
|
+
* signal, best match first. Present = addressed (a boundary should
|
|
174
|
+
* render it); absent = unplaced (the re-render fallback serves it).
|
|
121
175
|
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
176
|
+
* In-tree hoists stamp a single-element list (`[entry.ownerKey]`) —
|
|
177
|
+
* identical to the pre-TIM-1450 `ownerKey`. Late addressing (TIM-1450)
|
|
178
|
+
* stamps the full candidate list so the client can find the first
|
|
179
|
+
* *reachable* owner via its ancestry context, eliminating the
|
|
180
|
+
* owner-below-throw-site escape.
|
|
127
181
|
*
|
|
128
182
|
* See design/04-authorization.md §"Where a Deny Page Renders", TIM-1356.
|
|
129
183
|
*/
|
|
130
|
-
|
|
184
|
+
owners?: string[];
|
|
131
185
|
|
|
132
186
|
/**
|
|
133
187
|
* When true, the pipeline must respond with `rscErrorEnvelope` so the
|
|
@@ -235,8 +289,11 @@ export { RedirectType } from '../shared/redirect-type.ts';
|
|
|
235
289
|
/**
|
|
236
290
|
* Render-phase signal thrown by `redirect()` and `redirectExternal()`.
|
|
237
291
|
* Caught by the framework to produce a 3xx response or client-side navigation.
|
|
292
|
+
*
|
|
293
|
+
* Detect with `isRedirectSignal()`, never `instanceof` — see "Signal branding" above.
|
|
238
294
|
*/
|
|
239
295
|
export class RedirectSignal extends Error {
|
|
296
|
+
readonly [REDIRECT_BRAND] = true;
|
|
240
297
|
readonly location: string;
|
|
241
298
|
readonly status: number;
|
|
242
299
|
|
|
@@ -258,7 +315,7 @@ export class RedirectSignal extends Error {
|
|
|
258
315
|
* See also: isFrameworkSignalError() in client/browser-dev.ts (client-only).
|
|
259
316
|
*/
|
|
260
317
|
export function isControlFlowSignal(error: unknown): boolean {
|
|
261
|
-
if (error
|
|
318
|
+
if (isDenySignal(error) || isRedirectSignal(error)) {
|
|
262
319
|
return true;
|
|
263
320
|
}
|
|
264
321
|
if (error && typeof error === 'object') {
|
|
@@ -396,11 +453,14 @@ export interface RenderErrorDigest<
|
|
|
396
453
|
* resourceId: params.id,
|
|
397
454
|
* })
|
|
398
455
|
* ```
|
|
456
|
+
*
|
|
457
|
+
* Detect with `isRenderError()`, never `instanceof` — see "Signal branding" above.
|
|
399
458
|
*/
|
|
400
459
|
export class RenderError<
|
|
401
460
|
TCode extends string = string,
|
|
402
461
|
TData extends JsonSerializable = JsonSerializable,
|
|
403
462
|
> extends Error {
|
|
463
|
+
readonly [RENDER_ERROR_BRAND] = true;
|
|
404
464
|
readonly code: TCode;
|
|
405
465
|
readonly digest: RenderErrorDigest<TCode, TData>;
|
|
406
466
|
readonly status: number;
|
|
@@ -474,8 +534,12 @@ export function _resetWaitUntilWarning(): void {
|
|
|
474
534
|
*
|
|
475
535
|
* Defined in primitives.ts (not ssr-entry.ts) because ssr-entry.ts imports
|
|
476
536
|
* react-dom/server which cannot be loaded in the RSC environment.
|
|
537
|
+
*
|
|
538
|
+
* Detect with `isSsrStreamError()` — it is thrown in the SSR environment and
|
|
539
|
+
* caught in RSC, so `instanceof` never matches. See "Signal branding" above.
|
|
477
540
|
*/
|
|
478
541
|
export class SsrStreamError extends Error {
|
|
542
|
+
readonly [SSR_STREAM_ERROR_BRAND] = true;
|
|
479
543
|
override readonly cause: unknown;
|
|
480
544
|
|
|
481
545
|
constructor(message: string, cause: unknown) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legacy shared-cache control fields that predate RFC 9213 and so do not
|
|
3
|
+
* follow the `<target>-Cache-Control` naming pattern — the one part of this
|
|
4
|
+
* rule that cannot be matched by shape. `X-Accel-Expires` is nginx
|
|
5
|
+
* `proxy_cache`, which gives it precedence over `Cache-Control`.
|
|
6
|
+
*
|
|
7
|
+
* A closed historical set, unlike the vendor `*-Cache-Control` names, which
|
|
8
|
+
* keep being minted and are handled by pattern below.
|
|
9
|
+
*/
|
|
10
|
+
const LEGACY_CDN_CACHE_HEADERS = ['Surrogate-Control', 'Edge-Control', 'X-Accel-Expires'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Whether a response header overrides `Cache-Control` at a shared cache.
|
|
14
|
+
*
|
|
15
|
+
* Matched by *shape*, not by vendor name. RFC 9213 defines targeted fields
|
|
16
|
+
* as `<target>-Cache-Control`, and every CDN spells its own: `CDN-`,
|
|
17
|
+
* `Cloudflare-CDN-`, `Netlify-CDN-`, `Vercel-CDN-`, and whichever ships
|
|
18
|
+
* next. A vendor list here is a denylist with an unbounded tail — it was
|
|
19
|
+
* extended twice during review, each time by one name, which is the signal
|
|
20
|
+
* that enumeration is the wrong shape. The suffix rule is total.
|
|
21
|
+
*
|
|
22
|
+
* `Cache-Control` itself does not match (no `-` prefix) and is replaced
|
|
23
|
+
* separately.
|
|
24
|
+
*/
|
|
25
|
+
function isSharedCacheOverride(name: string): boolean {
|
|
26
|
+
const lowered = name.toLowerCase();
|
|
27
|
+
return (
|
|
28
|
+
lowered.endsWith('-cache-control') ||
|
|
29
|
+
LEGACY_CDN_CACHE_HEADERS.some((legacy) => legacy.toLowerCase() === lowered)
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Disable caching, including CDN overrides that take precedence over Cache-Control. */
|
|
34
|
+
export function preventResponseCaching(headers: Headers): void {
|
|
35
|
+
// Collected before deleting — mutating Headers while iterating its live
|
|
36
|
+
// key iterator can skip entries.
|
|
37
|
+
const overrides: string[] = [];
|
|
38
|
+
for (const name of headers.keys()) {
|
|
39
|
+
if (isSharedCacheOverride(name)) overrides.push(name);
|
|
40
|
+
}
|
|
41
|
+
for (const name of overrides) {
|
|
42
|
+
headers.delete(name);
|
|
43
|
+
}
|
|
44
|
+
headers.set('Cache-Control', 'private, no-store');
|
|
45
|
+
}
|