@vesk/adapter 0.2.9 → 0.2.11

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 (42) hide show
  1. package/dist/client-bundle.d.ts +29 -0
  2. package/dist/client-bundle.d.ts.map +1 -1
  3. package/dist/client-bundle.js +333 -52
  4. package/dist/dev-api.d.ts +78 -0
  5. package/dist/dev-api.d.ts.map +1 -0
  6. package/dist/dev-api.js +338 -0
  7. package/dist/dev-config.d.ts +48 -0
  8. package/dist/dev-config.d.ts.map +1 -0
  9. package/dist/dev-config.js +964 -0
  10. package/dist/dev-server.d.ts +85 -0
  11. package/dist/dev-server.d.ts.map +1 -1
  12. package/dist/dev-server.js +329 -8
  13. package/dist/error-codeframe.d.ts +23 -0
  14. package/dist/error-codeframe.d.ts.map +1 -0
  15. package/dist/error-codeframe.js +127 -0
  16. package/dist/error-tips.d.ts +7 -0
  17. package/dist/error-tips.d.ts.map +1 -0
  18. package/dist/error-tips.js +91 -0
  19. package/dist/hmr-utils.d.ts +14 -0
  20. package/dist/hmr-utils.d.ts.map +1 -0
  21. package/dist/hmr-utils.js +56 -0
  22. package/dist/hmr.d.ts +40 -0
  23. package/dist/hmr.d.ts.map +1 -1
  24. package/dist/hmr.js +139 -20
  25. package/dist/index.d.ts +37 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +105 -22
  28. package/dist/paths.d.ts +8 -0
  29. package/dist/paths.d.ts.map +1 -1
  30. package/dist/paths.js +32 -0
  31. package/dist/platform-handler.d.ts.map +1 -1
  32. package/dist/platform-handler.js +2 -1
  33. package/dist/plugins.d.ts +147 -0
  34. package/dist/plugins.d.ts.map +1 -0
  35. package/dist/plugins.js +1109 -0
  36. package/dist/prod-server.d.ts.map +1 -1
  37. package/dist/prod-server.js +43 -9
  38. package/dist/ssr-function.d.ts.map +1 -1
  39. package/dist/ssr-function.js +14 -2
  40. package/dist/types.d.ts +1 -1
  41. package/dist/types.d.ts.map +1 -1
  42. package/package.json +4 -4
@@ -1,4 +1,33 @@
1
1
  import type { RouteNode, ClientBundleOptions, ClientBundleResult } from '@vesk/adapter/src/types';
2
+ /**
3
+ * Pure name-collection used by `generateClientBundle`'s tree-shake request.
4
+ *
5
+ * Parses the (already compiled) client module with the compiler's AST pass and
6
+ * keeps only genuine top-level `import { … } from '@vesk/runtime…'` declarations.
7
+ * Import-shaped text inside template literals, strings, or comments — e.g. a
8
+ * doc code sample stored as `const md = \`import { VeskResponse } from
9
+ * '@vesk/runtime/server'\`` — never becomes an ImportDeclaration node, so those
10
+ * server-only names cannot poison the client runtime request. This is
11
+ * deliberately AST-only (no text scanning): text heuristics are what let
12
+ * server-only names leak into the client tree-shake request in the first
13
+ * place. If the compiled module ever fails to parse, we collect nothing rather
14
+ * than guess.
15
+ */
16
+ export declare function extractRuntimeImportNames(code: string): string[];
17
+ /**
18
+ * Builds the eval-safe HMR update snippet for one compiled file: drops
19
+ * imports, re-exports and the registry preamble (`const __components = …`,
20
+ * `const __hydrators = …`), demotes remaining value exports, and wraps the
21
+ * rest in a block. Component registrations (`__components["X"] = …`) still
22
+ * reach the live registries and stay visible to nothing else, file
23
+ * top-level bindings (`const navItems`, helpers, …) stay visible to the
24
+ * component closures that reference them, and repeated evals of the same
25
+ * file can no longer redeclare its top-level consts. Without the file
26
+ * scope, an updated component renders against missing bindings and the
27
+ * swap fails with a ReferenceError that surfaces nowhere the tests assert
28
+ * (no reload, stale DOM, zero page errors).
29
+ */
30
+ export declare function buildHmrEvalSnippet(code: string): string;
2
31
  export declare function generateClientBundle(routeTree: RouteNode[], appDir: string, componentMap?: Map<string, string>, options?: ClientBundleOptions): Promise<ClientBundleResult>;
