@vesk/compiler 0.1.14 → 0.2.9

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 (50) hide show
  1. package/dist/api-routes.d.ts +6 -1
  2. package/dist/api-routes.d.ts.map +1 -1
  3. package/dist/api-routes.js +52 -7
  4. package/dist/client-codegen.d.ts +15 -0
  5. package/dist/client-codegen.d.ts.map +1 -1
  6. package/dist/client-codegen.js +84 -11
  7. package/dist/config.d.ts +2 -0
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/config.js +25 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +33 -4
  12. package/dist/ir-generator.d.ts.map +1 -1
  13. package/dist/ir-generator.js +208 -9
  14. package/dist/md-inline.d.ts +17 -0
  15. package/dist/md-inline.d.ts.map +1 -0
  16. package/dist/md-inline.js +149 -0
  17. package/dist/middleware.d.ts.map +1 -1
  18. package/dist/middleware.js +42 -7
  19. package/dist/module-imports.d.ts +51 -0
  20. package/dist/module-imports.d.ts.map +1 -0
  21. package/dist/module-imports.js +715 -0
  22. package/dist/router.d.ts +2 -0
  23. package/dist/router.d.ts.map +1 -1
  24. package/dist/router.js +48 -3
  25. package/dist/server-codegen.d.ts +1 -1
  26. package/dist/server-codegen.d.ts.map +1 -1
  27. package/dist/server-codegen.js +1 -1
  28. package/dist/server-jsgen.d.ts.map +1 -1
  29. package/dist/server-jsgen.js +9 -4
  30. package/dist/server-render.d.ts.map +1 -1
  31. package/dist/server-render.js +30 -10
  32. package/dist/server-utils.d.ts +31 -0
  33. package/dist/server-utils.d.ts.map +1 -1
  34. package/dist/server-utils.js +478 -52
  35. package/dist/tokens.d.ts.map +1 -1
  36. package/dist/tokens.js +24 -1
  37. package/dist/typecheck.d.ts +2 -2
  38. package/dist/typecheck.d.ts.map +1 -1
  39. package/dist/typecheck.js +71 -27
  40. package/dist/types.d.ts +4 -129
  41. package/dist/types.d.ts.map +1 -1
  42. package/dist/types.js +1 -0
  43. package/dist/vesk-plugin.d.ts +3 -0
  44. package/dist/vesk-plugin.d.ts.map +1 -1
  45. package/dist/vesk-plugin.js +518 -1
  46. package/dist/vsk-imports.d.ts.map +1 -1
  47. package/dist/vsk-imports.js +3 -1
  48. package/dist/vsk-tsx.d.ts.map +1 -1
  49. package/dist/vsk-tsx.js +27 -5
  50. package/package.json +2 -1
