@vesk/compiler 0.0.1
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.
- package/README.md +23 -0
- package/dist/acorn-ts-plugin/index.d.ts +482 -0
- package/dist/acorn-ts-plugin/index.d.ts.map +1 -0
- package/dist/acorn-ts-plugin/index.js +5028 -0
- package/dist/actions.d.ts +22 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +167 -0
- package/dist/api-routes.d.ts +30 -0
- package/dist/api-routes.d.ts.map +1 -0
- package/dist/api-routes.js +350 -0
- package/dist/client-codegen.d.ts +19 -0
- package/dist/client-codegen.d.ts.map +1 -0
- package/dist/client-codegen.js +1523 -0
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +106 -0
- package/dist/errors.d.ts +33 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +210 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/ir-generator.d.ts +10 -0
- package/dist/ir-generator.d.ts.map +1 -0
- package/dist/ir-generator.js +1062 -0
- package/dist/ir.d.ts +160 -0
- package/dist/ir.d.ts.map +1 -0
- package/dist/ir.js +219 -0
- package/dist/middleware.d.ts +15 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +183 -0
- package/dist/package.json +15 -0
- package/dist/parser.d.ts +38 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +242 -0
- package/dist/router.d.ts +31 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +451 -0
- package/dist/scan.d.ts +106 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +438 -0
- package/dist/server-codegen.d.ts +5 -0
- package/dist/server-codegen.d.ts.map +1 -0
- package/dist/server-codegen.js +4 -0
- package/dist/server-cookies.d.ts +2 -0
- package/dist/server-cookies.d.ts.map +1 -0
- package/dist/server-cookies.js +15 -0
- package/dist/server-head.d.ts +10 -0
- package/dist/server-head.d.ts.map +1 -0
- package/dist/server-head.js +289 -0
- package/dist/server-jsgen.d.ts +6 -0
- package/dist/server-jsgen.d.ts.map +1 -0
- package/dist/server-jsgen.js +446 -0
- package/dist/server-render.d.ts +16 -0
- package/dist/server-render.d.ts.map +1 -0
- package/dist/server-render.js +397 -0
- package/dist/server-utils.d.ts +93 -0
- package/dist/server-utils.d.ts.map +1 -0
- package/dist/server-utils.js +747 -0
- package/dist/ssr-store.d.ts +4 -0
- package/dist/ssr-store.d.ts.map +1 -0
- package/dist/ssr-store.js +29 -0
- package/dist/strip-ts.d.ts +25 -0
- package/dist/strip-ts.d.ts.map +1 -0
- package/dist/strip-ts.js +171 -0
- package/dist/tokens.d.ts +50 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +334 -0
- package/dist/typecheck.d.ts +19 -0
- package/dist/typecheck.d.ts.map +1 -0
- package/dist/typecheck.js +365 -0
- package/dist/types.d.ts +147 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +13 -0
- package/dist/vesk-plugin.d.ts +53 -0
- package/dist/vesk-plugin.d.ts.map +1 -0
- package/dist/vesk-plugin.js +413 -0
- package/dist/vsk-imports.d.ts +30 -0
- package/dist/vsk-imports.d.ts.map +1 -0
- package/dist/vsk-imports.js +141 -0
- package/dist/vsk-tsx.d.ts +3 -0
- package/dist/vsk-tsx.d.ts.map +1 -0
- package/dist/vsk-tsx.js +604 -0
- package/package.json +52 -0
package/dist/vsk-tsx.js
ADDED
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
import { parse } from '@vesk/compiler/src/parser';
|
|
2
|
+
import { getPropsType } from '@vesk/compiler/src/ir-generator';
|
|
3
|
+
import { skipWhitespace, findBalancedEnd, splitTopLevel, startsWithIdentifier, stripDeclKeyword, stripTrailingSemicolons, isWhitespaceChar, isIdentStart, isIdentChar, htmlTagEnd, collapseNewlineWhitespace, } from '@vesk/compiler/src/scan';
|
|
4
|
+
import { containsIdentifier, isIdentifierImported } from '@vesk/compiler/src/tokens';
|
|
5
|
+
function applyEdits(source, edits) {
|
|
6
|
+
const sorted = edits.sort((a, b) => b.start - a.start || b.end - a.end);
|
|
7
|
+
let out = source;
|
|
8
|
+
for (const e of sorted) {
|
|
9
|
+
out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
10
|
+
}
|
|
11
|
+
return out;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Collects edit operations that rewrite `&[...] = track<...>(...)`
|
|
15
|
+
* declarations into plain typed `let` declarations, using the parsed AST so
|
|
16
|
+
* nested generics (`track<Map<string, number>>(...)`), type annotations and
|
|
17
|
+
* nested statement positions are handled without regex. `wrapped` is the
|
|
18
|
+
* fake component source (offsets inside it), `prefixLen` the distance from
|
|
19
|
+
* the start of `wrapped` to the start of the real body text.
|
|
20
|
+
*/
|
|
21
|
+
function collectTrackDeclEdits(wrapped, ast, prefixLen) {
|
|
22
|
+
const edits = [];
|
|
23
|
+
function walk(stmts) {
|
|
24
|
+
for (const stmt of stmts) {
|
|
25
|
+
if (!stmt)
|
|
26
|
+
continue;
|
|
27
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
28
|
+
const decls = stmt.declarations || [];
|
|
29
|
+
for (let di = 0; di < decls.length; di++) {
|
|
30
|
+
const decl = decls[di];
|
|
31
|
+
const id = decl.id;
|
|
32
|
+
if (!id || id.type !== 'ArrayPattern' || id.lazy !== true)
|
|
33
|
+
continue;
|
|
34
|
+
if (!decl.init)
|
|
35
|
+
continue;
|
|
36
|
+
const names = (id.elements || [])
|
|
37
|
+
.map((el) => (el ? el.name : null))
|
|
38
|
+
.filter((n) => typeof n === 'string');
|
|
39
|
+
const initText = wrapped.slice(decl.init.start, decl.init.end);
|
|
40
|
+
// The first declarator's replacement must also absorb the statement's
|
|
41
|
+
// `const`/`let`/`var` keyword, which starts at `stmt.start`, not at
|
|
42
|
+
// the declarator's start.
|
|
43
|
+
const start = di === 0 ? stmt.start : decl.start;
|
|
44
|
+
const end = decl.end;
|
|
45
|
+
// Don't duplicate the statement terminator already present after the
|
|
46
|
+
// declaration (avoids `let x = ...;;`).
|
|
47
|
+
const terminator = wrapped[end] === ';' ? '' : ';';
|
|
48
|
+
if (names.length === 0) {
|
|
49
|
+
edits.push({ start: start - prefixLen, end: end - prefixLen, text: `let _ = ${initText}${terminator}` });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const annotation = id.typeAnnotation?.typeAnnotation
|
|
53
|
+
? wrapped.slice(id.typeAnnotation.typeAnnotation.start, id.typeAnnotation.typeAnnotation.end).trim()
|
|
54
|
+
: decl.init.typeArguments
|
|
55
|
+
? wrapped.slice(decl.init.typeArguments.start + 1, decl.init.typeArguments.end - 1).trim()
|
|
56
|
+
: '';
|
|
57
|
+
const first = names[0];
|
|
58
|
+
let replacement;
|
|
59
|
+
if (annotation) {
|
|
60
|
+
replacement = `let ${first}: ${annotation} = (${initText} as unknown as ${annotation});`;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
replacement = `let ${first}: any = ${initText};`;
|
|
64
|
+
}
|
|
65
|
+
for (let n = 1; n < names.length; n++) {
|
|
66
|
+
replacement += ` let ${names[n]}: any = ${first};`;
|
|
67
|
+
}
|
|
68
|
+
// The last emitted statement sits directly before `end`; drop its
|
|
69
|
+
// terminator when the source already provides one (avoids `;;`).
|
|
70
|
+
if (wrapped[end] === ';')
|
|
71
|
+
replacement = replacement.slice(0, -1);
|
|
72
|
+
edits.push({ start: start - prefixLen, end: end - prefixLen, text: replacement });
|
|
73
|
+
}
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const kids = [];
|
|
77
|
+
if (stmt.consequent)
|
|
78
|
+
kids.push(stmt.consequent);
|
|
79
|
+
if (stmt.alternate)
|
|
80
|
+
kids.push(stmt.alternate);
|
|
81
|
+
if (Array.isArray(stmt.body))
|
|
82
|
+
kids.push(...stmt.body);
|
|
83
|
+
else if (stmt.body && stmt.body.type)
|
|
84
|
+
kids.push(stmt.body);
|
|
85
|
+
if (Array.isArray(stmt.cases)) {
|
|
86
|
+
for (const c of stmt.cases)
|
|
87
|
+
if (Array.isArray(c.consequent))
|
|
88
|
+
kids.push(...c.consequent);
|
|
89
|
+
}
|
|
90
|
+
if (stmt.handler)
|
|
91
|
+
kids.push(stmt.handler.body);
|
|
92
|
+
if (stmt.finalizer)
|
|
93
|
+
kids.push(stmt.finalizer.body);
|
|
94
|
+
walk(kids);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
walk(ast.body);
|
|
98
|
+
return edits;
|
|
99
|
+
}
|
|
100
|
+
const REWRITER_WRAPPER = 'component __VskRewriter() { ';
|
|
101
|
+
const REWRITER_WRAPPER_LEN = REWRITER_WRAPPER.length;
|
|
102
|
+
function rewriteTrackDecls(text) {
|
|
103
|
+
if (!text.includes('&['))
|
|
104
|
+
return text;
|
|
105
|
+
const wrapped = REWRITER_WRAPPER + text + ' }';
|
|
106
|
+
let ast;
|
|
107
|
+
try {
|
|
108
|
+
ast = parse(wrapped);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return text;
|
|
112
|
+
}
|
|
113
|
+
const edits = collectTrackDeclEdits(wrapped, ast, REWRITER_WRAPPER_LEN);
|
|
114
|
+
if (edits.length === 0)
|
|
115
|
+
return text;
|
|
116
|
+
return applyEdits(text, edits);
|
|
117
|
+
}
|
|
118
|
+
function stripStyleBlocks(text) {
|
|
119
|
+
const out = [];
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (i < text.length) {
|
|
122
|
+
if (text[i] === '<' && text.slice(i, i + 6) === '<style') {
|
|
123
|
+
const next = i + 6;
|
|
124
|
+
if (next >= text.length || isWhitespaceChar(text[next]) || text[next] === '>' || text[next] === '/') {
|
|
125
|
+
const openEnd = htmlTagEnd(text, i);
|
|
126
|
+
if (openEnd !== -1) {
|
|
127
|
+
const closeIdx = text.indexOf('</style>', openEnd);
|
|
128
|
+
if (closeIdx !== -1) {
|
|
129
|
+
i = closeIdx + '</style>'.length;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
out.push(text[i]);
|
|
136
|
+
i++;
|
|
137
|
+
}
|
|
138
|
+
return out.join('');
|
|
139
|
+
}
|
|
140
|
+
function getComponents(ast) {
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const stmt of ast.body || []) {
|
|
143
|
+
if (stmt.type === 'ComponentDeclaration') {
|
|
144
|
+
out.push({ node: stmt, exported: false, defaultExport: false, start: stmt.start });
|
|
145
|
+
}
|
|
146
|
+
else if (stmt.type === 'ExportNamedDeclaration' && stmt.declaration?.type === 'ComponentDeclaration') {
|
|
147
|
+
out.push({ node: stmt.declaration, exported: true, defaultExport: false, start: stmt.start });
|
|
148
|
+
}
|
|
149
|
+
else if (stmt.type === 'ExportDefaultDeclaration' && stmt.declaration?.type === 'ComponentDeclaration') {
|
|
150
|
+
out.push({ node: stmt.declaration, exported: true, defaultExport: true, start: stmt.start });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
function blankForClauses(source, ast) {
|
|
156
|
+
const annotations = ast.__vskAnnotations ?? [];
|
|
157
|
+
let out = source;
|
|
158
|
+
for (const ann of annotations) {
|
|
159
|
+
if (ann.kind !== 'for-clause')
|
|
160
|
+
continue;
|
|
161
|
+
out = out.slice(0, ann.clauseStart) + ' '.repeat(ann.clauseEnd - ann.clauseStart) + out.slice(ann.clauseEnd);
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
function extractForHeader(text) {
|
|
166
|
+
if (!startsWithIdentifier(text, 'for'))
|
|
167
|
+
return null;
|
|
168
|
+
const i = skipWhitespace(text, 3);
|
|
169
|
+
if (text[i] !== '(')
|
|
170
|
+
return null;
|
|
171
|
+
const end = findBalancedEnd(text, i);
|
|
172
|
+
return text.slice(i + 1, end);
|
|
173
|
+
}
|
|
174
|
+
function getJSXTagName(el) {
|
|
175
|
+
const name = el?.openingElement?.name ?? el?.name;
|
|
176
|
+
if (!name)
|
|
177
|
+
return null;
|
|
178
|
+
if (name.type === 'JSXIdentifier')
|
|
179
|
+
return name.name;
|
|
180
|
+
if (name.type === 'JSXMemberExpression')
|
|
181
|
+
return name.object.name + '.' + name.property.name;
|
|
182
|
+
if (name.type === 'JSXNamespacedName')
|
|
183
|
+
return name.namespace.name + ':' + name.name.name;
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
function tsxBlock(source, body, indent) {
|
|
187
|
+
const stmts = body && body.type === 'BlockStatement'
|
|
188
|
+
? body.body
|
|
189
|
+
: body ? (Array.isArray(body) ? body : [body]) : [];
|
|
190
|
+
const inner = tsxEmitBody(source, stmts, indent + ' ');
|
|
191
|
+
return `{\n${inner}\n${indent}}`;
|
|
192
|
+
}
|
|
193
|
+
function tsxEmitJSXChildren(source, children) {
|
|
194
|
+
const parts = [];
|
|
195
|
+
let i = 0;
|
|
196
|
+
let forPending = false;
|
|
197
|
+
while (i < children.length) {
|
|
198
|
+
const child = children[i];
|
|
199
|
+
if (child.type !== 'JSXText')
|
|
200
|
+
forPending = false;
|
|
201
|
+
if (child.type === 'JSXText') {
|
|
202
|
+
const text = collapseNewlineWhitespace(child.value);
|
|
203
|
+
const trimmed = text.trim();
|
|
204
|
+
if (!trimmed || trimmed.startsWith('//')) {
|
|
205
|
+
i++;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const forDecl = extractForHeader(trimmed);
|
|
209
|
+
const nextChild = children[i + 1];
|
|
210
|
+
if (forDecl !== null && nextChild !== undefined && (nextChild.type === 'JSXExpressionContainer' ||
|
|
211
|
+
nextChild.type === 'JSXElement' ||
|
|
212
|
+
nextChild.type === 'JSXFragment')) {
|
|
213
|
+
let bodyText;
|
|
214
|
+
if (nextChild.type === 'JSXExpressionContainer') {
|
|
215
|
+
const exprNode = nextChild.expression;
|
|
216
|
+
if (exprNode.type === 'JSXEmptyExpression') {
|
|
217
|
+
parts.push(text);
|
|
218
|
+
i++;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
bodyText = tsxEmitJSXExprBody(source, exprNode);
|
|
222
|
+
}
|
|
223
|
+
else if (nextChild.type === 'JSXFragment') {
|
|
224
|
+
bodyText = tsxEmitJSXFragment(source, nextChild);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
bodyText = tsxEmitJSXElement(source, nextChild);
|
|
228
|
+
}
|
|
229
|
+
let emptyText = '';
|
|
230
|
+
let consumed = 2;
|
|
231
|
+
const emptyTextNode = children[i + 2];
|
|
232
|
+
const emptyContainer = children[i + 3];
|
|
233
|
+
if (emptyTextNode && emptyTextNode.type === 'JSXText' &&
|
|
234
|
+
['#empty', 'empty'].includes(emptyTextNode.value.trim()) &&
|
|
235
|
+
emptyContainer && emptyContainer.type === 'JSXExpressionContainer' &&
|
|
236
|
+
emptyContainer.expression.type !== 'JSXEmptyExpression') {
|
|
237
|
+
emptyText = tsxEmitJSXExprBody(source, emptyContainer.expression);
|
|
238
|
+
consumed = 4;
|
|
239
|
+
}
|
|
240
|
+
const ofParts = splitTopLevel(forDecl, 'of');
|
|
241
|
+
if (ofParts.length === 2) {
|
|
242
|
+
const itemVar = stripDeclKeyword(ofParts[0]).trim();
|
|
243
|
+
const arrExpr = ofParts[1].trim();
|
|
244
|
+
const map = `{ ${arrExpr}.map((${itemVar}: (typeof ${arrExpr})[number]) => (${bodyText})) }`;
|
|
245
|
+
if (emptyText) {
|
|
246
|
+
parts.push(`{ (() => { const _v = ${arrExpr}; return _v.length === 0 ? (${emptyText}) : _v.map((${itemVar}: (typeof _v)[number]) => (${bodyText})); })() }`);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
parts.push(map);
|
|
250
|
+
}
|
|
251
|
+
i += consumed;
|
|
252
|
+
forPending = true;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const inParts = splitTopLevel(forDecl, 'in');
|
|
256
|
+
if (inParts.length === 2) {
|
|
257
|
+
const itemVar = stripDeclKeyword(inParts[0]).trim();
|
|
258
|
+
const objExpr = inParts[1].trim();
|
|
259
|
+
parts.push(`{ Object.keys(${objExpr}).map((${itemVar}) => (${bodyText})) }`);
|
|
260
|
+
i += consumed;
|
|
261
|
+
forPending = true;
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (forPending && trimmed === '}') {
|
|
266
|
+
forPending = false;
|
|
267
|
+
i++;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
forPending = false;
|
|
271
|
+
parts.push(text);
|
|
272
|
+
i++;
|
|
273
|
+
}
|
|
274
|
+
else if (child.type === 'JSXExpressionContainer') {
|
|
275
|
+
if (child.expression.type === 'JSXEmptyExpression') {
|
|
276
|
+
i++;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
parts.push(`{${source.slice(child.expression.start, child.expression.end)}}`);
|
|
280
|
+
i++;
|
|
281
|
+
}
|
|
282
|
+
else if (child.type === 'JSXElement') {
|
|
283
|
+
if (getJSXTagName(child) === 'style') {
|
|
284
|
+
i++;
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
parts.push(tsxEmitJSXElement(source, child));
|
|
288
|
+
i++;
|
|
289
|
+
}
|
|
290
|
+
else if (child.type === 'JSXFragment') {
|
|
291
|
+
parts.push(tsxEmitJSXFragment(source, child));
|
|
292
|
+
i++;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
i++;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return parts.join('');
|
|
299
|
+
}
|
|
300
|
+
function tsxEmitJSXExprBody(source, expr) {
|
|
301
|
+
if (expr.type === 'JSXElement')
|
|
302
|
+
return tsxEmitJSXElement(source, expr);
|
|
303
|
+
if (expr.type === 'JSXFragment')
|
|
304
|
+
return tsxEmitJSXFragment(source, expr);
|
|
305
|
+
return source.slice(expr.start, expr.end);
|
|
306
|
+
}
|
|
307
|
+
function tsxEmitJSXElement(source, el) {
|
|
308
|
+
const opening = source.slice(el.openingElement.start, el.openingElement.end);
|
|
309
|
+
if (el.openingElement.selfClosing)
|
|
310
|
+
return opening;
|
|
311
|
+
const inner = tsxEmitJSXChildren(source, el.children ?? []);
|
|
312
|
+
const closing = el.closingElement ? source.slice(el.closingElement.start, el.closingElement.end) : '';
|
|
313
|
+
return opening + inner + closing;
|
|
314
|
+
}
|
|
315
|
+
function tsxEmitJSXFragment(source, frag) {
|
|
316
|
+
const inner = tsxEmitJSXChildren(source, frag.children ?? []);
|
|
317
|
+
return '<>' + inner + '</>';
|
|
318
|
+
}
|
|
319
|
+
function tsxEmitIf(source, stmt, indent) {
|
|
320
|
+
const cond = source.slice(stmt.test.start, stmt.test.end);
|
|
321
|
+
let out = `${indent}if (${cond}) ${tsxBlock(source, stmt.consequent, indent)}`;
|
|
322
|
+
if (stmt.alternate) {
|
|
323
|
+
const altBlock = stmt.alternate.type === 'BlockStatement'
|
|
324
|
+
? tsxBlock(source, stmt.alternate, indent)
|
|
325
|
+
: tsxBlock(source, [stmt.alternate], indent);
|
|
326
|
+
out += ` else ${altBlock}`;
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
}
|
|
330
|
+
function tsxEmitForOf(source, stmt, indent) {
|
|
331
|
+
const left = source.slice(stmt.left.start, stmt.left.end);
|
|
332
|
+
const right = source.slice(stmt.right.start, stmt.right.end);
|
|
333
|
+
return `${indent}for (${left} of ${right}) ${tsxBlock(source, stmt.body, indent)}`;
|
|
334
|
+
}
|
|
335
|
+
function tsxEmitForIn(source, stmt, indent) {
|
|
336
|
+
const left = source.slice(stmt.left.start, stmt.left.end);
|
|
337
|
+
const right = source.slice(stmt.right.start, stmt.right.end);
|
|
338
|
+
return `${indent}for (${left} in ${right}) ${tsxBlock(source, stmt.body, indent)}`;
|
|
339
|
+
}
|
|
340
|
+
function tsxEmitFor(source, stmt, indent) {
|
|
341
|
+
const init = stmt.init ? source.slice(stmt.init.start, stmt.init.end) : '';
|
|
342
|
+
const test = stmt.test ? source.slice(stmt.test.start, stmt.test.end) : '';
|
|
343
|
+
const update = stmt.update ? source.slice(stmt.update.start, stmt.update.end) : '';
|
|
344
|
+
return `${indent}for (${init}; ${test}; ${update}) ${tsxBlock(source, stmt.body, indent)}`;
|
|
345
|
+
}
|
|
346
|
+
function tsxEmitWhile(source, stmt, indent) {
|
|
347
|
+
const test = source.slice(stmt.test.start, stmt.test.end);
|
|
348
|
+
return `${indent}while (${test}) ${tsxBlock(source, stmt.body, indent)}`;
|
|
349
|
+
}
|
|
350
|
+
function tsxEmitDoWhile(source, stmt, indent) {
|
|
351
|
+
const test = source.slice(stmt.test.start, stmt.test.end);
|
|
352
|
+
return `${indent}do ${tsxBlock(source, stmt.body, indent)} while (${test});`;
|
|
353
|
+
}
|
|
354
|
+
function tsxEmitSwitch(source, stmt, indent) {
|
|
355
|
+
const disc = source.slice(stmt.discriminant.start, stmt.discriminant.end);
|
|
356
|
+
const lines = [`${indent}switch (${disc}) {`];
|
|
357
|
+
for (const c of stmt.cases ?? []) {
|
|
358
|
+
if (c.test) {
|
|
359
|
+
lines.push(`${indent} case ${source.slice(c.test.start, c.test.end)}:`);
|
|
360
|
+
}
|
|
361
|
+
else {
|
|
362
|
+
lines.push(`${indent} default:`);
|
|
363
|
+
}
|
|
364
|
+
const bodyText = tsxEmitBody(source, c.consequent, indent + ' ');
|
|
365
|
+
if (bodyText)
|
|
366
|
+
lines.push(bodyText);
|
|
367
|
+
}
|
|
368
|
+
lines.push(`${indent}}`);
|
|
369
|
+
return lines.join('\n');
|
|
370
|
+
}
|
|
371
|
+
function tsxEmitTry(source, stmt, indent) {
|
|
372
|
+
let out = `${indent}try ${tsxBlock(source, stmt.block, indent)}`;
|
|
373
|
+
if (stmt.handler) {
|
|
374
|
+
const param = stmt.handler.param ? source.slice(stmt.handler.param.start, stmt.handler.param.end) : '';
|
|
375
|
+
out += ` catch${param ? ` (${param})` : ''} ${tsxBlock(source, stmt.handler.body, indent)}`;
|
|
376
|
+
}
|
|
377
|
+
if (stmt.finalizer) {
|
|
378
|
+
out += ` finally ${tsxBlock(source, stmt.finalizer, indent)}`;
|
|
379
|
+
}
|
|
380
|
+
return out;
|
|
381
|
+
}
|
|
382
|
+
function tsxEmitStatement(source, stmt, indent, isLast) {
|
|
383
|
+
switch (stmt.type) {
|
|
384
|
+
case 'JSXElement':
|
|
385
|
+
if (getJSXTagName(stmt) === 'style')
|
|
386
|
+
return '';
|
|
387
|
+
return indent + tsxEmitJSXElement(source, stmt) + (isLast ? '' : ';');
|
|
388
|
+
case 'JSXFragment':
|
|
389
|
+
return indent + tsxEmitJSXFragment(source, stmt) + (isLast ? '' : ';');
|
|
390
|
+
case 'JSXExpressionContainer': {
|
|
391
|
+
if (stmt.expression.type === 'JSXEmptyExpression')
|
|
392
|
+
return '';
|
|
393
|
+
const text = `{${source.slice(stmt.expression.start, stmt.expression.end)}}`;
|
|
394
|
+
return indent + text + (isLast ? '' : ';');
|
|
395
|
+
}
|
|
396
|
+
case 'VeskBlock': {
|
|
397
|
+
if (stmt.tag === 'empty')
|
|
398
|
+
return '';
|
|
399
|
+
const lines = [];
|
|
400
|
+
const innerStmts = stmt.body ?? [];
|
|
401
|
+
for (let i = 0; i < innerStmts.length; i++) {
|
|
402
|
+
const t = tsxEmitStatement(source, innerStmts[i], indent, isLast && i === innerStmts.length - 1);
|
|
403
|
+
if (t !== '')
|
|
404
|
+
lines.push(t);
|
|
405
|
+
}
|
|
406
|
+
return lines.join('\n');
|
|
407
|
+
}
|
|
408
|
+
case 'IfStatement':
|
|
409
|
+
return tsxEmitIf(source, stmt, indent);
|
|
410
|
+
case 'ForOfStatement':
|
|
411
|
+
return tsxEmitForOf(source, stmt, indent);
|
|
412
|
+
case 'ForInStatement':
|
|
413
|
+
return tsxEmitForIn(source, stmt, indent);
|
|
414
|
+
case 'ForStatement':
|
|
415
|
+
return tsxEmitFor(source, stmt, indent);
|
|
416
|
+
case 'WhileStatement':
|
|
417
|
+
return tsxEmitWhile(source, stmt, indent);
|
|
418
|
+
case 'DoWhileStatement':
|
|
419
|
+
return tsxEmitDoWhile(source, stmt, indent);
|
|
420
|
+
case 'SwitchStatement':
|
|
421
|
+
return tsxEmitSwitch(source, stmt, indent);
|
|
422
|
+
case 'TryStatement':
|
|
423
|
+
return tsxEmitTry(source, stmt, indent);
|
|
424
|
+
case 'LabeledStatement': {
|
|
425
|
+
const body = tsxEmitStatement(source, stmt.body, indent, isLast);
|
|
426
|
+
return `${indent}${stmt.label.name}:\n${body}`;
|
|
427
|
+
}
|
|
428
|
+
default: {
|
|
429
|
+
let text = stripTrailingSemicolons(source.slice(stmt.start, stmt.end).trimEnd());
|
|
430
|
+
if (text === '')
|
|
431
|
+
return '';
|
|
432
|
+
return indent + text + (text.endsWith('}') || isLast ? '' : ';');
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
function tsxEmitBody(source, stmts, indent) {
|
|
437
|
+
const lines = [];
|
|
438
|
+
let i = 0;
|
|
439
|
+
while (i < stmts.length) {
|
|
440
|
+
const stmt = stmts[i];
|
|
441
|
+
const isLast = i === stmts.length - 1;
|
|
442
|
+
if (stmt.type === 'ForOfStatement' &&
|
|
443
|
+
stmts[i + 1] && stmts[i + 1].type === 'VeskBlock' && stmts[i + 1].tag === 'empty') {
|
|
444
|
+
const empty = stmts[i + 1];
|
|
445
|
+
lines.push(tsxEmitForOf(source, stmt, indent));
|
|
446
|
+
const inner = empty.body ?? [];
|
|
447
|
+
if (inner.length > 0) {
|
|
448
|
+
const emptyText = tsxEmitBody(source, inner, indent + ' ');
|
|
449
|
+
const arrExpr = source.slice(stmt.right.start, stmt.right.end);
|
|
450
|
+
lines.push(`${indent}${arrExpr}.length === 0 && (() => {`);
|
|
451
|
+
lines.push(emptyText);
|
|
452
|
+
lines.push(`${indent}})();`);
|
|
453
|
+
}
|
|
454
|
+
i += 2;
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
const text = tsxEmitStatement(source, stmt, indent, isLast);
|
|
458
|
+
if (text !== '')
|
|
459
|
+
lines.push(text);
|
|
460
|
+
i++;
|
|
461
|
+
}
|
|
462
|
+
return lines.join('\n');
|
|
463
|
+
}
|
|
464
|
+
function isStatementMode(node) {
|
|
465
|
+
const stmts = node.body?.body ?? [];
|
|
466
|
+
return !(stmts.length === 1 && stmts[0].type === 'ReturnStatement');
|
|
467
|
+
}
|
|
468
|
+
/** Replaces the first whole-word occurrence of `word` in `text`. */
|
|
469
|
+
function replaceFirstKeyword(text, word, replacement) {
|
|
470
|
+
let i = 0;
|
|
471
|
+
while (i < text.length) {
|
|
472
|
+
if (isIdentStart(text[i])) {
|
|
473
|
+
let j = i + 1;
|
|
474
|
+
while (j < text.length && isIdentChar(text[j]))
|
|
475
|
+
j++;
|
|
476
|
+
if (text.slice(i, j) === word)
|
|
477
|
+
return text.slice(0, i) + replacement + text.slice(j);
|
|
478
|
+
i = j;
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
i++;
|
|
482
|
+
}
|
|
483
|
+
return text;
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Removes a trailing `client` keyword from a component header (the header is
|
|
487
|
+
* the text from the declaration start up to the body `{`), so `component Foo
|
|
488
|
+
* client {` becomes `component Foo {`. Returns the text with the keyword
|
|
489
|
+
* replaced by a single space.
|
|
490
|
+
*/
|
|
491
|
+
function stripTrailingClient(text) {
|
|
492
|
+
let end = text.length;
|
|
493
|
+
while (end > 0 && isWhitespaceChar(text[end - 1]))
|
|
494
|
+
end--;
|
|
495
|
+
let wordStart = end;
|
|
496
|
+
while (wordStart > 0 && isIdentChar(text[wordStart - 1]))
|
|
497
|
+
wordStart--;
|
|
498
|
+
if (text.slice(wordStart, end) === 'client') {
|
|
499
|
+
return text.slice(0, wordStart).trimEnd() + ' ';
|
|
500
|
+
}
|
|
501
|
+
return text;
|
|
502
|
+
}
|
|
503
|
+
export function vskToTsx(source) {
|
|
504
|
+
let ast;
|
|
505
|
+
try {
|
|
506
|
+
ast = parse(source);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return source;
|
|
510
|
+
}
|
|
511
|
+
const clean = blankForClauses(source, ast);
|
|
512
|
+
const edits = [];
|
|
513
|
+
for (const { node, start } of getComponents(ast)) {
|
|
514
|
+
const body = node.body;
|
|
515
|
+
if (!body || body.start == null || body.end == null)
|
|
516
|
+
continue;
|
|
517
|
+
let transformed = stripTrailingClient(replaceFirstKeyword(clean.slice(start, body.start), 'component', 'function'));
|
|
518
|
+
if (!transformed.includes('(')) {
|
|
519
|
+
transformed = transformed.trimEnd() + '() ';
|
|
520
|
+
}
|
|
521
|
+
const statementMode = isStatementMode(node);
|
|
522
|
+
let wrapped;
|
|
523
|
+
if (statementMode) {
|
|
524
|
+
const bodyText = rewriteTrackDecls(tsxEmitBody(clean, body.body ?? [], ''));
|
|
525
|
+
wrapped = '{ ' + bodyText.trim() + ' }';
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
const raw = clean.slice(body.start, body.end);
|
|
529
|
+
const inner = raw.trim();
|
|
530
|
+
const hasBraces = inner.startsWith('{') && inner.endsWith('}');
|
|
531
|
+
const core = hasBraces ? inner.slice(1, -1) : inner;
|
|
532
|
+
const bodyText = rewriteTrackDecls(stripStyleBlocks(core));
|
|
533
|
+
wrapped = hasBraces ? '{ ' + bodyText.trim() + ' }' : bodyText;
|
|
534
|
+
}
|
|
535
|
+
edits.push({ start, end: body.end, text: transformed + wrapped });
|
|
536
|
+
}
|
|
537
|
+
const out = applyEdits(source, edits);
|
|
538
|
+
const isModule = (ast.body || []).some((s) => s.type === 'ImportDeclaration' ||
|
|
539
|
+
s.type === 'ExportNamedDeclaration' ||
|
|
540
|
+
s.type === 'ExportDefaultDeclaration' ||
|
|
541
|
+
s.type === 'ExportAllDeclaration');
|
|
542
|
+
if (isModule && containsIdentifier(out, 'Head') && !isIdentifierImported(out, 'Head')) {
|
|
543
|
+
return `declare const Head: (props: { children?: unknown }) => unknown;\n${out}`;
|
|
544
|
+
}
|
|
545
|
+
return out;
|
|
546
|
+
}
|
|
547
|
+
function isTypeDecl(stmt) {
|
|
548
|
+
return (stmt.type === 'TSInterfaceDeclaration' ||
|
|
549
|
+
stmt.type === 'TSTypeAliasDeclaration' ||
|
|
550
|
+
(stmt.type === 'ExportNamedDeclaration' &&
|
|
551
|
+
(stmt.declaration?.type === 'TSInterfaceDeclaration' || stmt.declaration?.type === 'TSTypeAliasDeclaration')));
|
|
552
|
+
}
|
|
553
|
+
export function generateVskDts(source) {
|
|
554
|
+
let ast;
|
|
555
|
+
try {
|
|
556
|
+
ast = parse(source);
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return '';
|
|
560
|
+
}
|
|
561
|
+
const lines = ['/* generated by vesk — do not edit */', ''];
|
|
562
|
+
const typeDeclNames = new Set();
|
|
563
|
+
for (const stmt of ast.body || []) {
|
|
564
|
+
if (isTypeDecl(stmt)) {
|
|
565
|
+
const name = stmt.id?.name || stmt.declaration?.id?.name;
|
|
566
|
+
if (name)
|
|
567
|
+
typeDeclNames.add(name);
|
|
568
|
+
lines.push(source.slice(stmt.start, stmt.end), '');
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
for (const stmt of ast.body || []) {
|
|
572
|
+
if (stmt.type === 'ImportDeclaration') {
|
|
573
|
+
const src = stmt.source?.value;
|
|
574
|
+
if (typeof src === 'string' && !src.endsWith('.css')) {
|
|
575
|
+
lines.push(source.slice(stmt.start, stmt.end), '');
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
for (const { node, defaultExport } of getComponents(ast)) {
|
|
580
|
+
const name = node.id?.name;
|
|
581
|
+
if (!name)
|
|
582
|
+
continue;
|
|
583
|
+
const propsType = getPropsType(node.params, source);
|
|
584
|
+
const typeName = `${name}Props`;
|
|
585
|
+
const hasType = propsType !== null && propsType !== undefined;
|
|
586
|
+
const collision = typeDeclNames.has(typeName);
|
|
587
|
+
const signature = hasType
|
|
588
|
+
? (collision ? propsType : typeName) + ' & { children?: unknown }'
|
|
589
|
+
: 'any';
|
|
590
|
+
if (hasType && !collision) {
|
|
591
|
+
lines.push(`export type ${typeName} = ${propsType};`, '');
|
|
592
|
+
}
|
|
593
|
+
else if (!hasType && !collision) {
|
|
594
|
+
lines.push(`export type ${typeName} = any;`, '');
|
|
595
|
+
}
|
|
596
|
+
if (defaultExport) {
|
|
597
|
+
lines.push(`export default function ${name}(props: ${signature}): unknown;`, '');
|
|
598
|
+
}
|
|
599
|
+
else {
|
|
600
|
+
lines.push(`export declare function ${name}(props: ${signature}): unknown;`, '');
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return lines.join('\n');
|
|
604
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vesk/compiler",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./src/*": "./dist/*.js",
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').cpSync('src/acorn-ts-plugin', 'dist/acorn-ts-plugin', { recursive: true })\"",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@sveltejs/acorn-typescript": "^1.0.0",
|
|
23
|
+
"acorn": "^8.15.0",
|
|
24
|
+
"esrap": "^2.3.0",
|
|
25
|
+
"is-reference": "^3.0.0",
|
|
26
|
+
"source-map": "^0.7.4",
|
|
27
|
+
"zimmerframe": "^1.1.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5.8.0"
|
|
31
|
+
},
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"author": "emeraldlinks",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/emeraldlinks/veskTs.git"
|
|
37
|
+
},
|
|
38
|
+
"homepage": "https://github.com/emeraldlinks/veskTs#readme",
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/emeraldlinks/veskTs/issues"
|
|
41
|
+
},
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=20.0.0"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"README.md"
|
|
48
|
+
],
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|