@timber-js/app 0.1.0 → 0.1.2
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/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/plugins/entries.d.ts.map +1 -1
- package/package.json +43 -58
- package/src/adapters/cloudflare.ts +325 -0
- package/src/adapters/nitro.ts +366 -0
- package/src/adapters/types.ts +63 -0
- package/src/cache/index.ts +91 -0
- package/src/cache/redis-handler.ts +91 -0
- package/src/cache/register-cached-function.ts +99 -0
- package/src/cache/singleflight.ts +26 -0
- package/src/cache/stable-stringify.ts +21 -0
- package/src/cache/timber-cache.ts +116 -0
- package/src/cli.ts +201 -0
- package/src/client/browser-entry.ts +663 -0
- package/src/client/error-boundary.tsx +209 -0
- package/src/client/form.tsx +200 -0
- package/src/client/head.ts +61 -0
- package/src/client/history.ts +46 -0
- package/src/client/index.ts +60 -0
- package/src/client/link-navigate-interceptor.tsx +62 -0
- package/src/client/link-status-provider.tsx +40 -0
- package/src/client/link.tsx +310 -0
- package/src/client/nuqs-adapter.tsx +117 -0
- package/src/client/router-ref.ts +25 -0
- package/src/client/router.ts +563 -0
- package/src/client/segment-cache.ts +194 -0
- package/src/client/segment-context.ts +57 -0
- package/src/client/ssr-data.ts +95 -0
- package/src/client/types.ts +4 -0
- package/src/client/unload-guard.ts +34 -0
- package/src/client/use-cookie.ts +122 -0
- package/src/client/use-link-status.ts +46 -0
- package/src/client/use-navigation-pending.ts +47 -0
- package/src/client/use-params.ts +71 -0
- package/src/client/use-pathname.ts +43 -0
- package/src/client/use-query-states.ts +133 -0
- package/src/client/use-router.ts +77 -0
- package/src/client/use-search-params.ts +74 -0
- package/src/client/use-selected-layout-segment.ts +110 -0
- package/src/content/index.ts +13 -0
- package/src/cookies/define-cookie.ts +137 -0
- package/src/cookies/index.ts +9 -0
- package/src/fonts/ast.ts +359 -0
- package/src/fonts/css.ts +68 -0
- package/src/fonts/fallbacks.ts +248 -0
- package/src/fonts/google.ts +332 -0
- package/src/fonts/local.ts +177 -0
- package/src/fonts/types.ts +88 -0
- package/src/index.ts +413 -0
- package/src/plugins/adapter-build.ts +118 -0
- package/src/plugins/build-manifest.ts +323 -0
- package/src/plugins/build-report.ts +353 -0
- package/src/plugins/cache-transform.ts +199 -0
- package/src/plugins/chunks.ts +90 -0
- package/src/plugins/content.ts +136 -0
- package/src/plugins/dev-error-overlay.ts +230 -0
- package/src/plugins/dev-logs.ts +280 -0
- package/src/plugins/dev-server.ts +389 -0
- package/src/plugins/dynamic-transform.ts +161 -0
- package/src/plugins/entries.ts +207 -0
- package/src/plugins/fonts.ts +581 -0
- package/src/plugins/mdx.ts +179 -0
- package/src/plugins/react-prod.ts +56 -0
- package/src/plugins/routing.ts +419 -0
- package/src/plugins/server-action-exports.ts +220 -0
- package/src/plugins/server-bundle.ts +113 -0
- package/src/plugins/shims.ts +168 -0
- package/src/plugins/static-build.ts +207 -0
- package/src/routing/codegen.ts +396 -0
- package/src/routing/index.ts +14 -0
- package/src/routing/interception.ts +173 -0
- package/src/routing/scanner.ts +487 -0
- package/src/routing/status-file-lint.ts +114 -0
- package/src/routing/types.ts +100 -0
- package/src/search-params/analyze.ts +192 -0
- package/src/search-params/codecs.ts +153 -0
- package/src/search-params/create.ts +314 -0
- package/src/search-params/index.ts +23 -0
- package/src/search-params/registry.ts +31 -0
- package/src/server/access-gate.tsx +142 -0
- package/src/server/action-client.ts +473 -0
- package/src/server/action-handler.ts +325 -0
- package/src/server/actions.ts +236 -0
- package/src/server/asset-headers.ts +81 -0
- package/src/server/body-limits.ts +102 -0
- package/src/server/build-manifest.ts +234 -0
- package/src/server/canonicalize.ts +90 -0
- package/src/server/client-module-map.ts +58 -0
- package/src/server/csrf.ts +79 -0
- package/src/server/deny-renderer.ts +302 -0
- package/src/server/dev-logger.ts +419 -0
- package/src/server/dev-span-processor.ts +78 -0
- package/src/server/dev-warnings.ts +282 -0
- package/src/server/early-hints-sender.ts +55 -0
- package/src/server/early-hints.ts +142 -0
- package/src/server/error-boundary-wrapper.ts +69 -0
- package/src/server/error-formatter.ts +184 -0
- package/src/server/flush.ts +182 -0
- package/src/server/form-data.ts +176 -0
- package/src/server/form-flash.ts +93 -0
- package/src/server/html-injectors.ts +445 -0
- package/src/server/index.ts +222 -0
- package/src/server/instrumentation.ts +136 -0
- package/src/server/logger.ts +145 -0
- package/src/server/manifest-status-resolver.ts +215 -0
- package/src/server/metadata-render.ts +527 -0
- package/src/server/metadata-routes.ts +189 -0
- package/src/server/metadata.ts +263 -0
- package/src/server/middleware-runner.ts +32 -0
- package/src/server/nuqs-ssr-provider.tsx +63 -0
- package/src/server/pipeline.ts +555 -0
- package/src/server/prerender.ts +139 -0
- package/src/server/primitives.ts +264 -0
- package/src/server/proxy.ts +43 -0
- package/src/server/request-context.ts +554 -0
- package/src/server/route-element-builder.ts +395 -0
- package/src/server/route-handler.ts +153 -0
- package/src/server/route-matcher.ts +316 -0
- package/src/server/rsc-entry/api-handler.ts +112 -0
- package/src/server/rsc-entry/error-renderer.ts +177 -0
- package/src/server/rsc-entry/helpers.ts +147 -0
- package/src/server/rsc-entry/index.ts +688 -0
- package/src/server/rsc-entry/ssr-bridge.ts +18 -0
- package/src/server/slot-resolver.ts +359 -0
- package/src/server/ssr-entry.ts +161 -0
- package/src/server/ssr-render.ts +200 -0
- package/src/server/status-code-resolver.ts +282 -0
- package/src/server/tracing.ts +281 -0
- package/src/server/tree-builder.ts +354 -0
- package/src/server/types.ts +150 -0
- package/src/shims/font-google.ts +67 -0
- package/src/shims/headers.ts +11 -0
- package/src/shims/image.ts +48 -0
- package/src/shims/link.ts +9 -0
- package/src/shims/navigation-client.ts +52 -0
- package/src/shims/navigation.ts +31 -0
- package/src/shims/server-only-noop.js +5 -0
- package/src/utils/directive-parser.ts +529 -0
- package/src/utils/format.ts +10 -0
- package/src/utils/startup-timer.ts +102 -0
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AST-based directive detection for 'use cache', 'use dynamic',
|
|
3
|
+
* 'use client', and 'use server'.
|
|
4
|
+
*
|
|
5
|
+
* Uses acorn to parse source code and detect directives properly,
|
|
6
|
+
* avoiding false positives from regex matching inside string literals,
|
|
7
|
+
* comments, or template expressions.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Parser } from 'acorn';
|
|
13
|
+
import acornJsx from 'acorn-jsx';
|
|
14
|
+
|
|
15
|
+
// acorn parser with JSX support
|
|
16
|
+
const jsxParser = Parser.extend(acornJsx());
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Types
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
export interface FileDirective {
|
|
23
|
+
/** The directive value, e.g. 'use client', 'use server' */
|
|
24
|
+
directive: string;
|
|
25
|
+
/** 1-based line number where the directive appears */
|
|
26
|
+
line: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface FunctionWithDirective {
|
|
30
|
+
/** Function name (or 'default' for anonymous default exports) */
|
|
31
|
+
name: string;
|
|
32
|
+
/** The directive found in the function body */
|
|
33
|
+
directive: string;
|
|
34
|
+
/** 1-based line number of the directive */
|
|
35
|
+
directiveLine: number;
|
|
36
|
+
/** Start offset of the function in the source */
|
|
37
|
+
start: number;
|
|
38
|
+
/** End offset of the function in the source */
|
|
39
|
+
end: number;
|
|
40
|
+
/** Start offset of the function body block (after the '{') */
|
|
41
|
+
bodyStart: number;
|
|
42
|
+
/** End offset of the function body block (the '}') */
|
|
43
|
+
bodyEnd: number;
|
|
44
|
+
/** Content between { and } of the function body */
|
|
45
|
+
bodyContent: string;
|
|
46
|
+
/** 'export ', 'export default ', or '' */
|
|
47
|
+
prefix: string;
|
|
48
|
+
/** Whether this is an arrow function */
|
|
49
|
+
isArrow: boolean;
|
|
50
|
+
/** The function signature (everything before the body '{') */
|
|
51
|
+
declaration: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// File-level directive detection
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Detect a file-level directive ('use client', 'use server', etc.).
|
|
60
|
+
*
|
|
61
|
+
* Per the ECMAScript spec, directives are string literal expression
|
|
62
|
+
* statements at the start of a program body (before any non-directive
|
|
63
|
+
* statements). This function checks the AST `Program.body` for
|
|
64
|
+
* `ExpressionStatement` nodes whose expression is a `Literal` string
|
|
65
|
+
* matching a known directive.
|
|
66
|
+
*
|
|
67
|
+
* Returns the first matching directive, or null if none found.
|
|
68
|
+
*/
|
|
69
|
+
export function detectFileDirective(
|
|
70
|
+
code: string,
|
|
71
|
+
directives: string[] = ['use client', 'use server']
|
|
72
|
+
): FileDirective | null {
|
|
73
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
74
|
+
let ast: any;
|
|
75
|
+
try {
|
|
76
|
+
ast = jsxParser.parse(code, {
|
|
77
|
+
ecmaVersion: 'latest',
|
|
78
|
+
sourceType: 'module',
|
|
79
|
+
locations: true,
|
|
80
|
+
});
|
|
81
|
+
} catch {
|
|
82
|
+
// If the file fails to parse (e.g. TypeScript syntax), fall back to
|
|
83
|
+
// a safe line-by-line check that only considers lines before any
|
|
84
|
+
// non-comment, non-whitespace, non-directive content.
|
|
85
|
+
return detectFileDirectiveFallback(code, directives);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
for (const node of ast.body) {
|
|
89
|
+
if (
|
|
90
|
+
node.type === 'ExpressionStatement' &&
|
|
91
|
+
node.expression.type === 'Literal' &&
|
|
92
|
+
typeof node.expression.value === 'string'
|
|
93
|
+
) {
|
|
94
|
+
if (directives.includes(node.expression.value)) {
|
|
95
|
+
return {
|
|
96
|
+
directive: node.expression.value,
|
|
97
|
+
line: node.loc.start.line,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} else {
|
|
101
|
+
// Directives must appear before any non-directive statements
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fallback for TypeScript files that acorn cannot parse.
|
|
111
|
+
*
|
|
112
|
+
* Scans lines from the top of the file. Skips blank lines and comments.
|
|
113
|
+
* Checks if the first real statement is a directive string literal.
|
|
114
|
+
*/
|
|
115
|
+
function detectFileDirectiveFallback(code: string, directives: string[]): FileDirective | null {
|
|
116
|
+
const lines = code.split('\n');
|
|
117
|
+
let inBlockComment = false;
|
|
118
|
+
|
|
119
|
+
for (let i = 0; i < lines.length; i++) {
|
|
120
|
+
let line = lines[i].trim();
|
|
121
|
+
|
|
122
|
+
// Handle block comments
|
|
123
|
+
if (inBlockComment) {
|
|
124
|
+
const endIdx = line.indexOf('*/');
|
|
125
|
+
if (endIdx === -1) continue;
|
|
126
|
+
line = line.slice(endIdx + 2).trim();
|
|
127
|
+
inBlockComment = false;
|
|
128
|
+
if (!line) continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Skip blank lines
|
|
132
|
+
if (!line) continue;
|
|
133
|
+
|
|
134
|
+
// Skip line comments
|
|
135
|
+
if (line.startsWith('//')) continue;
|
|
136
|
+
|
|
137
|
+
// Skip block comment start
|
|
138
|
+
if (line.startsWith('/*')) {
|
|
139
|
+
const endIdx = line.indexOf('*/', 2);
|
|
140
|
+
if (endIdx === -1) {
|
|
141
|
+
inBlockComment = true;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
line = line.slice(endIdx + 2).trim();
|
|
145
|
+
if (!line) continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for directive
|
|
149
|
+
for (const dir of directives) {
|
|
150
|
+
// Match 'directive' or "directive" optionally followed by ;
|
|
151
|
+
if (
|
|
152
|
+
line === `'${dir}'` ||
|
|
153
|
+
line === `'${dir}';` ||
|
|
154
|
+
line === `"${dir}"` ||
|
|
155
|
+
line === `"${dir}";`
|
|
156
|
+
) {
|
|
157
|
+
return { directive: dir, line: i + 1 };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// First non-comment, non-blank line is not a directive — stop
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
// Function-body directive detection
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Find all functions in the source code that contain a directive
|
|
174
|
+
* (e.g. 'use cache', 'use dynamic') as their first body statement.
|
|
175
|
+
*
|
|
176
|
+
* Parses the source with acorn and walks the AST looking for function
|
|
177
|
+
* declarations and arrow function expressions whose body is a
|
|
178
|
+
* BlockStatement with a directive prologue.
|
|
179
|
+
*
|
|
180
|
+
* Returns an array of function info objects, sorted by position
|
|
181
|
+
* (descending) for safe end-to-start replacement.
|
|
182
|
+
*/
|
|
183
|
+
export function findFunctionsWithDirective(
|
|
184
|
+
code: string,
|
|
185
|
+
directive: string
|
|
186
|
+
): FunctionWithDirective[] {
|
|
187
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
188
|
+
let ast: any;
|
|
189
|
+
try {
|
|
190
|
+
ast = jsxParser.parse(code, {
|
|
191
|
+
ecmaVersion: 'latest',
|
|
192
|
+
sourceType: 'module',
|
|
193
|
+
locations: true,
|
|
194
|
+
});
|
|
195
|
+
} catch {
|
|
196
|
+
// TypeScript fallback: return empty — callers should use the quick
|
|
197
|
+
// regex check first and skip non-matching files anyway
|
|
198
|
+
return findFunctionsWithDirectiveFallback(code, directive);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const results: FunctionWithDirective[] = [];
|
|
202
|
+
walkAst(ast, code, directive, results, []);
|
|
203
|
+
|
|
204
|
+
// Sort descending by start position for safe end-to-start replacement
|
|
205
|
+
results.sort((a, b) => b.start - a.start);
|
|
206
|
+
return results;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Recursive AST walker that finds functions with a directive in their body.
|
|
211
|
+
*/
|
|
212
|
+
function walkAst(
|
|
213
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
214
|
+
node: any,
|
|
215
|
+
code: string,
|
|
216
|
+
directive: string,
|
|
217
|
+
results: FunctionWithDirective[],
|
|
218
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
219
|
+
ancestors: any[]
|
|
220
|
+
): void {
|
|
221
|
+
if (!node || typeof node !== 'object') return;
|
|
222
|
+
|
|
223
|
+
if (Array.isArray(node)) {
|
|
224
|
+
for (const child of node) {
|
|
225
|
+
walkAst(child, code, directive, results, ancestors);
|
|
226
|
+
}
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (!node.type) return;
|
|
231
|
+
|
|
232
|
+
// Check function declarations and expressions
|
|
233
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression') {
|
|
234
|
+
checkFunctionBody(node, code, directive, results, ancestors);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check arrow functions with block bodies
|
|
238
|
+
if (node.type === 'ArrowFunctionExpression' && node.body && node.body.type === 'BlockStatement') {
|
|
239
|
+
checkFunctionBody(node, code, directive, results, ancestors);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Walk children
|
|
243
|
+
const newAncestors = [...ancestors, node];
|
|
244
|
+
for (const key of Object.keys(node)) {
|
|
245
|
+
if (key === 'type' || key === 'loc' || key === 'start' || key === 'end') continue;
|
|
246
|
+
const child = node[key];
|
|
247
|
+
if (child && typeof child === 'object') {
|
|
248
|
+
walkAst(child, code, directive, results, newAncestors);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Check if a function's body starts with the target directive.
|
|
255
|
+
*/
|
|
256
|
+
function checkFunctionBody(
|
|
257
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
258
|
+
node: any,
|
|
259
|
+
code: string,
|
|
260
|
+
directive: string,
|
|
261
|
+
results: FunctionWithDirective[],
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
|
+
ancestors: any[]
|
|
264
|
+
): void {
|
|
265
|
+
const body = node.type === 'ArrowFunctionExpression' ? node.body : node.body;
|
|
266
|
+
if (!body || body.type !== 'BlockStatement' || body.body.length === 0) return;
|
|
267
|
+
|
|
268
|
+
// Check the first statement for a directive
|
|
269
|
+
const firstStmt = body.body[0];
|
|
270
|
+
if (
|
|
271
|
+
firstStmt.type !== 'ExpressionStatement' ||
|
|
272
|
+
firstStmt.expression.type !== 'Literal' ||
|
|
273
|
+
firstStmt.expression.value !== directive
|
|
274
|
+
) {
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Determine function metadata from AST context
|
|
279
|
+
const parent = ancestors[ancestors.length - 1];
|
|
280
|
+
const grandparent = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : null;
|
|
281
|
+
|
|
282
|
+
let name = '';
|
|
283
|
+
let prefix = '';
|
|
284
|
+
const isArrow = node.type === 'ArrowFunctionExpression';
|
|
285
|
+
let funcStart = node.start;
|
|
286
|
+
let funcEnd = node.end;
|
|
287
|
+
|
|
288
|
+
if (node.type === 'FunctionDeclaration') {
|
|
289
|
+
name = node.id?.name || 'default';
|
|
290
|
+
|
|
291
|
+
// Check for export
|
|
292
|
+
if (parent?.type === 'ExportNamedDeclaration') {
|
|
293
|
+
prefix = 'export ';
|
|
294
|
+
funcStart = parent.start;
|
|
295
|
+
funcEnd = parent.end;
|
|
296
|
+
} else if (parent?.type === 'ExportDefaultDeclaration') {
|
|
297
|
+
prefix = 'export default ';
|
|
298
|
+
funcStart = parent.start;
|
|
299
|
+
funcEnd = parent.end;
|
|
300
|
+
}
|
|
301
|
+
} else if (node.type === 'ArrowFunctionExpression') {
|
|
302
|
+
// Arrow in variable declaration: const name = async () => {}
|
|
303
|
+
if (parent?.type === 'VariableDeclarator' && parent.id?.name) {
|
|
304
|
+
name = parent.id.name;
|
|
305
|
+
// Include the full variable declaration
|
|
306
|
+
if (grandparent?.type === 'VariableDeclaration') {
|
|
307
|
+
funcStart = grandparent.start;
|
|
308
|
+
funcEnd = grandparent.end;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} else if (node.type === 'FunctionExpression') {
|
|
312
|
+
// Function expression in variable: const name = async function() {}
|
|
313
|
+
name = node.id?.name || '';
|
|
314
|
+
if (parent?.type === 'VariableDeclarator' && parent.id?.name) {
|
|
315
|
+
name = parent.id.name;
|
|
316
|
+
if (grandparent?.type === 'VariableDeclaration') {
|
|
317
|
+
funcStart = grandparent.start;
|
|
318
|
+
funcEnd = grandparent.end;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!name) return; // Skip anonymous functions we can't name
|
|
324
|
+
|
|
325
|
+
// Extract the body content (between the braces)
|
|
326
|
+
const bodyStart = body.start + 1; // after '{'
|
|
327
|
+
const bodyEnd = body.end - 1; // before '}'
|
|
328
|
+
const bodyContent = code.slice(bodyStart, bodyEnd);
|
|
329
|
+
|
|
330
|
+
// Extract declaration (everything before the body '{')
|
|
331
|
+
const declaration = code.slice(funcStart, body.start).trim();
|
|
332
|
+
|
|
333
|
+
results.push({
|
|
334
|
+
name,
|
|
335
|
+
directive,
|
|
336
|
+
directiveLine: firstStmt.loc.start.line,
|
|
337
|
+
start: funcStart,
|
|
338
|
+
end: funcEnd,
|
|
339
|
+
bodyStart,
|
|
340
|
+
bodyEnd,
|
|
341
|
+
bodyContent,
|
|
342
|
+
prefix,
|
|
343
|
+
isArrow,
|
|
344
|
+
declaration,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------
|
|
349
|
+
// TypeScript fallback for function-body directives
|
|
350
|
+
// ---------------------------------------------------------------------------
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Fallback that uses regex to find functions with directives when acorn
|
|
354
|
+
* cannot parse the file (TypeScript with type annotations).
|
|
355
|
+
*
|
|
356
|
+
* This is less precise but handles common patterns. The regex approach
|
|
357
|
+
* is only used as a fallback — pure JS/JSX files always use the AST path.
|
|
358
|
+
*/
|
|
359
|
+
function findFunctionsWithDirectiveFallback(
|
|
360
|
+
code: string,
|
|
361
|
+
directive: string
|
|
362
|
+
): FunctionWithDirective[] {
|
|
363
|
+
const results: FunctionWithDirective[] = [];
|
|
364
|
+
const directivePattern = new RegExp(`['"]${escapeRegex(directive)}['"]`);
|
|
365
|
+
|
|
366
|
+
// Quick bail-out
|
|
367
|
+
if (!directivePattern.test(code)) return results;
|
|
368
|
+
|
|
369
|
+
// Pattern 1: function declarations
|
|
370
|
+
const fnDeclPattern =
|
|
371
|
+
/(?:(export\s+default\s+|export\s+))?async\s+function\s+(\w+)\s*\([^)]*\)\s*\{/g;
|
|
372
|
+
let match: RegExpExecArray | null;
|
|
373
|
+
|
|
374
|
+
while ((match = fnDeclPattern.exec(code)) !== null) {
|
|
375
|
+
const prefix = match[1]?.trim() || '';
|
|
376
|
+
const name = match[2];
|
|
377
|
+
const bodyStart = match.index + match[0].length;
|
|
378
|
+
const bodyEnd = findMatchingBraceFallback(code, bodyStart - 1);
|
|
379
|
+
if (bodyEnd === -1) continue;
|
|
380
|
+
|
|
381
|
+
const bodyContent = code.slice(bodyStart, bodyEnd);
|
|
382
|
+
// Check that the directive is the first meaningful statement
|
|
383
|
+
const trimmedBody = bodyContent.trimStart();
|
|
384
|
+
if (!trimmedBody.startsWith(`'${directive}'`) && !trimmedBody.startsWith(`"${directive}"`))
|
|
385
|
+
continue;
|
|
386
|
+
|
|
387
|
+
const directiveLine =
|
|
388
|
+
code.slice(0, bodyStart).split('\n').length +
|
|
389
|
+
bodyContent.slice(0, bodyContent.indexOf(directive)).split('\n').length -
|
|
390
|
+
1;
|
|
391
|
+
|
|
392
|
+
results.push({
|
|
393
|
+
name,
|
|
394
|
+
directive,
|
|
395
|
+
directiveLine,
|
|
396
|
+
start: match.index,
|
|
397
|
+
end: bodyEnd + 1,
|
|
398
|
+
bodyStart,
|
|
399
|
+
bodyEnd,
|
|
400
|
+
bodyContent,
|
|
401
|
+
prefix: prefix ? prefix + ' ' : '',
|
|
402
|
+
isArrow: false,
|
|
403
|
+
declaration: code.slice(match.index, bodyStart - 1).trimEnd(),
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Pattern 2: arrow functions
|
|
408
|
+
const arrowPattern = /(?:const|let|var)\s+(\w+)\s*=\s*async\s*(\([^)]*\)|[^=]*?)\s*=>\s*\{/g;
|
|
409
|
+
while ((match = arrowPattern.exec(code)) !== null) {
|
|
410
|
+
const name = match[1];
|
|
411
|
+
const bodyStart = match.index + match[0].length;
|
|
412
|
+
const bodyEnd = findMatchingBraceFallback(code, bodyStart - 1);
|
|
413
|
+
if (bodyEnd === -1) continue;
|
|
414
|
+
|
|
415
|
+
const bodyContent = code.slice(bodyStart, bodyEnd);
|
|
416
|
+
const trimmedBody = bodyContent.trimStart();
|
|
417
|
+
if (!trimmedBody.startsWith(`'${directive}'`) && !trimmedBody.startsWith(`"${directive}"`))
|
|
418
|
+
continue;
|
|
419
|
+
|
|
420
|
+
const directiveLine =
|
|
421
|
+
code.slice(0, bodyStart).split('\n').length +
|
|
422
|
+
bodyContent.slice(0, bodyContent.indexOf(directive)).split('\n').length -
|
|
423
|
+
1;
|
|
424
|
+
|
|
425
|
+
results.push({
|
|
426
|
+
name,
|
|
427
|
+
directive,
|
|
428
|
+
directiveLine,
|
|
429
|
+
start: match.index,
|
|
430
|
+
end: bodyEnd + 1,
|
|
431
|
+
bodyStart,
|
|
432
|
+
bodyEnd,
|
|
433
|
+
bodyContent,
|
|
434
|
+
prefix: '',
|
|
435
|
+
isArrow: true,
|
|
436
|
+
declaration: code.slice(match.index, bodyStart - 1).trimEnd(),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
results.sort((a, b) => b.start - a.start);
|
|
441
|
+
return results;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function escapeRegex(s: string): string {
|
|
445
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Find matching closing brace — same algorithm as cache-transform.ts
|
|
450
|
+
* but kept here for the fallback path only.
|
|
451
|
+
*/
|
|
452
|
+
function findMatchingBraceFallback(code: string, openPos: number): number {
|
|
453
|
+
let depth = 1;
|
|
454
|
+
let i = openPos + 1;
|
|
455
|
+
|
|
456
|
+
while (i < code.length && depth > 0) {
|
|
457
|
+
const ch = code[i];
|
|
458
|
+
|
|
459
|
+
if (ch === "'" || ch === '"') {
|
|
460
|
+
i = skipStringFallback(code, i);
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (ch === '`') {
|
|
464
|
+
i = skipTemplateFallback(code, i);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
468
|
+
i = code.indexOf('\n', i);
|
|
469
|
+
if (i === -1) return -1;
|
|
470
|
+
i++;
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
474
|
+
i = code.indexOf('*/', i + 2);
|
|
475
|
+
if (i === -1) return -1;
|
|
476
|
+
i += 2;
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (ch === '{') depth++;
|
|
481
|
+
else if (ch === '}') depth--;
|
|
482
|
+
i++;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return depth === 0 ? i - 1 : -1;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function skipStringFallback(code: string, start: number): number {
|
|
489
|
+
const quote = code[start];
|
|
490
|
+
let i = start + 1;
|
|
491
|
+
while (i < code.length) {
|
|
492
|
+
if (code[i] === '\\') {
|
|
493
|
+
i += 2;
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
if (code[i] === quote) return i + 1;
|
|
497
|
+
i++;
|
|
498
|
+
}
|
|
499
|
+
return i;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function skipTemplateFallback(code: string, start: number): number {
|
|
503
|
+
let i = start + 1;
|
|
504
|
+
while (i < code.length) {
|
|
505
|
+
if (code[i] === '\\') {
|
|
506
|
+
i += 2;
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
509
|
+
if (code[i] === '`') return i + 1;
|
|
510
|
+
if (code[i] === '$' && code[i + 1] === '{') {
|
|
511
|
+
i = findMatchingBraceFallback(code, i + 1) + 1;
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
i++;
|
|
515
|
+
}
|
|
516
|
+
return i;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Convenience: quick regex check (for fast bail-out before AST parsing)
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Quick regex check for whether code contains a directive string.
|
|
525
|
+
* Use as a fast bail-out before calling the AST-based functions.
|
|
526
|
+
*/
|
|
527
|
+
export function containsDirective(code: string, directive: string): boolean {
|
|
528
|
+
return code.includes(directive);
|
|
529
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared formatting utilities.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** Format a byte count as a human-readable string (e.g. "1.50 kB"). */
|
|
6
|
+
export function formatSize(bytes: number): string {
|
|
7
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
8
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(2)} kB`;
|
|
9
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
10
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Startup timer — records named phases with their durations.
|
|
3
|
+
*
|
|
4
|
+
* Used by the plugin system to instrument cold start and report a
|
|
5
|
+
* timing breakdown in dev mode. Zero overhead in production (disabled).
|
|
6
|
+
*
|
|
7
|
+
* See design/18-build-system.md, TIM-155.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { performance } from 'node:perf_hooks';
|
|
11
|
+
|
|
12
|
+
export interface PhaseRecord {
|
|
13
|
+
name: string;
|
|
14
|
+
startMs: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface StartupTimer {
|
|
19
|
+
/** Mark the beginning of a named phase. */
|
|
20
|
+
start(phase: string): void;
|
|
21
|
+
/** Mark the end of a named phase. Returns duration in ms. */
|
|
22
|
+
end(phase: string): number;
|
|
23
|
+
/** Get all completed phase records, ordered by start time. */
|
|
24
|
+
getPhases(): PhaseRecord[];
|
|
25
|
+
/** Total elapsed time from first start() to last end(). */
|
|
26
|
+
totalMs(): number;
|
|
27
|
+
/** Format a human-readable summary string. */
|
|
28
|
+
formatSummary(): string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Create a startup timer that records phase durations.
|
|
33
|
+
*/
|
|
34
|
+
export function createStartupTimer(): StartupTimer {
|
|
35
|
+
const pending = new Map<string, number>();
|
|
36
|
+
const phases: PhaseRecord[] = [];
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
start(phase: string): void {
|
|
40
|
+
pending.set(phase, performance.now());
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
end(phase: string): number {
|
|
44
|
+
const startMs = pending.get(phase);
|
|
45
|
+
if (startMs === undefined) {
|
|
46
|
+
return 0;
|
|
47
|
+
}
|
|
48
|
+
pending.delete(phase);
|
|
49
|
+
const durationMs = performance.now() - startMs;
|
|
50
|
+
phases.push({ name: phase, startMs, durationMs });
|
|
51
|
+
return durationMs;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
getPhases(): PhaseRecord[] {
|
|
55
|
+
return [...phases].sort((a, b) => a.startMs - b.startMs);
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
totalMs(): number {
|
|
59
|
+
if (phases.length === 0) return 0;
|
|
60
|
+
const sorted = this.getPhases();
|
|
61
|
+
const first = sorted[0];
|
|
62
|
+
const last = sorted[sorted.length - 1];
|
|
63
|
+
return last.startMs + last.durationMs - first.startMs;
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
formatSummary(): string {
|
|
67
|
+
const sorted = this.getPhases();
|
|
68
|
+
if (sorted.length === 0) return 'No phases recorded.';
|
|
69
|
+
|
|
70
|
+
const lines = sorted.map((p) => {
|
|
71
|
+
const ms = p.durationMs.toFixed(1);
|
|
72
|
+
return ` ${p.name.padEnd(30)} ${ms.padStart(8)}ms`;
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const total = this.totalMs().toFixed(1);
|
|
76
|
+
lines.push(` ${'total'.padEnd(30)} ${total.padStart(8)}ms`);
|
|
77
|
+
|
|
78
|
+
return ['[timber] startup timing:', ...lines].join('\n');
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* No-op timer for production builds — all methods are empty.
|
|
85
|
+
*/
|
|
86
|
+
export function createNoopTimer(): StartupTimer {
|
|
87
|
+
return {
|
|
88
|
+
start() {},
|
|
89
|
+
end() {
|
|
90
|
+
return 0;
|
|
91
|
+
},
|
|
92
|
+
getPhases() {
|
|
93
|
+
return [];
|
|
94
|
+
},
|
|
95
|
+
totalMs() {
|
|
96
|
+
return 0;
|
|
97
|
+
},
|
|
98
|
+
formatSummary() {
|
|
99
|
+
return '';
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|