@timber-js/app 0.1.1 → 0.1.3

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 (143) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +11 -7
  3. package/dist/index.js.map +1 -1
  4. package/dist/plugins/dev-server.d.ts.map +1 -1
  5. package/dist/plugins/entries.d.ts.map +1 -1
  6. package/package.json +5 -4
  7. package/src/adapters/cloudflare.ts +325 -0
  8. package/src/adapters/nitro.ts +366 -0
  9. package/src/adapters/types.ts +63 -0
  10. package/src/cache/index.ts +91 -0
  11. package/src/cache/redis-handler.ts +91 -0
  12. package/src/cache/register-cached-function.ts +99 -0
  13. package/src/cache/singleflight.ts +26 -0
  14. package/src/cache/stable-stringify.ts +21 -0
  15. package/src/cache/timber-cache.ts +116 -0
  16. package/src/cli.ts +201 -0
  17. package/src/client/browser-entry.ts +663 -0
  18. package/src/client/error-boundary.tsx +209 -0
  19. package/src/client/form.tsx +200 -0
  20. package/src/client/head.ts +61 -0
  21. package/src/client/history.ts +46 -0
  22. package/src/client/index.ts +60 -0
  23. package/src/client/link-navigate-interceptor.tsx +62 -0
  24. package/src/client/link-status-provider.tsx +40 -0
  25. package/src/client/link.tsx +310 -0
  26. package/src/client/nuqs-adapter.tsx +117 -0
  27. package/src/client/router-ref.ts +25 -0
  28. package/src/client/router.ts +563 -0
  29. package/src/client/segment-cache.ts +194 -0
  30. package/src/client/segment-context.ts +57 -0
  31. package/src/client/ssr-data.ts +95 -0
  32. package/src/client/types.ts +4 -0
  33. package/src/client/unload-guard.ts +34 -0
  34. package/src/client/use-cookie.ts +122 -0
  35. package/src/client/use-link-status.ts +46 -0
  36. package/src/client/use-navigation-pending.ts +47 -0
  37. package/src/client/use-params.ts +71 -0
  38. package/src/client/use-pathname.ts +43 -0
  39. package/src/client/use-query-states.ts +133 -0
  40. package/src/client/use-router.ts +77 -0
  41. package/src/client/use-search-params.ts +74 -0
  42. package/src/client/use-selected-layout-segment.ts +110 -0
  43. package/src/content/index.ts +13 -0
  44. package/src/cookies/define-cookie.ts +137 -0
  45. package/src/cookies/index.ts +9 -0
  46. package/src/fonts/ast.ts +359 -0
  47. package/src/fonts/css.ts +68 -0
  48. package/src/fonts/fallbacks.ts +248 -0
  49. package/src/fonts/google.ts +332 -0
  50. package/src/fonts/local.ts +177 -0
  51. package/src/fonts/types.ts +88 -0
  52. package/src/index.ts +420 -0
  53. package/src/plugins/adapter-build.ts +118 -0
  54. package/src/plugins/build-manifest.ts +323 -0
  55. package/src/plugins/build-report.ts +353 -0
  56. package/src/plugins/cache-transform.ts +199 -0
  57. package/src/plugins/chunks.ts +90 -0
  58. package/src/plugins/content.ts +136 -0
  59. package/src/plugins/dev-error-overlay.ts +230 -0
  60. package/src/plugins/dev-logs.ts +280 -0
  61. package/src/plugins/dev-server.ts +391 -0
  62. package/src/plugins/dynamic-transform.ts +161 -0
  63. package/src/plugins/entries.ts +214 -0
  64. package/src/plugins/fonts.ts +581 -0
  65. package/src/plugins/mdx.ts +179 -0
  66. package/src/plugins/react-prod.ts +56 -0
  67. package/src/plugins/routing.ts +419 -0
  68. package/src/plugins/server-action-exports.ts +220 -0
  69. package/src/plugins/server-bundle.ts +113 -0
  70. package/src/plugins/shims.ts +168 -0
  71. package/src/plugins/static-build.ts +207 -0
  72. package/src/routing/codegen.ts +396 -0
  73. package/src/routing/index.ts +14 -0
  74. package/src/routing/interception.ts +173 -0
  75. package/src/routing/scanner.ts +487 -0
  76. package/src/routing/status-file-lint.ts +114 -0
  77. package/src/routing/types.ts +100 -0
  78. package/src/search-params/analyze.ts +192 -0
  79. package/src/search-params/codecs.ts +153 -0
  80. package/src/search-params/create.ts +314 -0
  81. package/src/search-params/index.ts +23 -0
  82. package/src/search-params/registry.ts +31 -0
  83. package/src/server/access-gate.tsx +142 -0
  84. package/src/server/action-client.ts +473 -0
  85. package/src/server/action-handler.ts +325 -0
  86. package/src/server/actions.ts +236 -0
  87. package/src/server/asset-headers.ts +81 -0
  88. package/src/server/body-limits.ts +102 -0
  89. package/src/server/build-manifest.ts +234 -0
  90. package/src/server/canonicalize.ts +90 -0
  91. package/src/server/client-module-map.ts +58 -0
  92. package/src/server/csrf.ts +79 -0
  93. package/src/server/deny-renderer.ts +302 -0
  94. package/src/server/dev-logger.ts +419 -0
  95. package/src/server/dev-span-processor.ts +78 -0
  96. package/src/server/dev-warnings.ts +282 -0
  97. package/src/server/early-hints-sender.ts +55 -0
  98. package/src/server/early-hints.ts +142 -0
  99. package/src/server/error-boundary-wrapper.ts +69 -0
  100. package/src/server/error-formatter.ts +184 -0
  101. package/src/server/flush.ts +182 -0
  102. package/src/server/form-data.ts +176 -0
  103. package/src/server/form-flash.ts +93 -0
  104. package/src/server/html-injectors.ts +445 -0
  105. package/src/server/index.ts +222 -0
  106. package/src/server/instrumentation.ts +136 -0
  107. package/src/server/logger.ts +145 -0
  108. package/src/server/manifest-status-resolver.ts +215 -0
  109. package/src/server/metadata-render.ts +527 -0
  110. package/src/server/metadata-routes.ts +189 -0
  111. package/src/server/metadata.ts +263 -0
  112. package/src/server/middleware-runner.ts +32 -0
  113. package/src/server/nuqs-ssr-provider.tsx +63 -0
  114. package/src/server/pipeline.ts +555 -0
  115. package/src/server/prerender.ts +139 -0
  116. package/src/server/primitives.ts +264 -0
  117. package/src/server/proxy.ts +43 -0
  118. package/src/server/request-context.ts +554 -0
  119. package/src/server/route-element-builder.ts +395 -0
  120. package/src/server/route-handler.ts +153 -0
  121. package/src/server/route-matcher.ts +316 -0
  122. package/src/server/rsc-entry/api-handler.ts +112 -0
  123. package/src/server/rsc-entry/error-renderer.ts +177 -0
  124. package/src/server/rsc-entry/helpers.ts +147 -0
  125. package/src/server/rsc-entry/index.ts +688 -0
  126. package/src/server/rsc-entry/ssr-bridge.ts +18 -0
  127. package/src/server/slot-resolver.ts +359 -0
  128. package/src/server/ssr-entry.ts +161 -0
  129. package/src/server/ssr-render.ts +200 -0
  130. package/src/server/status-code-resolver.ts +282 -0
  131. package/src/server/tracing.ts +281 -0
  132. package/src/server/tree-builder.ts +354 -0
  133. package/src/server/types.ts +150 -0
  134. package/src/shims/font-google.ts +67 -0
  135. package/src/shims/headers.ts +11 -0
  136. package/src/shims/image.ts +48 -0
  137. package/src/shims/link.ts +9 -0
  138. package/src/shims/navigation-client.ts +52 -0
  139. package/src/shims/navigation.ts +31 -0
  140. package/src/shims/server-only-noop.js +5 -0
  141. package/src/utils/directive-parser.ts +529 -0
  142. package/src/utils/format.ts +10 -0
  143. 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
+ }