@vesk/compiler 0.2.58 → 0.2.60

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.
@@ -1 +1 @@
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,EASjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkGxD,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;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAgJlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAuB9F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAoBD,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,SAAS,CAQtE;AAmgED,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AAmJD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAuED,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,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAOpQ;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAyC5J;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/E;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,EASjB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,uBAAuB,CAAC;AAkGxD,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;AAiDD,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG,gBAAgB,GAAG,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAgJlI;AAID,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,CAU5E;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,EAAE,CAuB9F;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,MAAM,CAY5F;AAoBD,qDAAqD;AACrD,MAAM,MAAM,SAAS,GAAG;IAAE,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAEtC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,UAAU,CAAC,EAAE,MAAM,EAAE,KAAK,SAAI,GAAG,SAAS,CAQtE;AA0iED,iBAAS,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAe3C;AAmJD,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAO9C;AAuED,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,CAAC;IAAC,aAAa,CAAC,EAAE,SAAS,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,MAAM,CAOpQ;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,MAAM,EACd,cAAc,EAAE,MAAM,GAAG,IAAI,EAC7B,UAAU,CAAC,EAAE,MAAM,EACnB,IAAI,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,GACjD;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,OAAO,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,aAAa,EAAE,MAAM,EAAE,CAAC;IAAC,cAAc,EAAE,MAAM,EAAE,CAAA;CAAE,CAyC5J;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAG/E;AAED,OAAO,EAAE,aAAa,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC"}
@@ -528,8 +528,16 @@ function emitNode(ctx, node, tracked, effectsVar, parentVar, compPrefix = '__com
528
528
  // The children ARE this layout's output, so they take the component's
529
529
  // return value rather than being appended to a parent that does not exist.
530
530
  if (ctx.hydrate) {
531
+ const promptExpr = ctx.markerless && (regionBudget || skipK > 0)
532
+ ? markerlessSkipExpr(regionBudget, skipK)
533
+ : null;
531
534
  ctx.push(`if (props.children !== undefined && props.children !== null) {`);
532
535
  ctx.push(` if (typeof props.children === 'function') {`);
536
+ if (promptExpr) {
537
+ ctx.push(` ${ctx.walker}.injectSkipK(${promptExpr});`);
538
+ if (regionBudget && ctx.markerless)
539
+ ctx.push(` ${regionBudget} = 0;`);
540
+ }
533
541
  ctx.push(` __pendingChild = props.children(__hydrate);`);
534
542
  ctx.push(` } else if (__hydrate && __hydrate.root) {`);
535
543
  ctx.push(` __hydrate.root.appendChild(toDomNode(props.children));`);
@@ -989,12 +997,34 @@ function emitClientHead(ctx, node, tracked) {
989
997
  }
990
998
  }
991
999
  }
