@vesk/compiler 0.2.42 → 0.2.43

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.
@@ -156,6 +156,24 @@ function matchKeywordSequence(input, words) {
156
156
  const c = input.charCodeAt(p);
157
157
  return isWsChar(c) || c === 123;
158
158
  }
159
+ /**
160
+ * True only while `tokenizeCode` drives the tokenizer directly. Acorn's
161
+ * `getOptions` copies only known option keys, so the request cannot travel on
162
+ * the options object — it is scoped here instead. Tokenizing is synchronous,
163
+ * so the save/restore is safe.
164
+ */
165
+ let standaloneTokens = false;
166
+ /** Runs `fn` with JSX tag recognition enabled without a parser driving it. */
167
+ export function withStandaloneTokens(fn) {
168
+ const prev = standaloneTokens;
169
+ standaloneTokens = true;
170
+ try {
171
+ return fn();
172
+ }
173
+ finally {
174
+ standaloneTokens = prev;
175
+ }
176
+ }
159
177
  export function VeskParserPlugin(config = {}) {
160
178
  return (Parser) => {
161
179
  const tt = Parser.tokTypes || acorn.tokTypes;
@@ -170,6 +188,15 @@ export function VeskParserPlugin(config = {}) {
170
188
  #jsxChildDepthStack = [];
171
189
  constructor(options, input) {
172
190
  super(options, input);
191
+ // The standalone tokenizer (`tokenizeCode`) drives `getToken()` without
192
+ // ever running the parser, so `component X { … }` never raises
193
+ // #componentDepth and every JSX tag would be read as relational
194
+ // operators. Callers that only need a token stream still need JSX, so
195
+ // they opt in explicitly; parser-driven parses are unaffected.
196
+ }
197
+ /** True when JSX tags are recognised without parser-driven componentDepth. */
198
+ #jsxAllowed() {
199
+ return this.#componentDepth > 0 || standaloneTokens;
173
200
  }
174
201
  #isBlockContext() {
175
202
  const ctx = this.curContext();
@@ -461,7 +488,7 @@ export function VeskParserPlugin(config = {}) {
461
488
  }
462
489
  return super.readToken(this.input.codePointAt(this.pos));
463
490
  }
464
- if (this.#componentDepth > 0 && code === 60 && this.#isBlockContext()) {
491
+ if (this.#jsxAllowed() && code === 60 && this.#isBlockContext()) {
465
492
  const next = this.input.charCodeAt(this.pos + 1);
466
493
  const startsNewStatement = this.hasPrecedingLineBreak();
467
494
  const inType = this.inType;
@@ -16,7 +16,58 @@ export declare function vskImportTarget(importText: string): string | null;
16
16
  */
17
17
  export declare function extractImportStatements(source: string): string[];
18
18
  export declare function collectVskImportPaths(imports: string[], sourcePath: string): string[];
19
+ /**
20
+ * Resolves `export ... from './path.vsk'` targets the same way imports resolve,
21
+ * so a `.vsk` barrel re-export is followed and compiled into the importing
22
+ * file's component registry instead of being emitted as a bare re-export.
23
+ */
24
+ export declare function collectVskReexportPaths(reexportSources: string[], sourcePath: string): string[];
25
+ /**
26
+ * Local names bound by *named* value imports from a `.vsk` module.
27
+ *
28
+ * `.vsk` imports are value-less for component resolution (components are
29
+ * registry entries), but a `.vsk` module may still export plain values —
30
+ * `export const MAX = 10`, `export function format(x) {}`. Those live in the
31
+ * sub-file's `__vesk` and are hoisted into the importer, so the importer's
32
+ * component scope must destructure them.
33
+ *
34
+ * Returned separately from `localValueImportNames` because a name bound this
35
+ * way must NOT be treated as a direct-call target at a JSX call site: it may be
36
+ * a component, which resolves through the registry, not through a scope local.
37
+ */
38
+ export declare function vskImportValueNames(imports: string[]): string[];
19
39
  export declare function vskImportLines(source: string): string[];
40
+ /**
41
+ * Registry aliases contributed by one file's export/import statements.
42
+ *
43
+ * `.vsk` modules are not real ES modules — a component is a registry entry
44
+ * keyed by its declared name, not a top-level binding — so neither
45
+ * `export { A as B }` nor `import { A as B } from './x.vsk'` can be satisfied
46
+ * by emitting JS. Both are instead recorded as registry aliases so `<B />`
47
+ * resolves to the same function as `<A />`. Each pair is
48
+ * `{ local: <canonical name>, exported: <name the tag uses> }`.
49
+ *
50
+ * `default` and `*` are deliberately skipped: the registry is keyed by the
51
+ * component's *declared* name, which is unrelated to the `default` specifier.
52
+ */
53
+ export declare function vskRegistryAliases(importLines: string[], exportAliases: Array<{
54
+ local: string;
55
+ exported: string;
56
+ }>): Array<{
57
+ local: string;
58
+ exported: string;
59
+ }>;
60
+ /**
61
+ * Registers every alias in `aliases` against `componentMap`, pointing each
62
+ * exported name at the function its local name already resolves to. Names that
63
+ * do not resolve (a non-component value, or a component from a file that was
64
+ * not compiled) are left untouched so the original "not found" diagnostic
65
+ * still names the real missing component.
66
+ */
67
+ export declare function applyVskRegistryAliases(componentMap: Map<string, Function>, aliases: Array<{
68
+ local: string;
69
+ exported: string;
70
+ }>): void;
20
71
  /**
21
72
  * True when an `ImportDeclaration` is type-only: an `import type { ... }`
22
73
  * statement, or a value import whose specifiers are all `type` specifiers.
@@ -1 +1 @@
1
- {"version":3,"file":"vsk-imports.d.ts","sourceRoot":"","sources":["../src/vsk-imports.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AAEH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAahE;AA0BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAerF;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAKnD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBhE"}
1
+ {"version":3,"file":"vsk-imports.d.ts","sourceRoot":"","sources":["../src/vsk-imports.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AAEH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAQjE;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAahE;AA0BD,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAUrF;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,eAAe,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAQ/F;AAWD;;;;;;;;;;;;GAYG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAW/D;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAEvD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EAAE,EACrB,aAAa,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GACxD,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAgB5C;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,EACnC,OAAO,EAAE,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,GAClD,IAAI,CAMN;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAKnD;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAsBhE"}
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs';
3
3
  import { print } from 'esrap';
4
4
  import ts from 'esrap/languages/ts';
5
5
  import { parse } from '@vesk/compiler/src/parser';
6
- import { resolveImportPath } from '@vesk/compiler/src/module-imports';
6
+ import { importBindingPairs, resolveImportPath } from '@vesk/compiler/src/module-imports';
7
7
  import { importModuleTarget, tokenizeCode } from '@vesk/compiler/src/tokens';
8
8
  /**
9
9
  * Resolve `import ... from './path.vsk'` statements so helper components can
@@ -88,21 +88,118 @@ export function collectVskImportPaths(imports, sourcePath) {
88
88
  const target = vskImportTarget(imp);
89
89
  if (!target)
90
90
  continue;
91
- if (target.startsWith('.')) {
92
- const full = resolve(dirname(sourcePath), target);
93
- if (existsSync(full))
94
- out.push(full);
91
+ const full = resolveVskTarget(target, sourcePath);
92
+ if (full)
93
+ out.push(full);
94
+ }
95
+ return out;
96
+ }
97
+ /**
98
+ * Resolves `export ... from './path.vsk'` targets the same way imports resolve,
99
+ * so a `.vsk` barrel re-export is followed and compiled into the importing
100
+ * file's component registry instead of being emitted as a bare re-export.
101
+ */
102
+ export function collectVskReexportPaths(reexportSources, sourcePath) {
103
+ const out = [];
104
+ for (const spec of reexportSources) {
105
+ if (!spec.endsWith('.vsk'))
95
106
  continue;
96
- }
97
- const aliased = resolveImportPath(target, dirname(sourcePath));
98
- if (aliased !== target && existsSync(aliased))
99
- out.push(aliased);
107
+ const full = resolveVskTarget(spec, sourcePath);
108
+ if (full)
109
+ out.push(full);
100
110
  }
101
111
  return out;
102
112
  }
113
+ function resolveVskTarget(target, sourcePath) {
114
+ if (target.startsWith('.')) {
115
+ const full = resolve(dirname(sourcePath), target);
116
+ return existsSync(full) ? full : null;
117
+ }
118
+ const aliased = resolveImportPath(target, dirname(sourcePath));
119
+ return aliased !== target && existsSync(aliased) ? aliased : null;
120
+ }
121
+ /**
122
+ * Local names bound by *named* value imports from a `.vsk` module.
123
+ *
124
+ * `.vsk` imports are value-less for component resolution (components are
125
+ * registry entries), but a `.vsk` module may still export plain values —
126
+ * `export const MAX = 10`, `export function format(x) {}`. Those live in the
127
+ * sub-file's `__vesk` and are hoisted into the importer, so the importer's
128
+ * component scope must destructure them.
129
+ *
130
+ * Returned separately from `localValueImportNames` because a name bound this
131
+ * way must NOT be treated as a direct-call target at a JSX call site: it may be
132
+ * a component, which resolves through the registry, not through a scope local.
133
+ */
134
+ export function vskImportValueNames(imports) {
135
+ const names = [];
136
+ for (const imp of imports) {
137
+ if (stripTypeImport(imp) === null)
138
+ continue;
139
+ if (vskImportTarget(imp) === null)
140
+ continue;
141
+ for (const pair of importBindingPairs(imp)) {
142
+ if (pair.imported === 'default' || pair.imported === '*')
143
+ continue;
144
+ names.push(pair.local);
145
+ }
146
+ }
147
+ return names;
148
+ }
103
149
  export function vskImportLines(source) {
104
150
  return extractImportStatements(source).filter((imp) => vskImportTarget(imp) !== null && stripTypeImport(imp) !== null);
105
151
  }
152
+ /**
153
+ * Registry aliases contributed by one file's export/import statements.
154
+ *
155
+ * `.vsk` modules are not real ES modules — a component is a registry entry
156
+ * keyed by its declared name, not a top-level binding — so neither
157
+ * `export { A as B }` nor `import { A as B } from './x.vsk'` can be satisfied
158
+ * by emitting JS. Both are instead recorded as registry aliases so `<B />`
159
+ * resolves to the same function as `<A />`. Each pair is
160
+ * `{ local: <canonical name>, exported: <name the tag uses> }`.
161
+ *
162
+ * `default` and `*` are deliberately skipped: the registry is keyed by the
163
+ * component's *declared* name, which is unrelated to the `default` specifier.
164
+ */
165
+ export function vskRegistryAliases(importLines, exportAliases) {
166
+ const aliases = [];
167
+ for (const alias of exportAliases) {
168
+ if (alias.local !== alias.exported)
169
+ aliases.push({ local: alias.local, exported: alias.exported });
170
+ }
171
+ for (const imp of importLines) {
172
+ if (stripTypeImport(imp) === null)
173
+ continue;
174
+ if (vskImportTarget(imp) === null)
175
+ continue;
176
+ for (const pair of importBindingPairs(imp)) {
177
+ if (pair.imported === 'default' || pair.imported === '*')
178
+ continue;
179
+ // `import { A as B }` → the CANONICAL name is `pair.imported` ('A') and
180
+ // the ALIAS the tag actually uses is `pair.local` ('B').
181
+ if (pair.local !== pair.imported)
182
+ aliases.push({ local: pair.imported, exported: pair.local });
183
+ }
184
+ }
185
+ return aliases;
186
+ }
187
+ /**
188
+ * Registers every alias in `aliases` against `componentMap`, pointing each
189
+ * exported name at the function its local name already resolves to. Names that
190
+ * do not resolve (a non-component value, or a component from a file that was
191
+ * not compiled) are left untouched so the original "not found" diagnostic
192
+ * still names the real missing component.
193
+ */
194
+ export function applyVskRegistryAliases(componentMap, aliases) {
195
+ for (const { local, exported } of aliases) {
196
+ if (componentMap.has(exported))
197
+ continue;
198
+ const fn = componentMap.get(local);
199
+ if (fn)
200
+ componentMap.set(exported, fn);
201
+ }
202
+ }
106
203
  /**
107
204
  * True when an `ImportDeclaration` is type-only: an `import type { ... }`
108
205
  * statement, or a value import whose specifiers are all `type` specifiers.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/compiler",
3
- "version": "0.2.42",
3
+ "version": "0.2.43",
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.42",
23
+ "@vesk/types": "^0.2.43",
24
24
  "acorn": "^8.15.0",
25
25
  "esrap": "^2.3.0",
26
26
  "is-reference": "^3.0.0",