3
32
  export declare function buildRuntimeCode(runtimeDir: string): string;
4
33
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAqC,MAAM,yBAAyB,CAAC;AAoCrI,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CAoT7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAID;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA+BrG"}
1
+ {"version":3,"file":"client-bundle.d.ts","sourceRoot":"","sources":["../src/client-bundle.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,SAAS,EAAE,mBAAmB,EAAE,kBAAkB,EAAqC,MAAM,yBAAyB,CAAC;AAoCrI;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CA8BhE;AAoLD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAkBxD;AAmCD,wBAAsB,oBAAoB,CACxC,SAAS,EAAE,SAAS,EAAE,EACtB,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAClC,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC,CA0S7B;AAMD,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,CA+B3D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAUlE;AAYD;;;;;;;;GAQG;AACH,wBAAsB,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCrG"}
@@ -1,8 +1,9 @@
1
- import { readFileSync, existsSync, writeFileSync, unlinkSync, statSync } from 'node:fs';
1
+ import { readFileSync, existsSync, writeFileSync, unlinkSync, statSync, rmSync } from 'node:fs';
2
2
  import { resolve, join, dirname, relative, sep } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
4
  import { build } from './esbuild-fallback.js';
5
5
  import { stripCodeTypes } from '@vesk/compiler/src/strip-ts';
6
+ import { parse } from '@vesk/compiler/src/parser';
6
7
  import { compileClient, compileClientBoth } from '@vesk/compiler/src/client-codegen';
7
8
  import { resolveComponentName } from '@vesk/compiler/src/server-codegen';
8
9
  import { collectVskImportPaths, vskImportLines } from '@vesk/compiler/src/vsk-imports';
@@ -39,6 +40,276 @@ function findRuntimeSrc(appDir) {
39
40
  }
40
41
  throw new Error('@vesk/runtime/dist not found — run "npm run build" first');
41
42
  }
