@timber-js/app 0.2.0-alpha.84 → 0.2.0-alpha.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +8 -0
- package/dist/_chunks/{actions-YHRCboUO.js → actions-DLnUaR65.js} +2 -2
- package/dist/_chunks/{actions-YHRCboUO.js.map → actions-DLnUaR65.js.map} +1 -1
- package/dist/_chunks/{chunk-DYhsFzuS.js → chunk-BYIpzuS7.js} +7 -1
- package/dist/_chunks/{define-cookie-C9pquwOg.js → define-cookie-BowvzoP0.js} +4 -4
- package/dist/_chunks/{define-cookie-C9pquwOg.js.map → define-cookie-BowvzoP0.js.map} +1 -1
- package/dist/_chunks/{request-context-Dl0hXED3.js → request-context-CK5tZqIP.js} +2 -2
- package/dist/_chunks/{request-context-Dl0hXED3.js.map → request-context-CK5tZqIP.js.map} +1 -1
- package/dist/client/form.d.ts +4 -1
- package/dist/client/form.d.ts.map +1 -1
- package/dist/client/index.js +2 -2
- package/dist/client/index.js.map +1 -1
- package/dist/config-validation.d.ts +51 -0
- package/dist/config-validation.d.ts.map +1 -0
- package/dist/cookies/index.js +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1168 -51
- package/dist/index.js.map +1 -1
- package/dist/plugins/dev-404-page.d.ts +56 -0
- package/dist/plugins/dev-404-page.d.ts.map +1 -0
- package/dist/plugins/dev-error-overlay.d.ts +14 -11
- package/dist/plugins/dev-error-overlay.d.ts.map +1 -1
- package/dist/plugins/dev-error-page.d.ts +58 -0
- package/dist/plugins/dev-error-page.d.ts.map +1 -0
- package/dist/plugins/dev-server.d.ts.map +1 -1
- package/dist/plugins/dev-terminal-error.d.ts +28 -0
- package/dist/plugins/dev-terminal-error.d.ts.map +1 -0
- package/dist/plugins/entries.d.ts.map +1 -1
- package/dist/plugins/fonts.d.ts +4 -0
- package/dist/plugins/fonts.d.ts.map +1 -1
- package/dist/plugins/routing.d.ts.map +1 -1
- package/dist/routing/convention-lint.d.ts +41 -0
- package/dist/routing/convention-lint.d.ts.map +1 -0
- package/dist/server/action-client.d.ts +13 -5
- package/dist/server/action-client.d.ts.map +1 -1
- package/dist/server/fallback-error.d.ts +9 -5
- package/dist/server/fallback-error.d.ts.map +1 -1
- package/dist/server/index.js +2 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/internal.js +2 -2
- package/dist/server/rsc-entry/index.d.ts.map +1 -1
- package/package.json +6 -7
- package/src/cli.ts +0 -0
- package/src/client/form.tsx +10 -5
- package/src/config-validation.ts +299 -0
- package/src/index.ts +17 -0
- package/src/plugins/dev-404-page.ts +418 -0
- package/src/plugins/dev-error-overlay.ts +165 -54
- package/src/plugins/dev-error-page.ts +536 -0
- package/src/plugins/dev-server.ts +63 -10
- package/src/plugins/dev-terminal-error.ts +217 -0
- package/src/plugins/entries.ts +3 -0
- package/src/plugins/fonts.ts +3 -2
- package/src/plugins/routing.ts +37 -5
- package/src/routing/convention-lint.ts +356 -0
- package/src/server/action-client.ts +17 -9
- package/src/server/fallback-error.ts +39 -88
- package/src/server/rsc-entry/index.ts +34 -2
package/dist/server/internal.js
CHANGED
|
@@ -3,8 +3,8 @@ import { a as warnRedirectInSuspense, c as warnSuspenseWrappingChildren, i as wa
|
|
|
3
3
|
import { t as classifyUrlSegment } from "../_chunks/segment-classify-BDNn6EzD.js";
|
|
4
4
|
import { i as getMetadataRouteServePath, n as classifyMetadataRoute, r as getMetadataRouteAutoLink, t as METADATA_ROUTE_CONVENTIONS } from "../_chunks/metadata-routes-DS3eKNmf.js";
|
|
5
5
|
import { a as timingAls, r as requestContextAls, t as earlyHintsSenderAls } from "../_chunks/als-registry-HS0LGUl2.js";
|
|
6
|
-
import { f as runWithRequestContext, l as getSetCookieHeaders, m as setSegmentParams, p as setMutableCookieContext, t as applyRequestHeaderOverlay, u as markResponseFlushed } from "../_chunks/request-context-
|
|
7
|
-
import { l as RenderError, n as executeAction, o as DenySignal, r as isRscActionRequest, s as RedirectSignal, t as buildNoJsResponse } from "../_chunks/actions-
|
|
6
|
+
import { f as runWithRequestContext, l as getSetCookieHeaders, m as setSegmentParams, p as setMutableCookieContext, t as applyRequestHeaderOverlay, u as markResponseFlushed } from "../_chunks/request-context-CK5tZqIP.js";
|
|
7
|
+
import { l as RenderError, n as executeAction, o as DenySignal, r as isRscActionRequest, s as RedirectSignal, t as buildNoJsResponse } from "../_chunks/actions-DLnUaR65.js";
|
|
8
8
|
import { c as replaceTraceId, d as withSpan, i as getOtelTraceId, l as runWithTraceId, o as getTraceId, r as generateTraceId, s as getTraceStore, u as setSpanAttribute } from "../_chunks/tracing-CCYbKn5n.js";
|
|
9
9
|
import "../client/error-boundary.js";
|
|
10
10
|
import "../_chunks/segment-context-fHFLF1PE.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA0DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAmCtB;;;;;;;;;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;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/server/rsc-entry/index.ts"],"names":[],"mappings":"AA0DA,OAAO,EAKL,KAAK,mBAAmB,EACzB,MAAM,cAAc,CAAC;AAmCtB;;;;;;;;;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;AAggBD,OAAO,EAAE,uBAAuB,EAAE,MAAM,0BAA0B,CAAC;AAInE,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;8BA3SrC,OAAO,KAAG,OAAO,CAAC,QAAQ,CAAC;AA6ShD,wBAAiE"}
|
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.85",
|
|
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,11 +110,6 @@
|
|
|
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
|
-
},
|
|
118
113
|
"dependencies": {
|
|
119
114
|
"@opentelemetry/api": "^1.9.1",
|
|
120
115
|
"@opentelemetry/context-async-hooks": "^2.6.1",
|
|
@@ -157,5 +152,9 @@
|
|
|
157
152
|
},
|
|
158
153
|
"engines": {
|
|
159
154
|
"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"
|
|
160
159
|
}
|
|
161
|
-
}
|
|
160
|
+
}
|
package/src/cli.ts
CHANGED
|
File without changes
|
package/src/client/form.tsx
CHANGED
|
@@ -90,16 +90,21 @@ export function useActionState<TData>(
|
|
|
90
90
|
* </button>
|
|
91
91
|
* ```
|
|
92
92
|
*/
|
|
93
|
-
export function useFormAction<
|
|
94
|
-
action: ActionFn<
|
|
95
|
-
): [
|
|
93
|
+
export function useFormAction<TData = unknown, TInput = unknown>(
|
|
94
|
+
action: ActionFn<TData, TInput> | ((input: TInput) => Promise<ActionResult<TData>>)
|
|
95
|
+
): [
|
|
96
|
+
(
|
|
97
|
+
...args: undefined extends TInput ? [input?: InputHint<TInput>] : [input: InputHint<TInput>]
|
|
98
|
+
) => Promise<ActionResult<TData>>,
|
|
99
|
+
boolean,
|
|
100
|
+
] {
|
|
96
101
|
const [isPending, startTransition] = useTransition();
|
|
97
102
|
|
|
98
|
-
const execute = (input
|
|
103
|
+
const execute = (input?: InputHint<TInput>): Promise<ActionResult<TData>> => {
|
|
99
104
|
return new Promise((resolve) => {
|
|
100
105
|
startTransition(async () => {
|
|
101
106
|
const result = await (action as (input: InputHint<TInput>) => Promise<ActionResult<TData>>)(
|
|
102
|
-
input
|
|
107
|
+
input as InputHint<TInput>
|
|
103
108
|
);
|
|
104
109
|
resolve(result);
|
|
105
110
|
});
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config validation — validates timber.config.ts at startup.
|
|
3
|
+
*
|
|
4
|
+
* Runs in the plugin's configResolved hook (once, at startup/build).
|
|
5
|
+
* Each check produces a clear error message with the invalid value,
|
|
6
|
+
* what's expected, and how to fix it.
|
|
7
|
+
*
|
|
8
|
+
* Design doc: 18-build-system.md
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { TimberUserConfig } from './config-types.js';
|
|
12
|
+
|
|
13
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export interface ConfigError {
|
|
16
|
+
field: string;
|
|
17
|
+
message: string;
|
|
18
|
+
value?: unknown;
|
|
19
|
+
suggestion?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── Validation ─────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Validate a TimberUserConfig object.
|
|
26
|
+
*
|
|
27
|
+
* Returns an array of errors. Empty array means the config is valid.
|
|
28
|
+
* Does not throw — the caller decides how to surface errors.
|
|
29
|
+
*/
|
|
30
|
+
export function validateConfig(config: TimberUserConfig): ConfigError[] {
|
|
31
|
+
const errors: ConfigError[] = [];
|
|
32
|
+
|
|
33
|
+
// output
|
|
34
|
+
if (config.output !== undefined && config.output !== 'server' && config.output !== 'static') {
|
|
35
|
+
errors.push({
|
|
36
|
+
field: 'output',
|
|
37
|
+
message: `Invalid output mode: "${String(config.output)}". Must be "server" or "static".`,
|
|
38
|
+
value: config.output,
|
|
39
|
+
suggestion: 'Use output: "server" (default) or output: "static" for static site generation.',
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// pageExtensions
|
|
44
|
+
if (config.pageExtensions !== undefined) {
|
|
45
|
+
if (!Array.isArray(config.pageExtensions)) {
|
|
46
|
+
errors.push({
|
|
47
|
+
field: 'pageExtensions',
|
|
48
|
+
message: 'pageExtensions must be an array of strings.',
|
|
49
|
+
value: config.pageExtensions,
|
|
50
|
+
suggestion: 'Example: pageExtensions: ["tsx", "ts", "jsx", "js", "mdx"]',
|
|
51
|
+
});
|
|
52
|
+
} else {
|
|
53
|
+
for (const ext of config.pageExtensions) {
|
|
54
|
+
if (typeof ext !== 'string') {
|
|
55
|
+
errors.push({
|
|
56
|
+
field: 'pageExtensions',
|
|
57
|
+
message: `pageExtensions contains a non-string value: ${JSON.stringify(ext)}`,
|
|
58
|
+
value: ext,
|
|
59
|
+
});
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
if (ext.startsWith('.')) {
|
|
63
|
+
errors.push({
|
|
64
|
+
field: 'pageExtensions',
|
|
65
|
+
message: `pageExtensions should not include the leading dot: "${ext}"`,
|
|
66
|
+
value: ext,
|
|
67
|
+
suggestion: `Use "${ext.slice(1)}" instead of "${ext}".`,
|
|
68
|
+
});
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// slowRequestMs
|
|
76
|
+
if (config.slowRequestMs !== undefined) {
|
|
77
|
+
if (typeof config.slowRequestMs !== 'number' || config.slowRequestMs < 0) {
|
|
78
|
+
errors.push({
|
|
79
|
+
field: 'slowRequestMs',
|
|
80
|
+
message: `slowRequestMs must be a non-negative number (got ${JSON.stringify(config.slowRequestMs)}).`,
|
|
81
|
+
value: config.slowRequestMs,
|
|
82
|
+
suggestion: 'Use slowRequestMs: 3000 (default) or slowRequestMs: 0 to disable.',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// renderTimeoutMs
|
|
88
|
+
if (config.renderTimeoutMs !== undefined) {
|
|
89
|
+
if (typeof config.renderTimeoutMs !== 'number' || config.renderTimeoutMs < 0) {
|
|
90
|
+
errors.push({
|
|
91
|
+
field: 'renderTimeoutMs',
|
|
92
|
+
message: `renderTimeoutMs must be a non-negative number (got ${JSON.stringify(config.renderTimeoutMs)}).`,
|
|
93
|
+
value: config.renderTimeoutMs,
|
|
94
|
+
suggestion: 'Use renderTimeoutMs: 30000 (default) or renderTimeoutMs: 0 to disable.',
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// serverTiming
|
|
100
|
+
if (config.serverTiming !== undefined) {
|
|
101
|
+
if (
|
|
102
|
+
config.serverTiming !== 'detailed' &&
|
|
103
|
+
config.serverTiming !== 'total' &&
|
|
104
|
+
config.serverTiming !== false
|
|
105
|
+
) {
|
|
106
|
+
errors.push({
|
|
107
|
+
field: 'serverTiming',
|
|
108
|
+
message: `Invalid serverTiming value: ${JSON.stringify(config.serverTiming)}.`,
|
|
109
|
+
value: config.serverTiming,
|
|
110
|
+
suggestion: 'Use "detailed", "total", or false.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// devBrowserLogs
|
|
116
|
+
if (config.devBrowserLogs !== undefined) {
|
|
117
|
+
const valid = ['error', 'warn', 'info', 'none'];
|
|
118
|
+
if (!valid.includes(config.devBrowserLogs)) {
|
|
119
|
+
errors.push({
|
|
120
|
+
field: 'devBrowserLogs',
|
|
121
|
+
message: `Invalid devBrowserLogs value: ${JSON.stringify(config.devBrowserLogs)}.`,
|
|
122
|
+
value: config.devBrowserLogs,
|
|
123
|
+
suggestion: `Use one of: ${valid.map((v) => `"${v}"`).join(', ')}.`,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// sitemap
|
|
129
|
+
if (config.sitemap != null && typeof config.sitemap === 'object') {
|
|
130
|
+
if (config.sitemap.enabled && !config.sitemap.baseUrl) {
|
|
131
|
+
errors.push({
|
|
132
|
+
field: 'sitemap.baseUrl',
|
|
133
|
+
message: 'sitemap.baseUrl is required when sitemap is enabled.',
|
|
134
|
+
suggestion: 'Add sitemap: { enabled: true, baseUrl: "https://example.com" }.',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
if (
|
|
138
|
+
config.sitemap.defaultPriority !== undefined &&
|
|
139
|
+
(config.sitemap.defaultPriority < 0 || config.sitemap.defaultPriority > 1)
|
|
140
|
+
) {
|
|
141
|
+
errors.push({
|
|
142
|
+
field: 'sitemap.defaultPriority',
|
|
143
|
+
message: `sitemap.defaultPriority must be between 0.0 and 1.0 (got ${config.sitemap.defaultPriority}).`,
|
|
144
|
+
value: config.sitemap.defaultPriority,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Unknown top-level keys
|
|
150
|
+
const knownKeys = new Set([
|
|
151
|
+
'output',
|
|
152
|
+
'debug',
|
|
153
|
+
'clientJavascript',
|
|
154
|
+
'adapter',
|
|
155
|
+
'cacheHandler',
|
|
156
|
+
'allowedOrigins',
|
|
157
|
+
'csrf',
|
|
158
|
+
'limits',
|
|
159
|
+
'pageExtensions',
|
|
160
|
+
'slowRequestMs',
|
|
161
|
+
'renderTimeoutMs',
|
|
162
|
+
'devBrowserLogs',
|
|
163
|
+
'dev',
|
|
164
|
+
'serverTiming',
|
|
165
|
+
'appDir',
|
|
166
|
+
'mdx',
|
|
167
|
+
'actionEncryption',
|
|
168
|
+
'reactCompiler',
|
|
169
|
+
'sitemap',
|
|
170
|
+
'buildDir',
|
|
171
|
+
'topLoader',
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
for (const key of Object.keys(config)) {
|
|
175
|
+
if (!knownKeys.has(key)) {
|
|
176
|
+
errors.push({
|
|
177
|
+
field: key,
|
|
178
|
+
message: `Unknown config option: "${key}".`,
|
|
179
|
+
suggestion: `Check for typos. Known options: ${[...knownKeys].sort().join(', ')}.`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return errors;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ─── Formatting ─────────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
const RED = '\x1b[31m';
|
|
190
|
+
const BOLD = '\x1b[1m';
|
|
191
|
+
const DIM = '\x1b[2m';
|
|
192
|
+
const RESET = '\x1b[0m';
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Format config errors for terminal output.
|
|
196
|
+
*/
|
|
197
|
+
export function formatConfigErrors(errors: ConfigError[]): string {
|
|
198
|
+
if (errors.length === 0) return '';
|
|
199
|
+
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
lines.push(
|
|
202
|
+
`${RED}${BOLD}[timber]${RESET} ${RED}${errors.length} config error${errors.length !== 1 ? 's' : ''} in timber.config.ts:${RESET}`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
for (const err of errors) {
|
|
206
|
+
lines.push('');
|
|
207
|
+
lines.push(` ${RED}✗${RESET} ${BOLD}${err.field}${RESET}: ${err.message}`);
|
|
208
|
+
if (err.suggestion) {
|
|
209
|
+
lines.push(` ${DIM}${err.suggestion}${RESET}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ─── Virtual Module Name Mapping ────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
const VIRTUAL_MODULE_NAMES: Record<string, string> = {
|
|
219
|
+
'virtual:timber-rsc-entry': 'RSC entry (server component handler)',
|
|
220
|
+
'virtual:timber-ssr-entry': 'SSR entry (HTML renderer)',
|
|
221
|
+
'virtual:timber-browser-entry': 'Browser entry (client hydration)',
|
|
222
|
+
'virtual:timber-config': 'Runtime config (timber.config.ts)',
|
|
223
|
+
'virtual:timber-route-manifest': 'Route manifest (app/ file tree)',
|
|
224
|
+
'virtual:timber-instrumentation': 'Instrumentation (instrumentation.ts)',
|
|
225
|
+
'virtual:timber-cache-handler': 'Cache handler (cacheHandler config)',
|
|
226
|
+
'virtual:timber-build-manifest': 'Build manifest (asset mapping)',
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Add timber-specific context to an error message that references virtual modules.
|
|
231
|
+
*
|
|
232
|
+
* If the error message contains a `virtual:timber-*` ID, appends a
|
|
233
|
+
* human-readable explanation. Does not replace the original message.
|
|
234
|
+
*/
|
|
235
|
+
export function addVirtualModuleContext(errorMessage: string): string {
|
|
236
|
+
for (const [id, name] of Object.entries(VIRTUAL_MODULE_NAMES)) {
|
|
237
|
+
if (errorMessage.includes(id)) {
|
|
238
|
+
return `${errorMessage}\n\n [timber] This error references "${id}" — timber's ${name}.\n This is an internal module. The issue is likely in your app code or timber configuration.`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return errorMessage;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ─── Peer Dependency Check ──────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
interface PeerDepResult {
|
|
247
|
+
name: string;
|
|
248
|
+
status: 'ok' | 'missing';
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Required (non-optional) peer dependencies that must be installed.
|
|
253
|
+
*/
|
|
254
|
+
const REQUIRED_PEERS = ['react', 'react-dom', '@vitejs/plugin-react', '@vitejs/plugin-rsc'];
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Check that required peer dependencies are installed.
|
|
258
|
+
*
|
|
259
|
+
* Uses require.resolve with a try/catch — no fs scanning.
|
|
260
|
+
* Returns only the missing packages.
|
|
261
|
+
*/
|
|
262
|
+
export function checkPeerDependencies(projectRoot: string): PeerDepResult[] {
|
|
263
|
+
const results: PeerDepResult[] = [];
|
|
264
|
+
|
|
265
|
+
for (const name of REQUIRED_PEERS) {
|
|
266
|
+
try {
|
|
267
|
+
// Use createRequire from the project root to resolve as the user would
|
|
268
|
+
const { createRequire } = require('node:module');
|
|
269
|
+
const userRequire = createRequire(`${projectRoot}/package.json`);
|
|
270
|
+
userRequire.resolve(name);
|
|
271
|
+
results.push({ name, status: 'ok' });
|
|
272
|
+
} catch {
|
|
273
|
+
results.push({ name, status: 'missing' });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return results;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Format missing peer dependencies as an actionable warning.
|
|
282
|
+
*/
|
|
283
|
+
export function formatMissingPeers(results: PeerDepResult[]): string {
|
|
284
|
+
const missing = results.filter((r) => r.status === 'missing');
|
|
285
|
+
if (missing.length === 0) return '';
|
|
286
|
+
|
|
287
|
+
const names = missing.map((r) => r.name);
|
|
288
|
+
const lines: string[] = [];
|
|
289
|
+
lines.push(`${RED}${BOLD}[timber]${RESET} ${RED}Missing required dependencies:${RESET}`);
|
|
290
|
+
lines.push('');
|
|
291
|
+
for (const name of names) {
|
|
292
|
+
lines.push(` ${RED}✗${RESET} ${name}`);
|
|
293
|
+
}
|
|
294
|
+
lines.push('');
|
|
295
|
+
lines.push(` ${DIM}Install with:${RESET}`);
|
|
296
|
+
lines.push(` ${BOLD}pnpm add ${names.join(' ')}${RESET}`);
|
|
297
|
+
|
|
298
|
+
return lines.join('\n');
|
|
299
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -366,6 +366,23 @@ export function timber(config?: TimberUserConfig): PluginOption[] {
|
|
|
366
366
|
// Start the overall dev server setup timer — ends in timber-dev-server
|
|
367
367
|
ctx.timer.start('dev-server-setup');
|
|
368
368
|
}
|
|
369
|
+
|
|
370
|
+
// Validate config and check peer dependencies at startup.
|
|
371
|
+
// Errors are logged to stderr but don't block startup — Vite may
|
|
372
|
+
// still work partially, and the errors guide the user to fix things.
|
|
373
|
+
const { validateConfig, formatConfigErrors, checkPeerDependencies, formatMissingPeers } =
|
|
374
|
+
require('./config-validation.js') as typeof import('./config-validation.js');
|
|
375
|
+
|
|
376
|
+
const configErrors = validateConfig(ctx.config);
|
|
377
|
+
if (configErrors.length > 0) {
|
|
378
|
+
process.stderr.write(`${formatConfigErrors(configErrors)}\n\n`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const peerResults = checkPeerDependencies(ctx.root);
|
|
382
|
+
const peerWarning = formatMissingPeers(peerResults);
|
|
383
|
+
if (peerWarning) {
|
|
384
|
+
process.stderr.write(`${peerWarning}\n\n`);
|
|
385
|
+
}
|
|
369
386
|
},
|
|
370
387
|
};
|
|
371
388
|
|