@timber-js/app 0.2.0-alpha.100 → 0.2.0-alpha.101

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/_chunks/{actions-CQ8Z8VGL.js → actions-BsfxK6yu.js} +2 -2
  2. package/dist/_chunks/{actions-CQ8Z8VGL.js.map → actions-BsfxK6yu.js.map} +1 -1
  3. package/dist/_chunks/{chunk-BYIpzuS7.js → chunk-Cfpv9whK.js} +1 -8
  4. package/dist/_chunks/{define-CfBPoJb0.js → define-k32T6NL2.js} +2 -2
  5. package/dist/_chunks/{define-CfBPoJb0.js.map → define-k32T6NL2.js.map} +1 -1
  6. package/dist/_chunks/{navigation-root-BCYczjml.js → navigation-root-yx917rG0.js} +2 -2
  7. package/dist/_chunks/{navigation-root-BCYczjml.js.map → navigation-root-yx917rG0.js.map} +1 -1
  8. package/dist/_chunks/{use-segment-params-BkpKAQ7D.js → use-segment-params-DlsOX1YS.js} +2 -2
  9. package/dist/_chunks/{use-segment-params-BkpKAQ7D.js.map → use-segment-params-DlsOX1YS.js.map} +1 -1
  10. package/dist/client/index.js +4 -4
  11. package/dist/client/internal.d.ts +1 -1
  12. package/dist/client/internal.d.ts.map +1 -1
  13. package/dist/client/internal.js +3 -3
  14. package/dist/fonts/ast.d.ts +98 -6
  15. package/dist/fonts/ast.d.ts.map +1 -1
  16. package/dist/fonts/local.d.ts +1 -1
  17. package/dist/fonts/transform.d.ts +15 -1
  18. package/dist/fonts/transform.d.ts.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +1481 -11536
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/dev-server.d.ts.map +1 -1
  23. package/dist/plugins/routing.d.ts.map +1 -1
  24. package/dist/plugins/static-build.d.ts +2 -23
  25. package/dist/plugins/static-build.d.ts.map +1 -1
  26. package/dist/segment-params/index.js +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/internal.js +1 -1
  29. package/package.json +2 -1
  30. package/src/client/internal.ts +1 -1
  31. package/src/fonts/ast.ts +488 -16
  32. package/src/fonts/local.ts +1 -1
  33. package/src/fonts/transform.ts +147 -236
  34. package/src/index.ts +14 -2
  35. package/src/plugins/dev-server.ts +8 -9
  36. package/src/plugins/routing.ts +1 -12
  37. package/src/plugins/static-build.ts +3 -83
  38. package/dist/plugins/server-action-exports.d.ts +0 -26
  39. package/dist/plugins/server-action-exports.d.ts.map +0 -1
  40. package/dist/routing/status-file-lint.d.ts +0 -34
  41. package/dist/routing/status-file-lint.d.ts.map +0 -1
  42. package/dist/utils/directive-parser.d.ts +0 -28
  43. package/dist/utils/directive-parser.d.ts.map +0 -1
  44. package/src/plugins/server-action-exports.ts +0 -220
  45. package/src/routing/status-file-lint.ts +0 -115
  46. package/src/utils/directive-parser.ts +0 -140
@@ -3,31 +3,25 @@
3
3
  *
4
4
  * When `output: 'static'` is set in timber.config.ts, this plugin:
5
5
  * 1. Validates that no dynamic APIs (getCookies(), getHeaders()) are used
6
- * 2. When client JavaScript is disabled, rejects 'use client' and 'use server' directives
7
- * 3. Coordinates build-time rendering of all pages
6
+ * 2. Coordinates build-time rendering of all pages
8
7
  *
9
8
  * Design doc: design/15-future-prerendering.md
10
9
  */
11
10
 
12
11
  import type { Plugin } from 'vite';
13
12
  import type { PluginContext } from '../plugin-context.js';
14
- import { detectFileDirective } from '../utils/directive-parser.js';
15
13
 
16
14
  // ---------------------------------------------------------------------------
17
15
  // Types
18
16
  // ---------------------------------------------------------------------------
19
17
 