1000
+ /**
1001
+ * Whether an expression can change after first render.
1002
+ *
1003
+ * Two ways it can:
1004
+ * 1. it reads `props` or a locally declared `track()` binding — visible
1005
+ * statically; or
1006
+ * 2. it CALLS something. A call is the only way to hide a cell read behind a
1007
+ * function boundary, and hiding one is not exotic — it is the documented
1008
+ * store pattern: a module-scope `track()` cell published as an accessor
1009
+ * (`notificationDrawerCell.get()`, `isAuthenticated()`,
1010
+ * `unreadNotificationCount()`). The compiler cannot see inside those calls,
1011
+ * so it must assume they subscribe; the runtime's `get()` records the
1012
+ * dependency when the expression is evaluated inside the emitted effect,
1013
+ * which is exactly where these conditions are re-evaluated.
1014
+ *
1015
+ * Treating a call as reactive costs one never-firing effect for a genuinely
1016
+ * static condition. The opposite mistake — treating a store-backed condition as
1017
+ * static — is a component that renders once and never updates again.
1018
+ */
992
1019
  function isReactiveExpression(node, tracked) {
993
1020
  const ast = node && (node.ast || node);
994
1021
  if (!ast)
995
1022
  return false;
996
1023
  let reactive = false;
997
1024
  walk(ast, tracked, {
1025
+ CallExpression() {
1026
+ reactive = true;
1027
+ },
998
1028
  Identifier(n, context) {
999
1029
  if (reactive)
1000
1030
  return;
@@ -1236,10 +1266,20 @@ function emitComponentCall(ctx, node, tracked, effectsVar, parentVar, compPrefix
1236
1266
  ctx.push(`return $f; })();`);
1237
1267
  propsEntries.push(`children: ${frag}`);
1238
1268
  }
1239
- if (ctx.markerless && promptExpr && !plainTarget) {
1240
- // Deposit the residue for the child's first top-level claim (it runs on
1241
- // this same walker with its own zero `topPendingSkip`). A region budget is
1242
- // spent by THIS call, so sibling claims in the branch stay positional.
1269
+ // Deposit the residue for the child's first top-level claim (it runs on
1270
+ // this same walker with its own zero `topPendingSkip`). A region budget is
1271
+ // spent by THIS call, so sibling claims in the branch stay positional.
1272
+ //
1273
+ // The deposit goes down for PLAIN targets too: "imported value" does not mean
1274
+ // "cannot claim". lucide-vesk icons are imported functions that follow the
1275
+ // framework component protocol `(props, registry, walker)` and claim their own
1276
+ // SSR root through it, so without the deposit they claimed the slot holding the
1277
+ // STATIC residue in front of them (`<button><span>Apply Now</span><svg/></button>`)
1278
+ // — the descriptor missed on `<span>` and the walker detached real
1279
+ // server-rendered content. A plain target that does NOT claim simply leaves the
1280
+ // deposit for the post-call adoption claim below, which carries an explicit
1281
+ // offset and therefore consumes/clears it.
1282
+ if (ctx.markerless && promptExpr) {
1243
1283
  ctx.push(`${walkerArg}.injectSkipK(${promptExpr});`);
1244
1284
  }
1245
1285
  ctx.push(`let ${v} = undefined;`);
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/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;AA4lDD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqV9E"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/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;AAgmDD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqV9E"}
@@ -2033,6 +2033,11 @@ function warnStaleConstBindings(comp, file) {
2033
2033
  continue;
2034
2034
  if (reactive.has(d.id.name))
2035
2035
  continue;
2036
+ // A FUNCTION initializer is not a snapshot: the body runs whenever the
2037
+ // binding is evaluated, so the tracked read happens there. Reporting it
2038
+ // pushed authors to inline helpers that were already correct.
2039
+ if (d.init.type === 'ArrowFunctionExpression' || d.init.type === 'FunctionExpression')
2040
+ continue;
2036
2041
  // A bare identifier initializer is an ALIAS, not a read: `const copy =
2037
2042
  // selCell` re-binds the cell and stays live. Only a read snapshots.
2038
2043
  if (d.init.type === 'Identifier' && reactive.has(d.init.name))
package/dist/parser.d.ts CHANGED
@@ -28,8 +28,18 @@ export interface VeskAnnotation {
28
28
  * Scans the source and blanks `; key <expr>` / `; index <ident>` clauses
29
29
  * found in `for (...)` headers, returning the rewritten code (same length
30
30
  * as the input, so every source offset is preserved) plus annotations.
31
+ *
32
+ * `delimiters` — the real string/template delimiters from
33
+ * `collectStringDelimiters` — is optional. Without it the scan assumes every
34
+ * `"`, `'` and `` ` `` opens a literal, which is wrong for JSX: an apostrophe
35
+ * in text (`<th>Joe Christopher's Standing</th>`) or in an attribute value
36
+ * opens a "string" that runs to the next quote anywhere in the file, so every
37
+ * `for` loop in between is invisible and its `; key …` clause reaches acorn
38
+ * un-blanked as `Unexpected token` at the header. Callers that can afford the
39
+ * tokenization pass it in; when it is unavailable the scan falls back to the
40
+ * plain lexical skip so mid-edit source never crashes it.
31
41
  */
32
- export declare function preprocessForClauses(source: string): {
42
+ export declare function preprocessForClauses(source: string, delimiters?: Set<number> | null): {
33
43
  code: string;
34
44
  annotations: VeskAnnotation[];
35
45
  };
@@ -1 +1 @@
1
- {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CA4LpG;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwFlE;AA2DD,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAM/D;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAuEzE"}
1
+ {"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AAOtC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,YAAY,CAAC;IACnB,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AA0BD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,GAC9B;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,cAAc,EAAE,CAAA;CAAE,CAmMjD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAwFlE;AA2DD,wBAAgB,gBAAgB,IAAI,OAAO,KAAK,CAAC,MAAM,CAEtD;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,IAAI,CAM/D;AAED,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CA6EzE"}
package/dist/parser.js CHANGED
@@ -3,7 +3,7 @@ import { tsPlugin } from './acorn-ts-plugin/index.js';
3
3
  import { VeskParserPlugin } from '@vesk/compiler/src/vesk-plugin';
4
4
  import { blankComments, containsForOfIn } from '@vesk/compiler/src/scan';
5
5
  import { VeskError, codeFrame } from '@vesk/compiler/src/errors';
6
- import { findSpecifierExports, hasTopLevelValueDeclaration } from '@vesk/compiler/src/tokens';
6
+ import { findSpecifierExports, hasTopLevelValueDeclaration, collectStringDelimiters, tokenizeCode } from '@vesk/compiler/src/tokens';
7
7
  function isIdentChar(code) {
8
8
  return ((code >= 97 && code <= 122) || (code >= 65 && code <= 90) || (code >= 48 && code <= 57) || code === 95 || code === 36);
9
9
  }
@@ -28,8 +28,18 @@ function offsetToLineCol(source, offset) {
28
28
  * Scans the source and blanks `; key <expr>` / `; index <ident>` clauses
29
29
  * found in `for (...)` headers, returning the rewritten code (same length
30
30
  * as the input, so every source offset is preserved) plus annotations.
31
+ *
32
+ * `delimiters` — the real string/template delimiters from
33
+ * `collectStringDelimiters` — is optional. Without it the scan assumes every
34
+ * `"`, `'` and `` ` `` opens a literal, which is wrong for JSX: an apostrophe
35
+ * in text (`<th>Joe Christopher's Standing</th>`) or in an attribute value
36
+ * opens a "string" that runs to the next quote anywhere in the file, so every
37
+ * `for` loop in between is invisible and its `; key …` clause reaches acorn
38
+ * un-blanked as `Unexpected token` at the header. Callers that can afford the
39
+ * tokenization pass it in; when it is unavailable the scan falls back to the
40
+ * plain lexical skip so mid-edit source never crashes it.
31
41
  */
32
- export function preprocessForClauses(source) {
42
+ export function preprocessForClauses(source, delimiters) {
33
43
  const annotations = [];
34
44
  const chars = source.split('');
35
45
  const isString = (ch) => ch === '"' || ch === "'" || ch === '`';
@@ -38,6 +48,13 @@ export function preprocessForClauses(source) {
38
48
  const ch = source[i];
39
49
  // Skip strings and comments (nested backtick templates keep depth).
40
50
  if (isString(ch)) {
51
+ // A quote the tokenizer says is not a delimiter is literal JSX text (or
52
+ // a quote inside a literal); step over the single character instead of
53
+ // swallowing the rest of the file.
54
+ if (delimiters && !delimiters.has(i)) {
55
+ i++;
56
+ continue;
57
+ }
41
58
  const quote = ch;
42
59
  let j = i + 1;
43
60
  let tplDepth = 0;
@@ -503,15 +520,21 @@ export function parseGeneratedJs(source) {
503
520
  }
504
521
  export function parse(source, options = {}) {
505
522
  const ParserClass = createBaseParser();
506
- const { code, annotations } = preprocessForClauses(blankGenericArrowTypeParams(blankComments(source)));
523
+ // Tokenize once for both delimiter detection and specifier-export filtering
524
+ // to avoid duplicate lexing/parsing overhead.
525
+ const sourceTokens = tokenizeCode(source);
526
+ const delimiters = sourceTokens === null
527
+ ? null
528
+ : collectStringDelimiters(source, sourceTokens);
529
+ const { code, annotations } = preprocessForClauses(blankGenericArrowTypeParams(blankComments(source)), delimiters);
507
530
  // Acorn validates that every `export { X }` names a real top-level binding.
508
531
  // A `.vsk` component is a registry entry, not a binding, so acorn rejects
509
532
  // `export { MyComponent }`. Remove only those specifier lists — one whose
510
533
  // local IS a real declaration keeps native module semantics — and carry the
511
534
  // pairs on the AST; the IR generator turns them into registry aliases.
512
535
  const vskSource = !options.filename || String(options.filename).endsWith('.vsk');
513
- const specifierExports = vskSource
514
- ? findSpecifierExports(code).filter((e) => !hasTopLevelValueDeclaration(code, e.local))
536
+ const specifierExports = vskSource && sourceTokens !== null
537
+ ? findSpecifierExports(code, sourceTokens).filter((e) => !hasTopLevelValueDeclaration(code, e.local, sourceTokens))
515
538
  : [];
516
539
  let parseable = code;
517
540
  if (specifierExports.length > 0) {
package/dist/tokens.d.ts CHANGED
@@ -20,13 +20,36 @@ export interface CodeToken {
20
20
  * their span so callers can read the actual character via `code[start]`.
21
21
  */
22
22
  export declare function tokenizeCode(code: string): CodeToken[] | null;
23
+ /**
24
+ * Returns the set of offsets at which a `"`, `'` or `` ` `` in `code` really
25
+ * opens a quoted literal, or `null` when the code cannot be tokenized.
26
+ *
27
+ * A character-level scanner cannot tell an apostrophe in JSX text
28
+ * (`<th>Joe Christopher's Standing</th>`) or inside an attribute value
29
+ * (`class="Bob's"`) from the opening quote of a JavaScript string, and the
30
+ * mistake is unbounded: the scanner skips to the next quote it can find — or to
31
+ * end of file — and everything in between is invisible to it. That is how the
32
+ * `; key …` clause of a `for` loop could go un-blanked and surface as
33
+ * `Unexpected token` at a header hundreds of lines away from the real cause.
34
+ *
35
+ * The tokenizer has no such ambiguity, so this hands the decision to it. A
36
+ * quote is a delimiter when a quoted-literal token starts there (ordinary JS
37
+ * strings and template backticks) or when no token covers it at all — which is
38
+ * how a JSX attribute value arrives, since acorn's JSX reader consumes the
39
+ * attribute's opening quote before tokenizing its content.
40
+ *
41
+ * Callers must fall back to their own lexical skip when this returns `null`;
42
+ * tokenizing fails on input that is mid-edit or otherwise unparsable, and the
43
+ * character scanners have to keep working there.
44
+ */
45
+ export declare function collectStringDelimiters(code: string, tokens?: CodeToken[] | null): Set<number> | null;
23
46
  /**
24
47
  * Returns the set of identifiers in `code` that are called — an identifier
25
48
  * token immediately followed by `(` or by a `<...>` generic clause and then
26
49
  * `(`. Member accesses (`obj.fn(`, `obj?.fn(`) are excluded so methods are
27
50
  * not mistaken for imported runtime functions.
28
51
  */
29
- export declare function collectCalledIdentifiers(code: string): Set<string>;
52
+ export declare function collectCalledIdentifiers(code: string, tokens?: CodeToken[] | null): Set<string>;
30
53
  /**
31
54
  * True when `code` contains the identifier `name` (as a plain identifier or
32
55
  * a JSX name), outside strings and comments.
@@ -53,7 +76,7 @@ export declare function importModuleTarget(importText: string): string | null;
53
76
  * with no binding — apart from an ordinary JS/TS declaration, whose
54
77
  * `export { name }` must keep native module semantics.
55
78
  */
56
- export declare function hasTopLevelValueDeclaration(code: string, name: string): boolean;
79
+ export declare function hasTopLevelValueDeclaration(code: string, name: string, tokens?: CodeToken[] | null): boolean;
57
80
  /** One entry of a same-file `export { A, B as C }` specifier list. */
58
81
  export interface VskSpecifierExport {
59
82
  local: string;
@@ -72,7 +95,7 @@ export interface VskSpecifierExport {
72
95
  * `export const A = 1`) and re-exports (`export … from '…'`) are left alone:
73
96
  * those parse fine and are handled in the IR generator.
74
97
  */
75
- export declare function findSpecifierExports(code: string): VskSpecifierExport[];
98
+ export declare function findSpecifierExports(code: string, tokens?: CodeToken[] | null): VskSpecifierExport[];
76
99
  /** `text` must be `source.slice(base, …)`; `node` is AST parsed from `source`. */
77
100
  export declare function stripTrackDeclMarkers(text: string, base: number, node: any): string;
78
101
  //# sourceMappingURL=tokens.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,IAAI,CA0B7D;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA2BlE;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAStE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAqBxE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAuC/D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAwID;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAkB/E;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,kBAAkB,EAAE,CAkCvE;AA6DD,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAcnF"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAaH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,IAAI,CA0B7D;AAKD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,IAAI,CAerG;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CA2B/F;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAStE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAqBxE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAuC/D;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAOpE;AAwID;;;;;GAKG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,OAAO,CAkB5G;AAED,sEAAsE;AACtE,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,GAAG,kBAAkB,EAAE,CAkCpG;AA6DD,kFAAkF;AAClF,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAcnF"}
package/dist/tokens.js CHANGED
@@ -44,26 +44,70 @@ export function tokenizeCode(code) {
44
44
  }
45
45
  });
46
46
  }
47
+ /** Token labels whose source text is a quoted literal (string or template). */
48
+ const QUOTED_LABELS = new Set(['string', 'template', '`', 'backQuote']);
49
+ /**
50
+ * Returns the set of offsets at which a `"`, `'` or `` ` `` in `code` really
51
+ * opens a quoted literal, or `null` when the code cannot be tokenized.
52
+ *
53
+ * A character-level scanner cannot tell an apostrophe in JSX text
54
+ * (`<th>Joe Christopher's Standing</th>`) or inside an attribute value
55
+ * (`class="Bob's"`) from the opening quote of a JavaScript string, and the
56
+ * mistake is unbounded: the scanner skips to the next quote it can find — or to
57
+ * end of file — and everything in between is invisible to it. That is how the
58
+ * `; key …` clause of a `for` loop could go un-blanked and surface as
59
+ * `Unexpected token` at a header hundreds of lines away from the real cause.
60
+ *
61
+ * The tokenizer has no such ambiguity, so this hands the decision to it. A
62
+ * quote is a delimiter when a quoted-literal token starts there (ordinary JS
63
+ * strings and template backticks) or when no token covers it at all — which is
64
+ * how a JSX attribute value arrives, since acorn's JSX reader consumes the
65
+ * attribute's opening quote before tokenizing its content.
66
+ *
67
+ * Callers must fall back to their own lexical skip when this returns `null`;
68
+ * tokenizing fails on input that is mid-edit or otherwise unparsable, and the
69
+ * character scanners have to keep working there.
70
+ */
71
+ export function collectStringDelimiters(code, tokens) {
72
+ const t = tokens == null ? tokenizeCode(code) : tokens;
73
+ if (t === null)
74
+ return null;
75
+ const starts = new Set();
76
+ // `t` is ordered by offset, so one pointer answers "is this offset
77
+ // inside a token?" for the whole source without materialising a bitmap.
78
+ let ti = 0;
79
+ for (let i = 0; i < code.length; i++) {
80
+ const c = code.charCodeAt(i);
81
+ if (c !== 34 && c !== 39 && c !== 96)
82
+ continue;
83
+ while (ti < t.length && t[ti].end <= i)
84
+ ti++;
85
+ const covered = ti < t.length && t[ti].start <= i;
86
+ if (!covered || QUOTED_LABELS.has(t[ti].label))
87
+ starts.add(i);
88
+ }
89
+ return starts;
90
+ }
47
91
  /**
48
92
  * Returns the set of identifiers in `code` that are called — an identifier
49
93
  * token immediately followed by `(` or by a `<...>` generic clause and then
50
94
  * `(`. Member accesses (`obj.fn(`, `obj?.fn(`) are excluded so methods are
51
95
  * not mistaken for imported runtime functions.
52
96
  */
53
- export function collectCalledIdentifiers(code) {
54
- const tokens = tokenizeCode(code);
55
- if (tokens !== null) {
97
+ export function collectCalledIdentifiers(code, tokens) {
98
+ const t = tokens == null ? tokenizeCode(code) : tokens;
99
+ if (t !== null) {
56
100
  const result = new Set();
57
- for (let i = 0; i < tokens.length; i++) {
58
- const t = tokens[i];
59
- if (t.label !== 'name')
101
+ for (let i = 0; i < t.length; i++) {
102
+ const tok = t[i];
103
+ if (tok.label !== 'name')
60
104
  continue;
61
- const prev = i > 0 ? tokens[i - 1] : null;
105
+ const prev = i > 0 ? t[i - 1] : null;
62
106
  if (prev && (prev.label === '.' || prev.label === '?.')) {
63
107
  // Member calls (`useFetch.text(`) surface the receiver so an
64
108
  // auto-importable function used as a receiver is still imported.
65
- const obj = i >= 2 ? tokens[i - 2] : null;
66
- const next = tokens[i + 1];
109
+ const obj = i >= 2 ? t[i - 2] : null;
110
+ const next = t[i + 1];
67
111
  if (obj && obj.label === 'name' && next) {
68
112
  const nextCh = code[next.start];
69
113
  if (nextCh === '(' || nextCh === '<')
@@ -71,12 +115,12 @@ export function collectCalledIdentifiers(code) {
71
115
  }
72
116
  continue;
73
117
  }
74
- const next = tokens[i + 1];
118
+ const next = t[i + 1];
75
119
  if (!next)
76
120
  continue;
77
121
  const nextCh = code[next.start];
78
122
  if (nextCh === '(' || nextCh === '<')
79
- result.add(t.value);
123
+ result.add(tok.value);
80
124
  }
81
125
  return result;
82
126
  }
@@ -367,22 +411,22 @@ function manualExtractImportNames(importText) {
367
411
  * with no binding — apart from an ordinary JS/TS declaration, whose
368
412
  * `export { name }` must keep native module semantics.
369
413
  */
370
- export function hasTopLevelValueDeclaration(code, name) {
371
- const tokens = tokenizeCode(code);
372
- if (tokens === null)
414
+ export function hasTopLevelValueDeclaration(code, name, tokens) {
415
+ const t = tokens == null ? tokenizeCode(code) : tokens;
416
+ if (t === null)
373
417
  return false;
374
418
  const declWords = ['const', 'let', 'var', 'function', 'class'];
375
- for (let i = 1; i < tokens.length; i++) {
376
- if (tokens[i].label !== 'name' || tokens[i].value !== name)
419
+ for (let i = 1; i < t.length; i++) {
420
+ if (t[i].label !== 'name' || t[i].value !== name)
377
421
  continue;
378
- const prev = tokens[i - 1];
422
+ const prev = t[i - 1];
379
423
  // Declaration keywords carry their own token label (`const`, `var`,
380
424
  // `function`, `class`); `let` arrives as a plain name. Match on value.
381
425
  if (!prev || !declWords.includes(prev.value))
382
426
  continue;
383
427
  // `export const X` / `export function X` are preceded by `export`.
384
428
  if (i >= 2) {
385
- const pp = tokens[i - 2];
429
+ const pp = t[i - 2];
386
430
  if (pp && pp.label === 'name' && pp.value === 'export')
387
431
  continue;
388
432
  }
@@ -400,24 +444,24 @@ export function hasTopLevelValueDeclaration(code, name) {
400
444
  * `export const A = 1`) and re-exports (`export … from '…'`) are left alone:
401
445
  * those parse fine and are handled in the IR generator.
402
446
  */
403
- export function findSpecifierExports(code) {
404
- const tokens = tokenizeCode(code);
405
- if (tokens === null)
447
+ export function findSpecifierExports(code, tokens) {
448
+ const t = tokens == null ? tokenizeCode(code) : tokens;
449
+ if (t === null)
406
450
  return [];
407
451
  const out = [];
408
- for (let i = 0; i < tokens.length; i++) {
409
- if (tokens[i].label !== 'export')
452
+ for (let i = 0; i < t.length; i++) {
453
+ if (t[i].label !== 'export')
410
454
  continue;
411
455
  // A specifier list starts with `{`. `export default`, `export const` and
412
456
  // `export *` take another route and are left for the IR generator.
413
- if (!tokens[i + 1] || tokens[i + 1].label !== '{')
457
+ if (!t[i + 1] || t[i + 1].label !== '{')
414
458
  continue;
415
459
  // Find this statement's `}` and note any module source, which makes it a
416
460
  // re-export (`export { A } from './x.vsk'`) rather than a specifier list.
417
461
  let k = i + 2;
418
462
  let depth = 1;
419
- for (; k < tokens.length; k++) {
420
- const lbl = tokens[k].label;
463
+ for (; k < t.length; k++) {
464
+ const lbl = t[k].label;
421
465
  if (lbl === 'string') {
422
466
  continue;
423
467
  }
@@ -431,8 +475,8 @@ export function findSpecifierExports(code) {
431
475
  }
432
476
  // A re-export puts `from '…'` AFTER the closing brace, so check past it.
433
477
  let isReexport = false;
434
- for (let j = k + 1; j < tokens.length; j++) {
435
- const lbl = tokens[j].label;
478
+ for (let j = k + 1; j < t.length; j++) {
479
+ const lbl = t[j].label;
436
480
  if (lbl === 'string') {
437
481
  isReexport = true;
438
482
  break;
@@ -442,12 +486,12 @@ export function findSpecifierExports(code) {
442
486
  }
443
487
  if (depth !== 0 || isReexport)
444
488
  continue;
445
- const pairs = readSpecifierPairs(code, tokens[i + 1].end, tokens[k].end);
489
+ const pairs = readSpecifierPairs(code, t[i + 1].end, t[k].end);
446
490
  if (pairs === null)
447
491
  continue;
448
- const stmtEnd = tokens[k].end;
492
+ const stmtEnd = t[k].end;
449
493
  for (const [local, exported] of pairs)
450
- out.push({ local, exported, start: tokens[i].start, end: stmtEnd });
494
+ out.push({ local, exported, start: t[i].start, end: stmtEnd });
451
495
  i = k;
452
496
  }
453
497
  return out;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/compiler",
3
- "version": "0.2.58",
3
+ "version": "0.2.60",
4
4
  "type": "module",
5
5
  "description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@sveltejs/acorn-typescript": "^1.0.0",
23
- "@vesk/types": "^0.2.58",
23
+ "@vesk/types": "^0.2.60",
24
24
  "acorn": "^8.15.0",
25
25
  "esrap": "^2.3.0",
26
26
  "is-reference": "^3.0.0",