43
+ /**
44
+ * Pure name-collection used by `generateClientBundle`'s tree-shake request.
45
+ *
46
+ * Parses the (already compiled) client module with the compiler's AST pass and
47
+ * keeps only genuine top-level `import { … } from '@vesk/runtime…'` declarations.
48
+ * Import-shaped text inside template literals, strings, or comments — e.g. a
49
+ * doc code sample stored as `const md = \`import { VeskResponse } from
50
+ * '@vesk/runtime/server'\`` — never becomes an ImportDeclaration node, so those
51
+ * server-only names cannot poison the client runtime request. This is
52
+ * deliberately AST-only (no text scanning): text heuristics are what let
53
+ * server-only names leak into the client tree-shake request in the first
54
+ * place. If the compiled module ever fails to parse, we collect nothing rather
55
+ * than guess.
56
+ */
57
+ export function extractRuntimeImportNames(code) {
58
+ const names = [];
59
+ let ast;
60
+ try {
61
+ ast = parse(code);
62
+ }
63
+ catch {
64
+ return names;
65
+ }
66
+ const body = ast.body ?? [];
67
+ for (const raw of body) {
68
+ const node = raw;
69
+ if (node.type !== 'ImportDeclaration')
70
+ continue;
71
+ const src = node.source?.value;
72
+ if (typeof src !== 'string' || !(src === '@vesk/runtime' || src.startsWith('@vesk/runtime/')))
73
+ continue;
74
+ if (node.importKind === 'type')
75
+ continue;
76
+ for (const spec of node.specifiers ?? []) {
77
+ const name = spec.type === 'ImportSpecifier' ? spec.imported?.name : spec.local?.name;
78
+ if (name)
79
+ names.push(name);
80
+ }
81
+ }
82
+ return names;
83
+ }
84
+ function isRuntimeImport(node) {
85
+ if (node.type !== 'ImportDeclaration')
86
+ return false;
87
+ const src = node.source?.value;
88
+ return src === '@vesk/runtime' || (typeof src === 'string' && src.startsWith('@vesk/runtime/'));
89
+ }
90
+ function isVskImport(node) {
91
+ if (node.type !== 'ImportDeclaration')
92
+ return false;
93
+ const src = node.source?.value;
94
+ return typeof src === 'string' && src.endsWith('.vsk');
95
+ }
96
+ function isAnyImport(node) {
97
+ return node.type === 'ImportDeclaration';
98
+ }
99
+ /**
100
+ * Generated client-preamble nodes the bundle must not re-emit: the local
101
+ * `const __components = {}` registry plus the `__cleanup` / `__place` helpers —
102
+ * the chunk IIFEs already define their own `__components`, so a second
103
+ * declaration in the same scope would be a syntax error.
104
+ */
105
+ function isRuntimePreamble(node) {
106
+ if (node.type === 'FunctionDeclaration' && (node.id?.name === '__cleanup' || node.id?.name === '__place')) {
107
+ return true;
108
+ }
109
+ if (node.type === 'VariableDeclaration') {
110
+ return node.declarations?.[0]?.id?.name === '__components';
111
+ }
112
+ return false;
113
+ }
114
+ /**
115
+ * `export default __components[<name>];` / `export const <name> = __components[<name>];`
116
+ * aliases emitted by codegen. The bundle registers components on `__components`
117
+ * directly, so these module-level re-exports must go.
118
+ */
119
+ function isComponentExport(node) {
120
+ if (node.type === 'ExportDefaultDeclaration') {
121
+ const d = node.declaration;
122
+ return !!d && d.type === 'MemberExpression' && d.object?.type === 'Identifier' && d.object.name === '__components';
123
+ }
124
+ if (node.type === 'ExportNamedDeclaration') {
125
+ const init = node.declaration?.type === 'VariableDeclaration' ? node.declaration.declarations?.[0]?.init : undefined;
126
+ return !!init && init.type === 'MemberExpression' && init.object?.type === 'Identifier' && init.object.name === '__components';
127
+ }
128
+ return false;
129
+ }
130
+ /**
131
+ * Extracts matched top-level statements from a compiled client module using
132
+ * the parser's exact `start`/`end` offsets — the complement of
133
+ * `removeCompiledNodes`. AST-only, like everything else in this file.
134
+ */
135
+ function extractCompiledNodes(code, isTarget) {
136
+ let ast;
137
+ try {
138
+ ast = parse(code);
139
+ }
140
+ catch {
141
+ return '';
142
+ }
143
+ const parts = [];
144
+ for (const raw of ast.body ?? []) {
145
+ const node = raw;
146
+ if (isTarget(node) && typeof node.start === 'number' && typeof node.end === 'number') {
147
+ let cut = node.end;
148
+ if (code[cut] === ';')
149
+ cut++;
150
+ parts.push(code.slice(node.start, cut));
151
+ }
152
+ }
153
+ return parts.join('\n');
154
+ }
155
+ function isModuleLevel(node) {
156
+ // import/export statements must stay at module top level (hoisted imports
157
+ // of the same module are harmless duplicates); everything else is scoped.
158
+ return node.type === 'ImportDeclaration'
159
+ || node.type === 'ExportNamedDeclaration'
160
+ || node.type === 'ExportDefaultDeclaration';
161
+ }
162
+ /**
163
+ * Demotes surviving value exports to plain declarations (`export const x`
164
+ * becomes `const x`) and drops bare re-export lists (`export { x }`,
165
+ * `export default …`). Code-split chunks execute as classic scripts inside
166
+ * an IIFE, so any `export` statement is a SyntaxError — and the chunk-local
167
+ * binding is all cross-file references need (components resolve through the
168
+ * `__components` / `__hydrators` registries). Offset-based on the parser AST.
169
+ */
170
+ function demoteExports(code) {
171
+ let ast;
172
+ try {
173
+ ast = parse(code);
174
+ }
175
+ catch {
176
+ return code;
177
+ }
178
+ const body = ast.body ?? [];
179
+ const edits = [];
180
+ for (const raw of body) {
181
+ const node = raw;
182
+ if (typeof node.start !== 'number' || typeof node.end !== 'number')
183
+ continue;
184
+ if (node.type === 'ExportNamedDeclaration' && node.declaration
185
+ && typeof node.declaration.start === 'number' && typeof node.declaration.end === 'number') {
186
+ edits.push({ start: node.start, end: node.end, text: code.slice(node.declaration.start, node.declaration.end) });
187
+ }
188
+ else if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
189
+ let cut = node.end;
190
+ if (code[cut] === ';')
191
+ cut++;
192
+ edits.push({ start: node.start, end: cut, text: '' });
193
+ }
194
+ }
195
+ edits.sort((a, b) => b.start - a.start);
196
+ for (const e of edits)
197
+ code = code.slice(0, e.start) + e.text + code.slice(e.end);
198
+ return code;
199
+ }
200
+ /**
201
+ * Code-split chunks execute as classic scripts, so NO import statement may
202
+ * survive in a contribution (runtime and `.vsk` imports are stripped
203
+ * earlier; npm packages and relative `.ts` values are unresolvable from a
204
+ * `/_vesk/static/` classic script). Strips every remaining import and
205
+ * demotes value exports — server-only components never execute
206
+ * client-side, and islands must receive data through props/serialization
207
+ * rather than module imports (a stripped binding referenced by an island
208
+ * fails loudly at that island instead of killing the whole chunk).
209
+ */
210
+ function stripChunkImports(code) {
211
+ return demoteExports(removeCompiledNodes(code, isAnyImport));
212
+ }
213
+ /**
214
+ * Scopes one file's contribution to the shared chunk: module-level
215
+ * import/export statements pass through untouched, all other top-level
216
+ * statements (component registrations, top-level `const` helpers, …) are
217
+ * wrapped in a block. Without this, same-named top-level bindings from
218
+ * different `.vsk` files (e.g. two files declaring `const stages`) collide
219
+ * in the concatenated chunk scope and the whole chunk fails to parse —
220
+ * killing hydration for the entire page. Cross-file references always go
221
+ * through the `__components` / `__hydrators` registries (never bare
222
+ * identifiers), so hiding file locals in a block is safe; assignments like
223
+ * `__components["X"] = …` still reach the outer binding.
224
+ */
225
+ function scopeFileContribution(code) {
226
+ const head = extractCompiledNodes(code, isModuleLevel);
227
+ const rest = removeCompiledNodes(code, isModuleLevel);
228
+ if (!rest.trim())
229
+ return head;
230
+ const scoped = (head ? head + '\n' : '') + '{\n' + rest + '\n}';
231
+ // Never trade a potential duplicate-binding error for a certain syntax
232
+ // error: if the scoped form does not parse, keep the original.
233
+ try {
234
+ parse(scoped);
235
+ return scoped;
236
+ }
237
+ catch {
238
+ return code;
239
+ }
240
+ }
241
+ /**
242
+ * Builds the eval-safe HMR update snippet for one compiled file: drops
243
+ * imports, re-exports and the registry preamble (`const __components = …`,
244
+ * `const __hydrators = …`), demotes remaining value exports, and wraps the
245
+ * rest in a block. Component registrations (`__components["X"] = …`) still
246
+ * reach the live registries and stay visible to nothing else, file
247
+ * top-level bindings (`const navItems`, helpers, …) stay visible to the
248
+ * component closures that reference them, and repeated evals of the same
249
+ * file can no longer redeclare its top-level consts. Without the file
250
+ * scope, an updated component renders against missing bindings and the
251
+ * swap fails with a ReferenceError that surfaces nowhere the tests assert
252
+ * (no reload, stale DOM, zero page errors).
253
+ */
254
+ export function buildHmrEvalSnippet(code) {
255
+ const isSnippetDrop = (node) => {
256
+ if (isAnyImport(node) || isComponentExport(node) || isRuntimePreamble(node))
257
+ return true;
258
+ return node.type === 'VariableDeclaration'
259
+ && (node.declarations?.[0]?.id?.name === '__components'
260
+ || node.declarations?.[0]?.id?.name === '__hydrators');
261
+ };
262
+ const body = demoteExports(removeCompiledNodes(code, isSnippetDrop)).trim();
263
+ if (!body)
264
+ return '';
265
+ const scoped = `{\n${body}\n}`;
266
+ // Never trade a potential duplicate-binding error for a certain syntax
267
+ // error: if the scoped form does not parse, keep the unscoped body.
268
+ try {
269
+ parse(scoped);
270
+ return scoped;
271
+ }
272
+ catch {
273
+ return body;
274
+ }
275
+ }
276
+ /**
277
+ * Removes matched top-level statements from a compiled client module using the
278
+ * parser's exact `start`/`end` offsets — never a text scan, so a doc sample
279
+ * stored inside a template literal can never be mistaken for a real statement.
280
+ * If the module cannot be parsed it is returned untouched (the compiler's own
281
+ * output is always valid ESM, so that branch is unreachable in practice).
282
+ */
283
+ function removeCompiledNodes(code, isTarget) {
284
+ let ast;
285
+ try {
286
+ ast = parse(code);
287
+ }
288
+ catch {
289
+ return code;
290
+ }
291
+ const ranges = [];
292
+ for (const raw of ast.body ?? []) {
293
+ const node = raw;
294
+ if (isTarget(node) && typeof node.start === 'number' && typeof node.end === 'number') {
295
+ ranges.push([node.start, node.end]);
296
+ }
297
+ }
298
+ if (ranges.length === 0)
299
+ return code;
300
+ ranges.sort((a, b) => b[0] - a[0]);
301
+ for (const [start, end] of ranges) {
302
+ let cut = end;
303
+ if (code[cut] === ';')
304
+ cut++;
305
+ if (code.startsWith('\r\n', cut))
306
+ cut += 2;
307
+ else if (code[cut] === '\n' || code[cut] === '\r')
308
+ cut++;
309
+ code = code.slice(0, start) + code.slice(cut);
310
+ }
311
+ return code;
312
+ }
42
313
  export async function generateClientBundle(routeTree, appDir, componentMap, options) {
43
314
  const runtimeDir = findRuntimeSrc(appDir);
44
315
  const seen = new Set();
@@ -55,27 +326,22 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
55
326
  function mustReuseWithoutStat(filePath) {
56
327
  return !!only && !only.has(filePath);
57
328
  }
329
+ /**
330
+ * Collects the runtime names a module genuinely imports from `@vesk/runtime*`
331
+ * (see `extractRuntimeImportNames`). Deliberately AST-only — import-shaped
332
+ * text inside template literals (e.g. a doc sample `const md = \`import {
333
+ * VeskResponse } from '@vesk/runtime/server'\``) must NOT force a client
334
+ * tree-shake request for a server-only name.
335
+ */
58
336
  function collectRuntimeImports(code) {
59
- // matches bare '@vesk/runtime' and any of its subpaths
60
- // ('@vesk/runtime/router', '@vesk/runtime/server', …)
61
- const re = /^import\s*\{([^}]*)\}\s*from\s*['"]@vesk\/runtime(\/[a-zA-Z0-9-]+)*['"];?\s*\n?/gm;
62
- for (const m of code.matchAll(re)) {
63
- for (const name of m[1].split(',')) {
64
- const trimmed = name.trim().replace(/^(\w+)\s+as\s+.*$/, '$1');
65
- if (!trimmed || /^(type|typeof)\s/.test(trimmed))
66
- continue;
67
- runtimeImportNames.add(trimmed);
68
- }
69
- }
337
+ for (const name of extractRuntimeImportNames(code))
338
+ runtimeImportNames.add(name);
70
339
  }
71
340
  function stripRuntimeImport(code) {
72
- return code.replace(/^import\s*\{[^}]*\}\s*from\s*['"]@vesk\/runtime(\/[a-zA-Z0-9-]+)*['"];?\s*\n?/gm, '')
73
- .replace(/const\s+__components\s*=\s*\{\};\s*\n?/g, '')
74
- .replace(/^function __cleanup\(start, end\) \{[\s\S]*?\n\}\s*\n?/gm, '')
75
- .replace(/^function __place\(start, end, nodes, fallback\) \{[\s\S]*?\n\}\s*\n?/gm, '');
341
+ return removeCompiledNodes(code, (node) => isRuntimeImport(node) || isRuntimePreamble(node));
76
342
  }
77
343
  function stripVskImports(code) {
78
- return code.replace(/^import\s*\{[^}]*\}\s*from\s*['"][^'"]*\.vsk['"];?\s*\n?/gm, '');
344
+ return removeCompiledNodes(code, isVskImport);
79
345
  }
80
346
  function resolveVskImports(filePath, compile) {
81
347
  const src = readFileSync(filePath, 'utf-8');
@@ -97,9 +363,7 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
97
363
  return resolved;
98
364
  }
99
365
  function stripExports(code) {
100
- return code
101
- .replace(/^export\s+default\s+__components\[.*?\];?\s*\n?/gm, '')
102
- .replace(/^export\s+(const|let|var)\s+\w+\s*=\s*__components\[.*?\];?\s*\n?/gm, '');
366
+ return removeCompiledNodes(code, isComponentExport);
103
367
  }
104
368
  function compileFile(filePath, resolvedName, output) {
105
369
  if (seen.has(filePath))
@@ -127,10 +391,12 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
127
391
  cachedFileHits++;
128
392
  for (const dep of cached.imports)
129
393
  compileFile(dep, cache?.files.get(dep)?.actualName ?? '', output);
130
- if (cached.compCode)
131
- output.push(cached.compCode);
132
- if (cached.hydCode)
133
- output.push(cached.hydCode);
394
+ // Comp and hyd contributions of one file share its top-level bindings
395
+ // (e.g. `const links` used by both the component and its hydrator),
396
+ // so they must be scoped together in a single block.
397
+ const cachedFileCode = [cached.compCode, cached.hydCode].filter(Boolean).join('\n');
398
+ if (cachedFileCode.trim())
399
+ output.push(scopeFileContribution(cachedFileCode));
134
400
  for (const n of cached.runtimeNames)
135
401
  runtimeImportNames.add(n);
136
402
  if (cached.actualName && resolvedName !== null && cached.actualName !== resolvedName) {
@@ -149,28 +415,26 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
149
415
  // One parse/IR pass feeds both client modes AND the component-name
150
416
  // lookup — the dev hot path pays the acorn+TS parse once per edit
151
417
  // instead of three times.
152
- const { comp: rawComp, hyd: rawHyd, name: actualName } = compileClientBoth(src, null);
153
- const compCode = rawComp ? stripExports(stripVskImports(stripRuntimeImport(rawComp))).replace(/^\n+/, '').replace(/\n+$/, '') : '';
154
- const hydCode = rawHyd ? stripExports(stripVskImports(stripRuntimeImport(rawHyd))).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
418
+ const { comp: rawComp, hyd: rawHyd, name: actualName } = compileClientBoth(src, null, filePath);
419
+ const compCode = rawComp ? stripChunkImports(stripExports(stripVskImports(stripRuntimeImport(rawComp)))).replace(/^\n+/, '').replace(/\n+$/, '') : '';
420
+ const hydCode = rawHyd ? stripChunkImports(stripExports(stripVskImports(stripRuntimeImport(rawHyd)))).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
155
421
  if (rawComp)
156
422
  collectRuntimeImports(rawComp);
157
423
  if (rawHyd)
158
424
  collectRuntimeImports(rawHyd);
159
425
  if (returnEdited && only.has(filePath)) {
160
- // Mirrors the HMR fnSources preparation in the dev servers: drop every
161
- // remaining top-level import so the snippet can be eval'd in the page
162
- // context where runtime globals already exist.
163
- const bare = rawComp
164
- ? stripExports(stripVskImports(stripRuntimeImport(rawComp)))
165
- .replace(/^import\s*[\s\S]*?from\s*['"][^'"]+['"];?\s*\n?/gm, '')
166
- : '';
167
- editedSources.set(filePath, bare);
426
+ // Eval-safe snippet for HMR fnSources (see buildHmrEvalSnippet):
427
+ // full file scope so component closures keep their top-level
428
+ // bindings, without imports/exports or the registry preamble, and
429
+ // block-wrapped so repeated evals cannot redeclare them.
430
+ editedSources.set(filePath, rawComp ? buildHmrEvalSnippet(rawComp) : '');
168
431
  editedNames.set(filePath, actualName);
169
432
  }
170
- if (compCode)
171
- output.push(compCode);
172
- if (hydCode)
173
- output.push(hydCode);
433
+ // Comp and hyd contributions of one file share its top-level bindings,
434
+ // so they are scoped together in a single block (see replay path above).
435
+ const fileCode = [compCode, hydCode].filter(Boolean).join('\n');
436
+ if (fileCode.trim())
437
+ output.push(scopeFileContribution(fileCode));
174
438
  if (actualName && resolvedName !== null && actualName !== resolvedName) {
175
439
  output.push(`Object.defineProperty(__components, ${JSON.stringify(resolvedName)}, { get: () => __components[${JSON.stringify(actualName)}], configurable: true });`);
176
440
  output.push(`Object.defineProperty(__hydrators, ${JSON.stringify(resolvedName)}, { get: () => __hydrators[${JSON.stringify(actualName)}], configurable: true });`);
@@ -286,19 +550,18 @@ export async function generateClientBundle(routeTree, appDir, componentMap, opti
286
550
  seen.add(filePath);
287
551
  const src = readFileSync(filePath, 'utf-8');
288
552
  resolveVskImports(filePath, (p, n) => compileFileMono(p, n || ''));
289
- const compCode = compileClient(src, null, { forceClient: true });
290
- if (compCode) {
553
+ const compCode = compileClient(src, null, { forceClient: true, sourcePath: filePath });
554
+ const hydCode = compileClient(src, null, { hydrate: true, forceClient: true, includeTopLevel: false, sourcePath: filePath });
555
+ if (compCode)
291
556
  collectRuntimeImports(compCode);
292
- const stripped = stripExports(stripVskImports(stripRuntimeImport(compCode)));
293
- componentLines.push(stripped.replace(/^\n+/, '').replace(/\n+$/, ''));
294
- }
295
- const hydCode = compileClient(src, null, { hydrate: true, forceClient: true, includeTopLevel: false });
296
- if (hydCode) {
557
+ if (hydCode)
297
558
  collectRuntimeImports(hydCode);
298
- const stripped = stripExports(stripVskImports(stripRuntimeImport(hydCode)))
299
- .replace(/__components/g, '__hydrators');
300
- hydratorLines.push(stripped.replace(/^\n+/, '').replace(/\n+$/, ''));
301
- }
559
+ // Comp and hyd share the file's top-level bindings — scope together.
560
+ const strippedComp = compCode ? stripExports(stripVskImports(stripRuntimeImport(compCode))).replace(/^\n+/, '').replace(/\n+$/, '') : '';
561
+ const strippedHyd = hydCode ? stripExports(stripVskImports(stripRuntimeImport(hydCode))).replace(/__components/g, '__hydrators').replace(/^\n+/, '').replace(/\n+$/, '') : '';
562
+ const fileCode = [strippedComp, strippedHyd].filter(Boolean).join('\n');
563
+ if (fileCode.trim())
564
+ componentLines.push(scopeFileContribution(fileCode));
302
565
  const actualName = resolveComponentName(src);
303
566
  if (actualName && actualName !== resolvedName) {
304
567
  aliasLines.push(`Object.defineProperty(__components, ${JSON.stringify(resolvedName)}, { get: () => __components[${JSON.stringify(actualName)}], configurable: true });`);
@@ -395,6 +658,13 @@ export function runtimeExportNames(runtimeDir) {
395
658
  return names;
396
659
  }
397
660
  let runtimeEntryId = 0;
661
+ /**
662
+ * Fixed entry filename for the runtime tree-shake. A stable entry path keeps
663
+ * esbuild's banner comment constant, so the output is deterministic across
664
+ * builds (a per-call unique filename would embed a new path into the bundle
665
+ * every time).
666
+ */
667
+ const RUNTIME_ENTRY = '.runtime-tree-entry.mjs';
398
668
  /**
399
669
  * Builds a single self-contained runtime module for the given used names.
400
670
  *
@@ -412,8 +682,12 @@ export async function buildTreeShakenRuntime(runtimeDir, usedNames) {
412
682
  console.error(`vesk: runtime names not exported — ${missing.join(', ')}; falling back to full runtime`);
413
683
  return buildRuntimeCode(runtimeDir);
414
684
  }
415
- const entry = join(runtimeDir, `.runtime-tree-entry-${runtimeEntryId++}.mjs`);
685
+ const entry = join(runtimeDir, RUNTIME_ENTRY);
416
686
  try {
687
+ try {
688
+ rmSync(entry);
689
+ }
690
+ catch { /* not present yet */ }
417
691
  writeFileSync(entry, `export { ${unique.join(', ')} } from './index-client.js';\n`);
418
692
  const result = await build({
419
693
  entryPoints: [entry],
@@ -423,7 +697,14 @@ export async function buildTreeShakenRuntime(runtimeDir, usedNames) {
423
697
  platform: 'browser',
424
698
  target: ['es2022'],
425
699
  treeShaking: true,
426
- minify: true,
700
+ // Do not let esbuild rename identifiers here. This bundle is one large
701
+ // IIFE built from many runtime modules, and the renaming pass of some
702
+ // esbuild versions (observed on 0.25.12) emits a free reference to an
703
+ // undefined minified name (ReferenceError: m is not defined) that kills
704
+ // client hydration for the whole app. Whitespace-only output keeps the
705
+ // runtime deterministic at a modest size cost; tree-shaking still prunes
706
+ // unreachable modules, so names coming out of the IIFE are untouched.
707
+ minify: false,
427
708
  write: false,
428
709
  logLevel: 'silent',
429
710
  });
@@ -0,0 +1,78 @@
1
+ import type { DevHmrState } from './dev-server';
2
+ import type { PluginRouterChangeEvent } from './dev-server';
3
+ export interface DiagnosticFinding {
4
+ severity: 'error' | 'warning' | 'info';
5
+ code: string;
6
+ file?: string | null;
7
+ line?: number | null;
8
+ column?: number | null;
9
+ message: string;
10
+ hint?: string | null;
11
+ }
12
+ export type CapabilityName = 'config.read' | 'config.write' | 'plugins' | 'diagnostics' | 'build' | 'file.read' | 'command';
13
+ export type DevApiCapabilities = Record<CapabilityName, boolean>;
14
+ /** Default DevTools capability set — `command` is gated off by default. */
15
+ export declare const DEFAULT_CAPABILITIES: DevApiCapabilities;
16
+ /** Gated-command allowlist (read-only/status commands only by default). */
17
+ export declare const DEFAULT_COMMAND_ALLOWLIST: RegExp[];
18
+ /** Server-enforced capability/permission table. */
19
+ export declare class CapabilityTable {
20
+ private caps;
21
+ private commandAllowlist;
22
+ constructor(caps?: Partial<DevApiCapabilities>, commandAllowlist?: RegExp[]);
23
+ allows(cap: CapabilityName): boolean;
24
+ commandAllowed(argv: string[]): boolean;
25
+ }
26
+ export interface DevPanelResponse {
27
+ status: number;
28
+ headers: Record<string, string>;
29
+ body: string;
30
+ encoding?: 'utf8' | 'base64';
31
+ /** When set, `body`/`encoding` are ignored and this async iterable of
32
+ (already-framed) strings is streamed to the client instead — used for
33
+ SSE agent progress. */
34
+ stream?: AsyncIterable<string>;
35
+ }
36
+ export interface RebuildResult {
37
+ ok: boolean;
38
+ error?: string;
39
+ ms?: number;
40
+ }
41
+ export interface CommandResult {
42
+ ok: boolean;
43
+ code: number;
44
+ stdout: string;
45
+ stderr: string;
46
+ }
47
+ export interface DevApiRouterOptions {
48
+ /** Directory containing app/ (parent of projectDir when config is at project root). */
49
+ appDir: string;
50
+ /** The `.vesk` state dir. */
51
+ veskDir: string;
52
+ /** Plugin names declared in config. */
53
+ configPluginNames: string[];
54
+ getHmrState?: () => DevHmrState;
55
+ onPluginChange?: (event: PluginRouterChangeEvent) => void | Promise<void>;
56
+ /** Capability overrides; `command` is off unless enabled + allowlisted. */
57
+ caps?: Partial<DevApiCapabilities>;
58
+ commandAllowlist?: RegExp[];
59
+ /** Dir where vesk.config.{ts,js} lives (defaults to dirname(appDir)). */
60
+ projectDir?: string;
61
+ /** Trigger a full rebuild + HMR reload (POST /__vesk/build). */
62
+ rebuild?: () => Promise<RebuildResult>;
63
+ /** Live diagnostics snapshot (GET /__vesk/diagnostics). */
64
+ getDiagnostics?: () => DiagnosticFinding[];
65
+ /** Gated command executor (POST /__vesk/command). */
66
+ runCommand?: (argv: string[]) => Promise<CommandResult>;
67
+ /** Response body cap. */
68
+ maxBodyBytes?: number;
69
+ }
70
+ export interface DevApiRouter {
71
+ route: (method: string, pathname: string, body?: unknown, search?: string) => Promise<DevPanelResponse | null>;
72
+ }
73
+ /**
74
+ * Build the unified DevTools API router. Exportable and dependency-injectable
75
+ * so the adapter + CLI dev servers can both route through it.
76
+ */
77
+ export declare function createDevApiRouter(opts: DevApiRouterOptions): DevApiRouter;
78
+ //# sourceMappingURL=dev-api.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-api.d.ts","sourceRoot":"","sources":["../src/dev-api.ts"],"names":[],"mappings":"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"}