20
18
  export interface StaticValidationError {
21
- type: 'dynamic-api' | 'nojs-directive';
19
+ type: 'dynamic-api';
22
20
  file: string;
23
21
  message: string;
24
22
  line?: number;
25
23
  }
26
24
 
27
- export interface StaticOptions {
28
- clientJavascriptDisabled: boolean;
29
- }
30
-
31
25
  // ---------------------------------------------------------------------------
32
26
  // Detection: dynamic APIs (getCookies, getHeaders, and legacy cookies/headers)
33
27
  // ---------------------------------------------------------------------------
@@ -80,78 +74,6 @@ export function detectDynamicApis(code: string, fileId: string): StaticValidatio
80
74
  return errors;
81
75
  }
82
76
 
83
- // ---------------------------------------------------------------------------
84
- // Detection: 'use client' / 'use server' directives (clientJavascript disabled)
85
- // ---------------------------------------------------------------------------
86
-
87
- /**
88
- * Detect 'use client' and 'use server' directives using AST-based parsing.
89
- * When client JavaScript is disabled, both are hard build errors — no React
90
- * runtime or server actions are allowed in the output.
91
- *
92
- * When client JavaScript is enabled, these are allowed (client components
93
- * hydrate, server actions get extracted to API endpoints).
94
- */
95
- export function detectDirectives(
96
- code: string,
97
- fileId: string,
98
- options: StaticOptions
99
- ): StaticValidationError[] {
100
- if (!options.clientJavascriptDisabled) return [];
101
-
102
- const errors: StaticValidationError[] = [];
103
-
104
- const clientDirective = detectFileDirective(code, ['use client']);
105
- if (clientDirective) {
106
- errors.push({
107
- type: 'nojs-directive',
108
- file: fileId,
109
- message:
110
- `'use client' is not allowed when client JavaScript is disabled (clientJavascript: false). ` +
111
- `This mode produces zero JavaScript — client components cannot exist.`,
112
- line: clientDirective.line,
113
- });
114
- }
115
-
116
- const serverDirective = detectFileDirective(code, ['use server']);
117
- if (serverDirective) {
118
- errors.push({
119
- type: 'nojs-directive',
120
- file: fileId,
121
- message:
122
- `'use server' is not allowed when client JavaScript is disabled (clientJavascript: false). ` +
123
- `This mode produces zero JavaScript — server actions cannot exist.`,
124
- line: serverDirective.line,
125
- });
126
- }
127
-
128
- return errors;
129
- }
130
-
131
- // ---------------------------------------------------------------------------
132
- // Combined validation
133
- // ---------------------------------------------------------------------------
134
-
135
- /**
136
- * Run all static mode validations on a source file.
137
- *
138
- * Combines:
139
- * - Dynamic API detection (getCookies, getHeaders) — always in static mode
140
- * - Directive detection ('use client', 'use server') — only when client JS is disabled
141
- */
142
- export function validateStaticMode(
143
- code: string,
144
- fileId: string,
145
- options: StaticOptions
146
- ): StaticValidationError[] {
147
- const errors: StaticValidationError[] = [];
148
-
149
- errors.push(...detectDynamicApis(code, fileId));
150
- errors.push(...detectDirectives(code, fileId, options));
151
-
152
- return errors;
153
- }
154
-
155
77
  // ---------------------------------------------------------------------------
156
78
  // Vite Plugin
157
79
  // ---------------------------------------------------------------------------