@@ -22,9 +22,14 @@ export type BuildWebRequestInput = {
22
22
  * `json()`/`text()` are patched accordingly. A `cookies` getter is attached
23
23
  * (NextRequest-compatible).
24
24
  */
25
- export declare function buildWebRequest(nodeReq: BuildWebRequestInput, url: string, body?: BodyInit | null): Request & {
25
+ export declare function buildWebRequest(nodeReq: BuildWebRequestInput, url: string, body?: BodyInit | null, options?: {
26
+ maxBodyBytes?: number;
27
+ }): Request & {
26
28
  cookies: Record<string, string>;
27
29
  query: Record<string, string>;
28
30
  };
31
+ export declare function tooLargeError(max: number): Error & {
32
+ status: number;
33
+ };
29
34
  export declare function executeApiRoute(filePath: string, method: string, request: Request, params?: Record<string, string>, locals?: Record<string, unknown>, devCache?: DevCache): Promise<Response | null>;
30
35
  //# sourceMappingURL=api-routes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-routes.d.ts","sourceRoot":"","sources":["../src/api-routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAS7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAG5D;AA+ED,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CAoDjI;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhE;AAED,MAAM,WAAW,QAAS,SAAQ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAAG;AAExD,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACjE,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAAC,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;CAC/E,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,GACrB,OAAO,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CAqC9E;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACnC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAkI1B"}
1
+ {"version":3,"file":"api-routes.d.ts","sourceRoot":"","sources":["../src/api-routes.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAW7D,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,YAAY,EAAE,CAG5D;AA+ED,wBAAgB,WAAW,CAAC,IAAI,EAAE,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,YAAY,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,GAAG,IAAI,CA0DjI;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWhE;AAED,MAAM,WAAW,QAAS,SAAQ,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC;CAAG;AAExD,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAAC,CAAC;IACjE,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACpC,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,qBAAqB,CAAC,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC,CAAC;CAC/E,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,oBAAoB,EAC7B,GAAG,EAAE,MAAM,EACX,IAAI,CAAC,EAAE,QAAQ,GAAG,IAAI,EACtB,OAAO,CAAC,EAAE;IAAE,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,GAClC,OAAO,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE,CA4C9E;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAIrE;AAED,wBAAsB,eAAe,CACnC,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,OAAO,EAChB,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,EACnC,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA0J1B"}
@@ -1,5 +1,7 @@
1
1
  import { readdirSync, statSync, existsSync } from 'fs';
2
2
  import { join } from 'path';
3
+ import { collapseSlashes } from '@vesk/compiler/src/router';
4
+ import { assertSameOrigin, DEFAULT_MAX_BODY_BYTES } from '@vesk/compiler/src/server-utils';
3
5
  function basename(p) {
4
6
  const idx = p.lastIndexOf('/');
5
7
  return idx === -1 ? p : p.slice(idx + 1);
@@ -69,7 +71,7 @@ function scanApiDir(rootDir, dir, parentPath) {
69
71
  : (parentPath || '/');
70
72
  const node = {
71
73
  path: seg,
72
- fullPath: fullPath.replace(/\/+/g, '/') || '/',
74
+ fullPath: collapseSlashes(fullPath) || '/',
73
75
  isDynamic,
74
76
  isCatchAll,
75
77
  filePath: hasRoute && routeFileName ? join(dir, routeFileName) : null,
@@ -95,7 +97,14 @@ function scanApiDir(rootDir, dir, parentPath) {
95
97
  return nodes;
96
98
  }
97
99
  export function matchApiUrl(tree, pathname) {
98
- const normalized = (pathname.split('?')[0].split('#')[0]).replace(/^\/api(\/|$)/, '/');
100
+ let normalized = pathname.split('?')[0].split('#')[0];
101
+ // strip a leading /api segment (exact or followed by /) without regex
102
+ if (normalized === '/api') {
103
+ normalized = '/';
104
+ }
105
+ else if (normalized.startsWith('/api/') && !normalized.startsWith('//')) {
106
+ normalized = normalized.slice(4);
107
+ }
99
108
  const parts = normalized.split('/').filter(Boolean);
100
109
  const params = {};
101
110
  function matchNodes(nodes, partIndex) {
@@ -169,7 +178,7 @@ export function parseCookies(str) {
169
178
  * `json()`/`text()` are patched accordingly. A `cookies` getter is attached
170
179
  * (NextRequest-compatible).
171
180
  */
172
- export function buildWebRequest(nodeReq, url, body) {
181
+ export function buildWebRequest(nodeReq, url, body, options) {
173
182
  const parsedUrl = new URL(url, `http://${nodeReq.headers?.host || 'localhost'}`);
174
183
  const method = nodeReq.method || 'GET';
175
184
  const headers = {};
@@ -183,8 +192,15 @@ export function buildWebRequest(nodeReq, url, body) {
183
192
  if (_bodyBuffer)
184
193
  return _bodyBuffer;
185
194
  const chunks = [];
186
- for await (const chunk of nodeReq)
187
- chunks.push(chunk);
195
+ let total = 0;
196
+ const max = options?.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
197
+ for await (const chunk of nodeReq) {
198
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
199
+ total += bytes.byteLength;
200
+ if (total > max)
201
+ throw tooLargeError(max);
202
+ chunks.push(bytes);
203
+ }
188
204
  _bodyBuffer = Buffer.concat(chunks);
189
205
  return _bodyBuffer;
190
206
  };
@@ -210,6 +226,11 @@ export function buildWebRequest(nodeReq, url, body) {
210
226
  Object.defineProperty(webRequest, 'query', { get: () => query, enumerable: true });
211
227
  return webRequest;
212
228
  }
229
+ export function tooLargeError(max) {
230
+ const err = new Error(`Request body exceeds limit (${max} bytes)`);
231
+ err.status = 413;
232
+ return err;
233
+ }
213
234
  export async function executeApiRoute(filePath, method, request, params = {}, locals = {}, devCache) {
214
235
  let mod;
215
236
  try {
@@ -224,11 +245,33 @@ export async function executeApiRoute(filePath, method, request, params = {}, lo
224
245
  }
225
246
  }
226
247
  catch (e) {
227
- return new Response(JSON.stringify({ error: 'Failed to load route module', details: e.message }), {
248
+ // details only in dev (devCache present); prod gets a generic message
249
+ const body = devCache
250
+ ? { error: 'Failed to load route module', details: e.message }
251
+ : { error: 'Failed to load route module' };
252
+ return new Response(JSON.stringify(body), {
228
253
  status: 500, headers: { 'Content-Type': 'application/json' },
229
254
  });
230
255
  }
231
256
  const routeConfig = (mod.config || {});
257
+ // Default CSRF defense for mutating API calls: same-origin check unless the
258
+ // route opts out via `config.csrf = false`.
259
+ {
260
+ const methodUpper = method.toUpperCase();
261
+ if (routeConfig.csrf !== false && methodUpper !== 'GET' && methodUpper !== 'HEAD' && methodUpper !== 'OPTIONS') {
262
+ try {
263
+ assertSameOrigin({
264
+ method: methodUpper,
265
+ headers: Object.fromEntries([...request.headers.entries()].map(([k, v]) => [k, Array.isArray(v) ? v.join(', ') : v])),
266
+ });
267
+ }
268
+ catch {
269
+ return new Response(JSON.stringify({ error: 'Cross-origin request blocked' }), {
270
+ status: 403, headers: { 'Content-Type': 'application/json' },
271
+ });
272
+ }
273
+ }
274
+ }
232
275
  const handler = mod[method];
233
276
  if (!handler) {
234
277
  if (method === 'OPTIONS') {
@@ -326,7 +369,9 @@ export async function executeApiRoute(filePath, method, request, params = {}, lo
326
369
  status: 404, headers: { 'Content-Type': 'application/json' },
327
370
  });
328
371
  }
329
- return new Response(JSON.stringify({ error: err.message }), {
372
+ // never leak internal error messages in production responses
373
+ const body = devCache ? { error: err.message } : { error: 'Internal Server Error' };
374
+ return new Response(JSON.stringify(body), {
330
375
  status: 500, headers: { 'Content-Type': 'application/json' },
331
376
  });
332
377
  }
@@ -14,6 +14,21 @@ export declare function compileClient(source: string, _componentName: string | n
14
14
  forceClient?: boolean;
15
15
  hydrate?: boolean;
16
16
  includeTopLevel?: boolean;
17
+ sourcePath?: string;
18
+ mdRoots?: string[];
17
19
  }): string;
20
+ /**
21
+ * Compiles a component source in BOTH client modes (plain + hydrate)
22
+ * sharing a single parse/IR pass. Used by the dev-server hot path, where
23
+ * every edit otherwise pays the full acorn+TS parse twice. Also returns
24
+ * the resolved component name using the exact selection order of
25
+ * `resolveComponentName`, so callers don't need a second full parse just
26
+ * to learn the name.
27
+ */
28
+ export declare function compileClientBoth(source: string, _componentName: string | null): {
29
+ comp: string;
30
+ hyd: string;
31
+ name: string | null;
32
+ };
18
33
  export { compileClient as compile, isStaticIR };
19
34
  //# sourceMappingURL=client-codegen.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EAOjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAsCxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAwGlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAqjCD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AA2HD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CA0D1K;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
1
+ {"version":3,"file":"client-codegen.d.ts","sourceRoot":"","sources":["../src/client-codegen.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,MAAM,EAAuB,MAAM,uBAAuB,CAAC;AACzE,OAAO,EAGL,cAAc,EAQd,gBAAgB,EAOjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAwCxD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,SAAS,GAAG,MAAM,CAAC;CAC1B;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAI1D;AAoBD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAwGlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAqjCD,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AA2HD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAoED,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,OAAO,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,eAAe,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAA;CAAO,GAAG,MAAM,CAOnN;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAqBnI;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -1,12 +1,15 @@
1
1
  import { walk } from 'zimmerframe';
2
2
  import { print } from 'esrap';
3
3
  import ts from 'esrap/languages/ts';
4
+ import tsx from 'esrap/languages/tsx';
4
5
  import { StaticNode, TextNode, DynamicBinding, OpaqueDynamicRegion, MapRegion, WhileLoop, SwitchBlock, TryCatch, ForLoop, TrackDecl, RuntimeStatement, ComponentRef, ComponentCall, ServerBlock, ClientBlock, HeadBlock, SlotNode, } from '@vesk/compiler/src/ir';
5
6
  import { parse } from '@vesk/compiler/src/parser';
6
7
  import { generateIR } from '@vesk/compiler/src/ir-generator';
7
8
  import { transformTopLevelForActions } from '@vesk/compiler/src/actions';
8
9
  import { extractRuntimeNames } from '@vesk/compiler/src/server-utils';
10
+ import { importBindingPairs } from '@vesk/compiler/src/module-imports';
9
11
  import { stripTrackGeneric } from '@vesk/compiler/src/scan';
12
+ import { inlineMdImportsFrom } from '@vesk/compiler/src/md-inline';
10
13
  import { stripTsTypes, hasTsSyntax } from '@vesk/compiler/src/strip-ts';
11
14
  function memberExpr(object, property) {
12
15
  return {
@@ -44,6 +47,28 @@ export function semicolonizeStatement(code) {
44
47
  return code;
45
48
  return code + ';';
46
49
  }
50
+ function containsJsx(node, depth = 0) {
51
+ if (!node || typeof node !== 'object' || depth > 40)
52
+ return false;
53
+ if (Array.isArray(node)) {
54
+ for (const item of node) {
55
+ if (containsJsx(item, depth + 1))
56
+ return true;
57
+ }
58
+ return false;
59
+ }
60
+ if (typeof node.type === 'string' && (node.type === 'JSXElement' || node.type === 'JSXFragment'))
61
+ return true;
62
+ for (const key of Object.keys(node)) {
63
+ const v = node[key];
64
+ if (v && typeof v === 'object' && containsJsx(v, depth + 1))
65
+ return true;
66
+ }
67
+ return false;
68
+ }
69
+ function printAst(ast) {
70
+ return print(ast, containsJsx(ast) ? tsx() : ts()).code;
71
+ }
47
72
  export function transformTracked(irNode, tracked) {
48
73
  const ast = irNode.ast;
49
74
  if (!ast)
@@ -51,7 +76,7 @@ export function transformTracked(irNode, tracked) {
51
76
  if (tracked.size === 0) {
52
77
  if (!hasTsSyntax(ast))
53
78
  return irNode.raw;
54
- return print(stripTsTypes(ast), ts()).code;
79
+ return printAst(stripTsTypes(ast));
55
80
  }
56
81
  const stripped = stripTsTypes(ast);
57
82
  const transformed = walk(stripped, tracked, {
@@ -139,7 +164,7 @@ export function transformTracked(irNode, tracked) {
139
164
  return context.next();
140
165
  },
141
166
  });
142
- return print(transformed, ts()).code;
167
+ return print(transformed, containsJsx(transformed) ? tsx() : ts()).code;
143
168
  }
144
169
  export function collectTrackedNames(body) {
145
170
  const names = new Map();
@@ -1458,15 +1483,13 @@ function hasKeyedMap(nodes) {
1458
1483
  }
1459
1484
  export function escapeHtml(str) {
1460
1485
  return str
1461
- .replace(/&/g, '&amp;')
1462
- .replace(/</g, '&lt;')
1463
- .replace(/>/g, '&gt;')
1464
- .replace(/"/g, '&quot;')
1465
- .replace(/'/g, '&#39;');
1486
+ .split('&').join('&amp;')
1487
+ .split('<').join('&lt;')
1488
+ .split('>').join('&gt;')
1489
+ .split('"').join('&quot;')
1490
+ .split("'").join('&#39;');
1466
1491
  }
1467
- export function compileClient(source, _componentName, options = {}) {
1468
- const ast = parse(source);
1469
- const ir = generateIR(ast, source);
1492
+ function emitClientFromIR(ir, options) {
1470
1493
  const needsClient = ir.components.some((c) => c.isClient || !isStaticComponent(c));
1471
1494
  if (!options.forceClient && !needsClient) {
1472
1495
  return '';
@@ -1510,7 +1533,16 @@ export function compileClient(source, _componentName, options = {}) {
1510
1533
  runtimeNames.push(name);
1511
1534
  }
1512
1535
  }
1513
- const runtimeImport = `import { ${runtimeNames.join(', ')} } from '@vesk/runtime';`;
1536
+ // A name the file already binds via its own imports (e.g. a local module that
1537
+ // exports its own `effect`) shadows the runtime binding — never inject a
1538
+ // duplicate `import { x } from '@vesk/runtime'`. Mirrors SSR, so the two agree.
1539
+ const boundLocally = new Set();
1540
+ for (const imp of ir.imports) {
1541
+ for (const pair of importBindingPairs(imp))
1542
+ boundLocally.add(pair.local);
1543
+ }
1544
+ const shadowedRuntimeNames = runtimeNames.filter(n => !boundLocally.has(n));
1545
+ const runtimeImport = `import { ${shadowedRuntimeNames.length > 0 ? shadowedRuntimeNames.join(', ') : 'destroy_block'} } from '@vesk/runtime';`;
1514
1546
  const moduleCode = `
1515
1547
  ${runtimeImport}
1516
1548
  ${importLines}
@@ -1520,4 +1552,45 @@ ${exportCode}
1520
1552
  `;
1521
1553
  return moduleCode.trim();
1522
1554
  }
1555
+ export function compileClient(source, _componentName, options = {}) {
1556
+ if (options.sourcePath) {
1557
+ source = inlineMdImportsFrom(source, options.sourcePath, options.mdRoots || []);
1558
+ }
1559
+ const ast = parse(source);
1560
+ const ir = generateIR(ast, source);
1561
+ return emitClientFromIR(ir, options);
1562
+ }
1563
+ /**
1564
+ * Compiles a component source in BOTH client modes (plain + hydrate)
1565
+ * sharing a single parse/IR pass. Used by the dev-server hot path, where
1566
+ * every edit otherwise pays the full acorn+TS parse twice. Also returns
1567
+ * the resolved component name using the exact selection order of
1568
+ * `resolveComponentName`, so callers don't need a second full parse just
1569
+ * to learn the name.
1570
+ */
1571
+ export function compileClientBoth(source, _componentName) {
1572
+ const ast = parse(source);
1573
+ // Downstream type-stripping mutates AST nodes in place (stripTsTypes),
1574
+ // so each emit mode needs its own tree. Cloning is far cheaper than the
1575
+ // second full acorn+TS parse this replaces.
1576
+ const hydAst = structuredClone(ast);
1577
+ const ir = generateIR(ast, source);
1578
+ const irHyd = generateIR(hydAst, source);
1579
+ const defaultComp = ir.components.find((c) => c.defaultExport);
1580
+ let name = null;
1581
+ if (defaultComp)
1582
+ name = defaultComp.name;
1583
+ else if (ir.components.length > 0)
1584
+ name = ir.components[0].name;
1585
+ else {
1586
+ const exportedComp = ir.components.find((c) => c.exported);
1587
+ if (exportedComp)
1588
+ name = exportedComp.name;
1589
+ }
1590
+ return {
1591
+ comp: emitClientFromIR(ir, { forceClient: true }),
1592
+ hyd: emitClientFromIR(irHyd, { forceClient: true, hydrate: true, includeTopLevel: false }),
1593
+ name,
1594
+ };
1595
+ }
1523
1596
  export { compileClient as compile, isStaticIR };
package/dist/config.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { VeskConfig, VeskPlugin, VeskSecurity } from '@vesk/compiler/src/types';
2
+ /** Tags allowed by default when md.html = 'allowlist' and no allowTags given. */
3
+ export declare const MD_DEFAULT_ALLOW_TAGS: string[];
2
4
  export declare function preset(name: string, overrides?: VeskSecurity): VeskSecurity;
3
5
  export declare function definePlugin<P extends VeskPlugin>(plugin: P): P;
4
6
  export declare function defineConfig(config: VeskConfig): VeskConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAsB,MAAM,0BAA0B,CAAC;AA0BzG,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,YAAiB,GAAG,YAAY,CAY/E;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAQ/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAqB3D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAkC7D"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,YAAY,EAAgC,MAAM,0BAA0B,CAAC;AAGnH,iFAAiF;AACjF,eAAO,MAAM,qBAAqB,UAIjC,CAAC;AA6CF,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,YAAiB,GAAG,YAAY,CAY/E;AAED,wBAAgB,YAAY,CAAC,CAAC,SAAS,UAAU,EAAE,MAAM,EAAE,CAAC,GAAG,CAAC,CAQ/D;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAuB3D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,UAAU,GAAG,UAAU,CAkC7D"}
package/dist/config.js CHANGED
@@ -1,4 +1,26 @@
1
1
  import { VeskError } from '@vesk/compiler/src/errors';
2
+ /** Tags allowed by default when md.html = 'allowlist' and no allowTags given. */
3
+ export const MD_DEFAULT_ALLOW_TAGS = [
4
+ 'a', 'abbr', 'b', 'bdi', 'bdo', 'br', 'cite', 'code', 'data', 'del', 'dfn', 'em',
5
+ 'i', 'ins', 'kbd', 'mark', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'small', 'span',
6
+ 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
7
+ ];
8
+ const MD_HTML_MODES = ['escape', 'allow', 'allowlist'];
9
+ function normalizeMdConfig(md) {
10
+ if (!md || typeof md !== 'object')
11
+ return md;
12
+ if (md.html !== undefined && !MD_HTML_MODES.includes(md.html)) {
13
+ throw VeskError.configError(`Unknown md.html mode: "${md.html}".`, MD_HTML_MODES);
14
+ }
15
+ const out = { ...md };
16
+ if (out.allowTags) {
17
+ if (!Array.isArray(out.allowTags)) {
18
+ throw VeskError.configError('md.allowTags must be an array of tag names.', []);
19
+ }
20
+ out.allowTags = out.allowTags.map((t) => String(t).toLowerCase().replace(/[^a-z0-9-]/g, '')).filter(Boolean);
21
+ }
22
+ return out;
23
+ }
2
24
  const SECURITY_PRESETS = {
3
25
  strict: {
4
26
  autoEscape: true,
@@ -71,6 +93,9 @@ export function defineConfig(config) {
71
93
  sec.redactLogs = true;
72
94
  if (config.routeDataCache === undefined)
73
95
  config.routeDataCache = 0;
96
+ const md = normalizeMdConfig(config.md);
97
+ if (md !== undefined)
98
+ config.md = md;
74
99
  return config;
75
100
  }
76
101
  export function validateConfig(config) {
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAWD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAeD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA6BnG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAWvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkB/G,QAAQ,IAAI,MAAM;IAgBlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAoBxD;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,WAAW,GAAE,MAAU,GAAG,MAAM,GAAG,IAAI,CAWrG;AAqCD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAgBD,qBAAa,SAAU,SAAQ,KAAK;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;gBAEF,OAAO,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB;IAYxD,MAAM,CAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IA6BnG,MAAM,CAAC,SAAS,CAAC,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAmB3D,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkBvF,MAAM,CAAC,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAO3G,MAAM,CAAC,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,YAAY,GAAE,MAAM,EAAO,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAWvG,MAAM,CAAC,sBAAsB,CAAC,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,SAAS;IAkB/G,QAAQ,IAAI,MAAM;IAgBlB,MAAM,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;CAclC"}
package/dist/errors.js CHANGED
@@ -31,19 +31,48 @@ export function didYouMean(name, candidates, maxDistance = 3) {
31
31
  return best;
32
32
  }
33
33
  function extractLineColumn(message) {
34
- const lineMatch = message.match(/(?:line|at\s+line)\s*(\d+)/i);
35
- const colMatch = message.match(/(?:column|col)\s*(\d+)/i);
36
34
  return {
37
- line: lineMatch ? parseInt(lineMatch[1]) : 0,
38
- column: colMatch ? parseInt(colMatch[1]) : 0,
35
+ line: findKeywordNumber(message, ['at line', 'line']),
36
+ column: findKeywordNumber(message, ['column', 'col']),
39
37
  };
40
38
  }
39
+ /**
40
+ * Finds `<keyword><whitespace>*<digits>` in `message`, trying longer keyword
41
+ * variants first so 'column' is not matched as 'col' + 'umn'. Char-scan only.
42
+ */
43
+ function findKeywordNumber(message, keywords) {
44
+ const lower = message.toLowerCase();
45
+ for (const kw of keywords) {
46
+ let from = 0;
47
+ while (from <= lower.length - kw.length) {
48
+ const idx = lower.indexOf(kw, from);
49
+ if (idx === -1)
50
+ break;
51
+ // word boundary before the keyword
52
+ const before = idx > 0 ? lower[idx - 1] : ' ';
53
+ if (!(before >= 'a' && before <= 'z')) {
54
+ let j = idx + kw.length;
55
+ while (j < message.length && (message[j] === ' ' || message[j] === '\t'))
56
+ j++;
57
+ if (message[j] >= '0' && message[j] <= '9') {
58
+ let end = j;
59
+ while (end < message.length && message[end] >= '0' && message[end] <= '9')
60
+ end++;
61
+ return parseInt(message.slice(j, end));
62
+ }
63
+ }
64
+ from = idx + 1;
65
+ }
66
+ }
67
+ return 0;
68
+ }
41
69
  const VESK_BUILTINS = [
42
70
  'useFetch', 'useRouter', 'useParams', 'usePathname', 'useSearchParams',
43
71
  'useNavigate', 'useHead', 'useTitle',
44
72
  'Form', 'Field', 'Link', 'NavLink', 'Outlet',
45
73
  'Image', 'Portal',
46
74
  'Experiment',
75
+ 'LoadingIndicator', 'useLoadingIndicator',
47
76
  'required', 'email', 'minLength', 'maxLength', 'pattern', 'custom',
48
77
  'track', 'get', 'set', 'derived', 'effect', 'batch', 'untrack',
49
78
  'cookies', 'headers', 'locals',
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAoBP,MAAM,uBAAuB,CAAC;AA+I/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAylBD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAwN3D"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAoBP,MAAM,uBAAuB,CAAC;AAgJ/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAuxBD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAiO3D"}