@@ -193,9 +115,7 @@ export function timberStaticBuild(ctx: PluginContext): Plugin {
193
115
  // Only check JS/TS files
194
116
  if (!/\.[jt]sx?$/.test(id)) return null;
195
117
 
196
- const errors = validateStaticMode(code, id, {
197
- clientJavascriptDisabled: ctx.clientJavascript.disabled,
198
- });
118
+ const errors = detectDynamicApis(code, id);
199
119
 
200
120
  if (errors.length > 0) {
201
121
  // Format all errors into a single build error message
@@ -1,26 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- /**
3
- * Rewrite 'use server' module exports to bypass RSC plugin AST validation.
4
- *
5
- * The RSC plugin's client/SSR proxy transform (transformProxyExport) requires
6
- * all `export const` initializers to be `async ArrowFunctionExpression`. But
7
- * `createActionClient().action()` and `validated()` return async functions via
8
- * CallExpressions — valid at runtime but rejected by the static AST check.
9
- *
10
- * This plugin rewrites non-function-expression exports from:
11
- *
12
- * export const foo = someCall();
13
- *
14
- * to:
15
- *
16
- * const foo = someCall();
17
- * export { foo };
18
- *
19
- * The `export { name }` form bypasses the RSC plugin's validation without
20
- * changing runtime semantics. Function expression exports (async arrows,
21
- * async function expressions) are left untouched.
22
- *
23
- * See design/08-forms-and-actions.md §"Middleware for Server Actions"
24
- */
25
- export declare function timberServerActionExports(): Plugin;
26
- //# sourceMappingURL=server-action-exports.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"server-action-exports.d.ts","sourceRoot":"","sources":["../../src/plugins/server-action-exports.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAC;AAOnC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,yBAAyB,IAAI,MAAM,CAelD"}
@@ -1,34 +0,0 @@
1
- /**
2
- * Lint status files and error.tsx for missing 'use client' directive.
3
- *
4
- * Status files (error.tsx, 404.tsx, 4xx.tsx, 5xx.tsx, NNN.tsx) and legacy
5
- * compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx) are passed
6
- * as fallbackComponent to TimberErrorBoundary — a 'use client' component.
7
- * RSC forbids passing server component functions as props to client
8
- * components, causing a hard-to-debug runtime error.
9
- *
10
- * This module provides a build/dev-time check that warns when these files
11
- * are missing the 'use client' directive.
12
- *
13
- * See design/10-error-handling.md §"Status-Code Files".
14
- */
15
- import type { RouteTree } from './types.js';
16
- export interface StatusFileLintWarning {
17
- filePath: string;
18
- fileType: string;
19
- }
20
- /**
21
- * Walk the route tree and check all status files and error files for
22
- * the 'use client' directive. Returns an array of warnings for files
23
- * that are missing it.
24
- *
25
- * MDX and JSON status files are excluded — MDX files are server components
26
- * by design (pre-rendered as elements via fallbackElement, see TIM-503),
27
- * and JSON files are data, not components.
28
- */
29
- export declare function lintStatusFileDirectives(tree: RouteTree): StatusFileLintWarning[];
30
- /**
31
- * Format warnings into human-readable console output.
32
- */
33
- export declare function formatStatusFileLintWarnings(warnings: StatusFileLintWarning[]): string;
34
- //# sourceMappingURL=status-file-lint.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"status-file-lint.d.ts","sourceRoot":"","sources":["../../src/routing/status-file-lint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAGH,OAAO,KAAK,EAAE,SAAS,EAAe,MAAM,YAAY,CAAC;AAOzD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,GAAG,qBAAqB,EAAE,CAIjF;AAqDD;;GAEG;AACH,wBAAgB,4BAA4B,CAAC,QAAQ,EAAE,qBAAqB,EAAE,GAAG,MAAM,CAiBtF"}
@@ -1,28 +0,0 @@
1
- /**
2
- * AST-based directive detection for 'use client' and 'use server'.
3
- *
4
- * Uses acorn to parse source code and detect directives properly,
5
- * avoiding false positives from regex matching inside string literals,
6
- * comments, or template expressions.
7
- *
8
- * @module
9
- */
10
- export interface FileDirective {
11
- /** The directive value, e.g. 'use client', 'use server' */
12
- directive: string;
13
- /** 1-based line number where the directive appears */
14
- line: number;
15
- }
16
- /**
17
- * Detect a file-level directive ('use client', 'use server', etc.).
18
- *
19
- * Per the ECMAScript spec, directives are string literal expression
20
- * statements at the start of a program body (before any non-directive
21
- * statements). This function checks the AST `Program.body` for
22
- * `ExpressionStatement` nodes whose expression is a `Literal` string
23
- * matching a known directive.
24
- *
25
- * Returns the first matching directive, or null if none found.
26
- */
27
- export declare function detectFileDirective(code: string, directives?: string[]): FileDirective | null;
28
- //# sourceMappingURL=directive-parser.d.ts.map
@@ -1 +0,0 @@
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"}
@@ -1,220 +0,0 @@
1
- import type { Plugin } from 'vite';
2
- import { Parser } from 'acorn';
3
- import acornJsx from 'acorn-jsx';
4
- import { detectFileDirective } from '../utils/directive-parser.js';
5
-
6
- const jsxParser = Parser.extend(acornJsx());
7
-
8
- /**
9
- * Rewrite 'use server' module exports to bypass RSC plugin AST validation.
10
- *
11
- * The RSC plugin's client/SSR proxy transform (transformProxyExport) requires
12
- * all `export const` initializers to be `async ArrowFunctionExpression`. But
13
- * `createActionClient().action()` and `validated()` return async functions via
14
- * CallExpressions — valid at runtime but rejected by the static AST check.
15
- *
16
- * This plugin rewrites non-function-expression exports from:
17
- *
18
- * export const foo = someCall();
19
- *
20
- * to:
21
- *
22
- * const foo = someCall();
23
- * export { foo };
24
- *
25
- * The `export { name }` form bypasses the RSC plugin's validation without
26
- * changing runtime semantics. Function expression exports (async arrows,
27
- * async function expressions) are left untouched.
28
- *
29
- * See design/08-forms-and-actions.md §"Middleware for Server Actions"
30
- */
31
- export function timberServerActionExports(): Plugin {
32
- return {
33
- name: 'timber-server-action-exports',
34
- transform(code, id) {
35
- // Skip non-JS/TS files
36
- if (!/\.[jt]sx?$/.test(id)) return;
37
- // Quick bail-out
38
- if (!code.includes('use server')) return;
39
- // Check for file-level directive
40
- const directive = detectFileDirective(code, ['use server']);
41
- if (!directive) return;
42
-
43
- return rewriteServerActionExports(code);
44
- },
45
- };
46
- }
47
-
48
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
- type AcornNode = any;
50
-
51
- /**
52
- * Check if an AST node is an async function expression (arrow or regular).
53
- * These are handled correctly by the RSC plugin and don't need rewriting.
54
- */
55
- function isAsyncFunctionExpr(node: AcornNode): boolean {
56
- if (!node) return false;
57
- return (
58
- (node.type === 'ArrowFunctionExpression' || node.type === 'FunctionExpression') && node.async
59
- );
60
- }
61
-
62
- /**
63
- * Extract identifier names from a declarator's id pattern.
64
- * Handles simple identifiers and destructuring patterns.
65
- */
66
- function extractDeclNames(id: AcornNode): string[] {
67
- if (id.type === 'Identifier') return [id.name];
68
-
69
- // ObjectPattern: const { a, b } = ...
70
- if (id.type === 'ObjectPattern') {
71
- return id.properties.flatMap((p: AcornNode) => extractDeclNames(p.value ?? p.argument));
72
- }
73
-
74
- // ArrayPattern: const [a, b] = ...
75
- if (id.type === 'ArrayPattern') {
76
- return id.elements.filter(Boolean).flatMap((e: AcornNode) => extractDeclNames(e));
77
- }
78
-
79
- // RestElement: const [...rest] = ...
80
- if (id.type === 'RestElement') {
81
- return extractDeclNames(id.argument);
82
- }
83
-
84
- // AssignmentPattern: const { a = 1 } = ...
85
- if (id.type === 'AssignmentPattern') {
86
- return extractDeclNames(id.left);
87
- }
88
-
89
- return [];
90
- }
91
-
92
- interface RewriteTarget {
93
- /** Start offset of the `export` keyword */
94
- exportStart: number;
95
- /** Start offset of the `const/let/var` keyword (right after `export `) */
96
- declStart: number;
97
- /** Names to re-export */
98
- names: string[];
99
- }
100
-
101
- function rewriteServerActionExports(code: string): { code: string; map: null } | undefined {
102
- let ast: AcornNode;
103
- try {
104
- ast = jsxParser.parse(code, {
105
- ecmaVersion: 'latest',
106
- sourceType: 'module',
107
- });
108
- } catch {
109
- // TypeScript that esbuild hasn't stripped yet — use regex fallback
110
- return rewriteServerActionExportsFallback(code);
111
- }
112
-
113
- const targets: RewriteTarget[] = [];
114
-
115
- for (const node of ast.body) {
116
- // Case 1: export const/let/var name = <non-function-expression>
117
- if (
118
- node.type === 'ExportNamedDeclaration' &&
119
- node.declaration?.type === 'VariableDeclaration'
120
- ) {
121
- const hasNonFunctionInit = node.declaration.declarations.some(
122
- (d: AcornNode) => d.init && !isAsyncFunctionExpr(d.init)
123
- );
124
- if (!hasNonFunctionInit) continue;
125
-
126
- const names = node.declaration.declarations.flatMap((d: AcornNode) => extractDeclNames(d.id));
127
- if (names.length === 0) continue;
128
-
129
- targets.push({
130
- exportStart: node.start,
131
- declStart: node.declaration.start,
132
- names,
133
- });
134
- }
135
-
136
- // Case 2: export default <non-function-expression>
137
- if (node.type === 'ExportDefaultDeclaration') {
138
- const decl = node.declaration;
139
- if (
140
- decl.type !== 'FunctionDeclaration' &&
141
- decl.type !== 'Identifier' &&
142
- !isAsyncFunctionExpr(decl)
143
- ) {
144
- // export default someCall() → const $$default = someCall(); export default $$default;
145
- targets.push({
146
- exportStart: node.start,
147
- declStart: -1, // sentinel for default export
148
- names: ['default'],
149
- });
150
- }
151
- }
152
- }
153
-
154
- if (targets.length === 0) return undefined;
155
-
156
- // Process from end to start to preserve character positions
157
- let result = code;
158
- const reExports: string[] = [];
159
-
160
- for (let i = targets.length - 1; i >= 0; i--) {
161
- const target = targets[i];
162
-
163
- if (target.declStart === -1) {
164
- // export default <expr> → const $$default = <expr>;\nexport default $$default;
165
- const node = ast.body.find(
166
- (n: AcornNode) => n.type === 'ExportDefaultDeclaration' && n.start === target.exportStart
167
- );
168
- if (node) {
169
- const exprCode = code.slice(node.declaration.start, node.declaration.end);
170
- const replacement = `const $$default = ${exprCode};\nexport default $$default;`;
171
- result = result.slice(0, node.start) + replacement + result.slice(node.end);
172
- }
173
- continue;
174
- }
175
-
176
- // Remove 'export ' from the declaration
177
- result = result.slice(0, target.exportStart) + result.slice(target.declStart);
178
- reExports.push(...target.names);
179
- }
180
-
181
- if (reExports.length > 0) {
182
- result += '\nexport { ' + reExports.join(', ') + ' };\n';
183
- }
184
-
185
- return { code: result, map: null };
186
- }
187
-
188
- /**
189
- * Regex fallback for TypeScript files that acorn cannot parse.
190
- *
191
- * Matches `export const/let/var name = <expr>` where the initializer does
192
- * NOT start with `async function` or `async (` (i.e., not an async
193
- * function expression or async arrow function).
194
- */
195
- function rewriteServerActionExportsFallback(code: string): { code: string; map: null } | undefined {
196
- // Match: export const/let/var <name> = <non-async-function-expr>
197
- const pattern =
198
- /^(export\s+)((?:const|let|var)\s+(\w+)\s*=\s*)(?!async\s+(?:function[\s(]|\())/gm;
199
-
200
- const names: string[] = [];
201
- let result = code;
202
- let offset = 0;
203
- let match;
204
-
205
- while ((match = pattern.exec(code)) !== null) {
206
- const exportKeyword = match[1]; // 'export ' (with trailing space)
207
- const name = match[3];
208
-
209
- // Remove 'export ' at this position (adjusted for prior removals)
210
- const pos = match.index + offset;
211
- result = result.slice(0, pos) + result.slice(pos + exportKeyword.length);
212
- offset -= exportKeyword.length;
213
- names.push(name);
214
- }
215
-
216
- if (names.length === 0) return undefined;
217
-
218
- result += '\nexport { ' + names.join(', ') + ' };\n';
219
- return { code: result, map: null };
220
- }
@@ -1,115 +0,0 @@
1
- /**
2
- * Lint status files and error.tsx for missing 'use client' directive.
3
- *
4
- * Status files (error.tsx, 404.tsx, 4xx.tsx, 5xx.tsx, NNN.tsx) and legacy
5
- * compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx) are passed
6
- * as fallbackComponent to TimberErrorBoundary — a 'use client' component.
7
- * RSC forbids passing server component functions as props to client
8
- * components, causing a hard-to-debug runtime error.
9
- *
10
- * This module provides a build/dev-time check that warns when these files
11
- * are missing the 'use client' directive.
12
- *
13
- * See design/10-error-handling.md §"Status-Code Files".
14
- */
15
-
16
- import { readFileSync } from 'node:fs';
17
- import type { RouteTree, SegmentNode } from './types.js';
18
- import { detectFileDirective } from '../utils/directive-parser.js';
19
- import { swallow } from '../server/logger.js';
20
-
21
- /** Extensions that require 'use client' (component files, not MDX/JSON). */
22
- const CLIENT_REQUIRED_EXTENSIONS = new Set(['tsx', 'jsx', 'ts', 'js']);
23
-
24
- export interface StatusFileLintWarning {
25
- filePath: string;
26
- fileType: string;
27
- }
28
-
29
- /**
30
- * Walk the route tree and check all status files and error files for
31
- * the 'use client' directive. Returns an array of warnings for files
32
- * that are missing it.
33
- *
34
- * MDX and JSON status files are excluded — MDX files are server components
35
- * by design (pre-rendered as elements via fallbackElement, see TIM-503),
36
- * and JSON files are data, not components.
37
- */
38
- export function lintStatusFileDirectives(tree: RouteTree): StatusFileLintWarning[] {
39
- const warnings: StatusFileLintWarning[] = [];
40
- walkNode(tree.root, warnings);
41
- return warnings;
42
- }
43
-
44
- function walkNode(node: SegmentNode, warnings: StatusFileLintWarning[]): void {
45
- // Check error.tsx
46
- if (node.error) {
47
- checkFile(node.error.filePath, node.error.extension, 'error', warnings);
48
- }
49
-
50
- // Check status-code files (404.tsx, 4xx.tsx, 5xx.tsx, etc.)
51
- if (node.statusFiles) {
52
- for (const [code, file] of Object.entries(node.statusFiles)) {
53
- checkFile(file.filePath, file.extension, code, warnings);
54
- }
55
- }
56
-
57
- // Check legacy compat files (not-found.tsx, forbidden.tsx, unauthorized.tsx)
58
- if (node.legacyStatusFiles) {
59
- for (const [name, file] of Object.entries(node.legacyStatusFiles)) {
60
- checkFile(file.filePath, file.extension, name, warnings);
61
- }
62
- }
63
-
64
- // Recurse into children and slots
65
- for (const child of node.children) {
66
- walkNode(child, warnings);
67
- }
68
- for (const slotNode of Object.values(node.slots)) {
69
- walkNode(slotNode, warnings);
70
- }
71
- }
72
-
73
- function checkFile(
74
- filePath: string,
75
- extension: string,
76
- fileType: string,
77
- warnings: StatusFileLintWarning[]
78
- ): void {
79
- if (!CLIENT_REQUIRED_EXTENSIONS.has(extension)) return;
80
-
81
- let code: string;
82
- try {
83
- code = readFileSync(filePath, 'utf-8');
84
- } catch (err) {
85
- swallow(err, `status-file-lint: unreadable file ${filePath}`);
86
- return;
87
- }
88
-
89
- const directive = detectFileDirective(code, ['use client']);
90
- if (!directive) {
91
- warnings.push({ filePath, fileType });
92
- }
93
- }
94
-
95
- /**
96
- * Format warnings into human-readable console output.
97
- */
98
- export function formatStatusFileLintWarnings(warnings: StatusFileLintWarning[]): string {
99
- const lines = [
100
- `[timber] ${warnings.length} status/error file${warnings.length > 1 ? 's' : ''} missing 'use client' directive:`,
101
- '',
102
- ];
103
-
104
- for (const w of warnings) {
105
- lines.push(` ${w.filePath}`);
106
- }
107
-
108
- lines.push('');
109
- lines.push(
110
- " Status files and error.tsx are rendered inside TimberErrorBoundary (a 'use client' component)."
111
- );
112
- lines.push(" Add 'use client' as the first line of each file to avoid a runtime error.");
113
-
114
- return lines.join('\n');
115
- }