tosijs-ui 1.6.21 → 1.7.0-beta.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/CHANGELOG.md +132 -0
- package/bin/make-icon-data.js +0 -0
- package/dist/code-editor-cm.js +41 -17
- package/dist/code-editor.d.ts +32 -2
- package/dist/code-editor.js +192 -25
- package/dist/doc-system/site/bundle-guard.d.ts +50 -0
- package/dist/doc-system/site/bundle-guard.js +111 -0
- package/dist/doc-system/site/check-examples.js +11 -3
- package/dist/doc-system/site/dev-server.js +97 -7
- package/dist/doc-system/site/epub-cli.d.ts +1 -0
- package/dist/doc-system/site/epub-cli.js +22 -0
- package/dist/doc-system/site/epub.js +34 -9
- package/dist/doc-system/site/make-llms-txt.js +1 -1
- package/dist/doc-system/site/orchestrator.js +139 -53
- package/dist/doc-system/site/site-config.d.ts +10 -0
- package/dist/iife.js +78 -71
- package/dist/iife.js.map +8 -8
- package/dist/live-example/code-transform.d.ts +39 -0
- package/dist/live-example/code-transform.js +305 -4
- package/dist/live-example/component.d.ts +10 -0
- package/dist/live-example/component.js +105 -21
- package/dist/live-example/execution.d.ts +16 -0
- package/dist/live-example/execution.js +29 -8
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/llms.txt +1 -1
- package/package.json +5 -6
|
@@ -18,6 +18,45 @@ export declare function contextVarName(key: string): string;
|
|
|
18
18
|
* the AsyncFunction body.
|
|
19
19
|
*/
|
|
20
20
|
export declare function rewriteImports(code: string, contextKeys: string[]): string;
|
|
21
|
+
/**
|
|
22
|
+
* Blank out the CONTENTS of strings, template literals, comments and regex literals,
|
|
23
|
+
* preserving length and newlines so every index still lines up with the original.
|
|
24
|
+
*
|
|
25
|
+
* The scanner below counts brackets and matches `^const` over raw text, which made it
|
|
26
|
+
* wrong in two ways that both failed silently:
|
|
27
|
+
*
|
|
28
|
+
* const label = 'Items (' → an unbalanced '(' inside a string ran the depth
|
|
29
|
+
* counter away, swallowing the REST OF THE FILE, so
|
|
30
|
+
* every later binding vanished from completions.
|
|
31
|
+
* `const in a template` → matched at column 0 inside a template literal and
|
|
32
|
+
* yielded the name `in`, and the epilogue then emitted
|
|
33
|
+
* `try{__tosiScope.in=in}` — a PARSE error, which no
|
|
34
|
+
* try/catch can absorb, killing the whole example.
|
|
35
|
+
*
|
|
36
|
+
* Identifiers never live inside literals, so masking loses nothing we want.
|
|
37
|
+
*/
|
|
38
|
+
export declare function maskLiterals(code: string): string;
|
|
39
|
+
/**
|
|
40
|
+
* Best-effort list of the identifiers a snippet binds at the TOP level (column 0, so
|
|
41
|
+
* genuinely function-scope, not inside a block). Handles `const/let/var` — including
|
|
42
|
+
* MULTI-LINE destructuring, MULTIPLE declarators (`const a = 1, b = 2`), nested
|
|
43
|
+
* patterns, aliases, defaults and rest — plus `function`/`class` declarations.
|
|
44
|
+
*
|
|
45
|
+
* This is a scanner, not a parse. tjs-lang already owns an acorn-based
|
|
46
|
+
* `collectScopeSymbols()` that does this properly, but it has no public export
|
|
47
|
+
* (filed upstream); when it lands, drive this off it. Meanwhile imperfect is safe:
|
|
48
|
+
* the capture epilogue guards every name individually, so an over- or under-match
|
|
49
|
+
* degrades completions rather than breaking the example.
|
|
50
|
+
*/
|
|
51
|
+
export declare function extractTopLevelBindingNames(code: string): string[];
|
|
52
|
+
/**
|
|
53
|
+
* Build an epilogue that captures the values of `names` (as they stand at the end
|
|
54
|
+
* of a run) into `captureVar(scopeObject)`. Each binding is read behind its own
|
|
55
|
+
* try/catch so a stale/over-matched name can't abort the whole capture, and the
|
|
56
|
+
* whole thing is a trailing block so it never shadows user code. Returns '' when
|
|
57
|
+
* there's nothing to capture.
|
|
58
|
+
*/
|
|
59
|
+
export declare function buildScopeCapture(names: string[], captureVar: string): string;
|
|
21
60
|
/**
|
|
22
61
|
* Execute code as an async function with injected context
|
|
23
62
|
*/
|
|
@@ -41,6 +41,302 @@ export function rewriteImports(code, contextKeys) {
|
|
|
41
41
|
}
|
|
42
42
|
return result;
|
|
43
43
|
}
|
|
44
|
+
const OPEN = '([{';
|
|
45
|
+
const CLOSE = ')]}';
|
|
46
|
+
/** Index of the first `ch` at bracket depth 0, or -1. */
|
|
47
|
+
function indexAtDepth0(src, ch) {
|
|
48
|
+
let depth = 0;
|
|
49
|
+
for (let i = 0; i < src.length; i++) {
|
|
50
|
+
const c = src[i];
|
|
51
|
+
if (OPEN.includes(c))
|
|
52
|
+
depth++;
|
|
53
|
+
else if (CLOSE.includes(c))
|
|
54
|
+
depth--;
|
|
55
|
+
else if (c === ch && depth === 0) {
|
|
56
|
+
if (ch === '=') {
|
|
57
|
+
// Only a bare assignment counts — skip ==, =>, <=, >=, !=.
|
|
58
|
+
const next = src[i + 1];
|
|
59
|
+
const prev = src[i - 1];
|
|
60
|
+
if (next === '=' || next === '>' || (prev && '=!<>'.includes(prev)))
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
return i;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return -1;
|
|
67
|
+
}
|
|
68
|
+
/** Split on `,` at bracket depth 0, so nested commas (`{ a: { b, c } }`) don't split. */
|
|
69
|
+
function splitAtDepth0(src) {
|
|
70
|
+
const parts = [];
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let start = 0;
|
|
73
|
+
for (let i = 0; i < src.length; i++) {
|
|
74
|
+
const c = src[i];
|
|
75
|
+
if (OPEN.includes(c))
|
|
76
|
+
depth++;
|
|
77
|
+
else if (CLOSE.includes(c))
|
|
78
|
+
depth--;
|
|
79
|
+
else if (c === ',' && depth === 0) {
|
|
80
|
+
parts.push(src.slice(start, i));
|
|
81
|
+
start = i + 1;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
parts.push(src.slice(start));
|
|
85
|
+
return parts.filter((p) => p.trim() !== '');
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Blank out the CONTENTS of strings, template literals, comments and regex literals,
|
|
89
|
+
* preserving length and newlines so every index still lines up with the original.
|
|
90
|
+
*
|
|
91
|
+
* The scanner below counts brackets and matches `^const` over raw text, which made it
|
|
92
|
+
* wrong in two ways that both failed silently:
|
|
93
|
+
*
|
|
94
|
+
* const label = 'Items (' → an unbalanced '(' inside a string ran the depth
|
|
95
|
+
* counter away, swallowing the REST OF THE FILE, so
|
|
96
|
+
* every later binding vanished from completions.
|
|
97
|
+
* `const in a template` → matched at column 0 inside a template literal and
|
|
98
|
+
* yielded the name `in`, and the epilogue then emitted
|
|
99
|
+
* `try{__tosiScope.in=in}` — a PARSE error, which no
|
|
100
|
+
* try/catch can absorb, killing the whole example.
|
|
101
|
+
*
|
|
102
|
+
* Identifiers never live inside literals, so masking loses nothing we want.
|
|
103
|
+
*/
|
|
104
|
+
export function maskLiterals(code) {
|
|
105
|
+
const out = code.split('');
|
|
106
|
+
const blank = (from, to) => {
|
|
107
|
+
for (let i = from; i < to && i < out.length; i++) {
|
|
108
|
+
if (out[i] !== '\n')
|
|
109
|
+
out[i] = ' ';
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
// A `/` starts a regex (not division) only where a value can't precede it.
|
|
113
|
+
const regexAllowedAfter = /[=(,:[!&|?{};+\-*%~^<>]$/;
|
|
114
|
+
let i = 0;
|
|
115
|
+
while (i < code.length) {
|
|
116
|
+
const c = code[i];
|
|
117
|
+
const next = code[i + 1];
|
|
118
|
+
if (c === '/' && next === '/') {
|
|
119
|
+
const end = code.indexOf('\n', i);
|
|
120
|
+
blank(i, end === -1 ? code.length : end);
|
|
121
|
+
i = end === -1 ? code.length : end;
|
|
122
|
+
}
|
|
123
|
+
else if (c === '/' && next === '*') {
|
|
124
|
+
const end = code.indexOf('*/', i + 2);
|
|
125
|
+
blank(i, end === -1 ? code.length : end + 2);
|
|
126
|
+
i = end === -1 ? code.length : end + 2;
|
|
127
|
+
}
|
|
128
|
+
else if (c === "'" || c === '"') {
|
|
129
|
+
let j = i + 1;
|
|
130
|
+
while (j < code.length && code[j] !== c) {
|
|
131
|
+
if (code[j] === '\\')
|
|
132
|
+
j++;
|
|
133
|
+
if (code[j] === '\n')
|
|
134
|
+
break; // unterminated — don't run away
|
|
135
|
+
j++;
|
|
136
|
+
}
|
|
137
|
+
blank(i + 1, j); // keep the quotes — see NB below
|
|
138
|
+
i = j + 1;
|
|
139
|
+
}
|
|
140
|
+
else if (c === '`') {
|
|
141
|
+
// Template literal: skip to the matching backtick, stepping over ${…} (which can
|
|
142
|
+
// nest braces and contain more backticks).
|
|
143
|
+
let j = i + 1;
|
|
144
|
+
let braces = 0;
|
|
145
|
+
while (j < code.length) {
|
|
146
|
+
if (code[j] === '\\')
|
|
147
|
+
j += 2;
|
|
148
|
+
else if (braces === 0 && code[j] === '`')
|
|
149
|
+
break;
|
|
150
|
+
else if (code[j] === '$' && code[j + 1] === '{') {
|
|
151
|
+
braces++;
|
|
152
|
+
j += 2;
|
|
153
|
+
}
|
|
154
|
+
else if (braces > 0 && code[j] === '{') {
|
|
155
|
+
braces++;
|
|
156
|
+
j++;
|
|
157
|
+
}
|
|
158
|
+
else if (braces > 0 && code[j] === '}') {
|
|
159
|
+
braces--;
|
|
160
|
+
j++;
|
|
161
|
+
}
|
|
162
|
+
else
|
|
163
|
+
j++;
|
|
164
|
+
}
|
|
165
|
+
blank(i + 1, j); // keep the backticks
|
|
166
|
+
i = j + 1;
|
|
167
|
+
}
|
|
168
|
+
else if (c === '/') {
|
|
169
|
+
const before = code.slice(0, i).replace(/\s+$/, '');
|
|
170
|
+
if (before === '' || regexAllowedAfter.test(before)) {
|
|
171
|
+
let j = i + 1;
|
|
172
|
+
let inClass = false;
|
|
173
|
+
while (j < code.length) {
|
|
174
|
+
if (code[j] === '\\')
|
|
175
|
+
j++;
|
|
176
|
+
else if (code[j] === '[')
|
|
177
|
+
inClass = true;
|
|
178
|
+
else if (code[j] === ']')
|
|
179
|
+
inClass = false;
|
|
180
|
+
else if (code[j] === '/' && !inClass)
|
|
181
|
+
break;
|
|
182
|
+
else if (code[j] === '\n')
|
|
183
|
+
break; // unterminated — it was division after all
|
|
184
|
+
j++;
|
|
185
|
+
}
|
|
186
|
+
blank(i + 1, j); // keep the slashes
|
|
187
|
+
i = j + 1;
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
i++; // division
|
|
191
|
+
}
|
|
192
|
+
else
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
return out.join('');
|
|
196
|
+
}
|
|
197
|
+
// Reserved words are identifier-SHAPED but can't be read as one: emitting
|
|
198
|
+
// `try{__tosiScope.in=in}` is a SyntaxError that no try/catch absorbs, and it takes the
|
|
199
|
+
// whole example down with it. Masking should already prevent these from being matched;
|
|
200
|
+
// this is the belt to that suspenders.
|
|
201
|
+
const RESERVED = new Set(('break case catch class const continue debugger default delete do else enum export ' +
|
|
202
|
+
'extends false finally for function if import in instanceof new null return super ' +
|
|
203
|
+
'switch this throw true try typeof var void while with yield let static implements ' +
|
|
204
|
+
'interface package private protected public await').split(' '));
|
|
205
|
+
const isCapturableName = (name) => /^[A-Za-z_$][\w$]*$/.test(name) && !RESERVED.has(name);
|
|
206
|
+
/** Pull the bound identifiers out of one declaration's left-hand side. */
|
|
207
|
+
function patternNames(lhs) {
|
|
208
|
+
lhs = lhs.trim();
|
|
209
|
+
if (!lhs)
|
|
210
|
+
return [];
|
|
211
|
+
if (/^[{[]/.test(lhs)) {
|
|
212
|
+
// Object/array destructuring. Split depth-0 commas (NOT a naive `split(',')`,
|
|
213
|
+
// which shreds `{ a: { b, c } }`), then per element: drop a rest `...`, take the
|
|
214
|
+
// TARGET of `key: target` (the target is what's bound, and may itself be a
|
|
215
|
+
// pattern), drop any `= default`, and recurse into nested patterns.
|
|
216
|
+
const names = [];
|
|
217
|
+
for (let part of splitAtDepth0(lhs.slice(1, -1))) {
|
|
218
|
+
part = part.trim().replace(/^\.\.\./, '');
|
|
219
|
+
if (!part)
|
|
220
|
+
continue;
|
|
221
|
+
const colon = indexAtDepth0(part, ':');
|
|
222
|
+
if (colon !== -1)
|
|
223
|
+
part = part.slice(colon + 1).trim();
|
|
224
|
+
const eq = indexAtDepth0(part, '=');
|
|
225
|
+
if (eq !== -1)
|
|
226
|
+
part = part.slice(0, eq).trim();
|
|
227
|
+
if (/^[{[]/.test(part)) {
|
|
228
|
+
names.push(...patternNames(part));
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const m = part.match(/^[A-Za-z_$][\w$]*/);
|
|
232
|
+
if (m)
|
|
233
|
+
names.push(m[0]);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return names;
|
|
237
|
+
}
|
|
238
|
+
const m = lhs.match(/^[A-Za-z_$][\w$]*/);
|
|
239
|
+
return m ? [m[0]] : [];
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Read a `const/let/var` declaration starting at `from` — across newlines, so a
|
|
243
|
+
* wrapped destructure (`const {\n app,\n} = …`) is captured whole. Ends at a depth-0
|
|
244
|
+
* `;`, or at a newline where the statement is plainly complete (the previous
|
|
245
|
+
* non-space char isn't an operator/comma continuing it). Anything unbalanced keeps
|
|
246
|
+
* reading, because depth > 0.
|
|
247
|
+
*/
|
|
248
|
+
function readDeclaration(code, from) {
|
|
249
|
+
let depth = 0;
|
|
250
|
+
let i = from;
|
|
251
|
+
for (; i < code.length; i++) {
|
|
252
|
+
const c = code[i];
|
|
253
|
+
if (OPEN.includes(c))
|
|
254
|
+
depth++;
|
|
255
|
+
else if (CLOSE.includes(c))
|
|
256
|
+
depth--;
|
|
257
|
+
else if (depth === 0 && c === ';')
|
|
258
|
+
break;
|
|
259
|
+
else if (depth === 0 && c === '\n') {
|
|
260
|
+
const soFar = code.slice(from, i).trimEnd();
|
|
261
|
+
const last = soFar[soFar.length - 1];
|
|
262
|
+
if (last !== undefined && !',=+-*/%&|?:<>!~^'.includes(last))
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// FAIL SAFE. Running to EOF with depth > 0 means the brackets never balanced, and
|
|
267
|
+
// consuming the tail would silently drop every binding after this point (the old
|
|
268
|
+
// behaviour, and the whole bug). Masking should make this unreachable; if it happens
|
|
269
|
+
// anyway, give up on THIS declaration only — take the first line and let the scan
|
|
270
|
+
// resume — rather than swallowing the rest of the example.
|
|
271
|
+
if (i >= code.length && depth > 0) {
|
|
272
|
+
const eol = code.indexOf('\n', from);
|
|
273
|
+
return code.slice(from, eol === -1 ? code.length : eol);
|
|
274
|
+
}
|
|
275
|
+
return code.slice(from, i);
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Best-effort list of the identifiers a snippet binds at the TOP level (column 0, so
|
|
279
|
+
* genuinely function-scope, not inside a block). Handles `const/let/var` — including
|
|
280
|
+
* MULTI-LINE destructuring, MULTIPLE declarators (`const a = 1, b = 2`), nested
|
|
281
|
+
* patterns, aliases, defaults and rest — plus `function`/`class` declarations.
|
|
282
|
+
*
|
|
283
|
+
* This is a scanner, not a parse. tjs-lang already owns an acorn-based
|
|
284
|
+
* `collectScopeSymbols()` that does this properly, but it has no public export
|
|
285
|
+
* (filed upstream); when it lands, drive this off it. Meanwhile imperfect is safe:
|
|
286
|
+
* the capture epilogue guards every name individually, so an over- or under-match
|
|
287
|
+
* degrades completions rather than breaking the example.
|
|
288
|
+
*/
|
|
289
|
+
export function extractTopLevelBindingNames(code) {
|
|
290
|
+
// Scan MASKED source: literals and comments are blanked (same length, same newlines),
|
|
291
|
+
// so a stray bracket or a `const` inside a string can't derail the scan. Indices still
|
|
292
|
+
// line up with `code`, and identifiers only ever live outside literals.
|
|
293
|
+
const masked = maskLiterals(code);
|
|
294
|
+
const names = new Set();
|
|
295
|
+
const declRe = /^(?:export\s+)?(const|let|var|function\*?|class)\s+/gm;
|
|
296
|
+
let m;
|
|
297
|
+
while ((m = declRe.exec(masked)) !== null) {
|
|
298
|
+
const kind = m[1];
|
|
299
|
+
const from = m.index + m[0].length;
|
|
300
|
+
if (kind === 'class' || kind.startsWith('function')) {
|
|
301
|
+
const id = masked.slice(from).match(/^[A-Za-z_$][\w$]*/);
|
|
302
|
+
if (id)
|
|
303
|
+
names.add(id[0]);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const decl = readDeclaration(masked, from);
|
|
307
|
+
// Each depth-0 comma starts another declarator (`const a = 1, b = 2`).
|
|
308
|
+
for (const declarator of splitAtDepth0(decl)) {
|
|
309
|
+
const eq = indexAtDepth0(declarator, '=');
|
|
310
|
+
const lhs = eq === -1 ? declarator : declarator.slice(0, eq);
|
|
311
|
+
for (const n of patternNames(lhs))
|
|
312
|
+
names.add(n);
|
|
313
|
+
}
|
|
314
|
+
// Don't rescan inside the declaration we just consumed.
|
|
315
|
+
declRe.lastIndex = from + decl.length;
|
|
316
|
+
}
|
|
317
|
+
return [...names].filter(isCapturableName);
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Build an epilogue that captures the values of `names` (as they stand at the end
|
|
321
|
+
* of a run) into `captureVar(scopeObject)`. Each binding is read behind its own
|
|
322
|
+
* try/catch so a stale/over-matched name can't abort the whole capture, and the
|
|
323
|
+
* whole thing is a trailing block so it never shadows user code. Returns '' when
|
|
324
|
+
* there's nothing to capture.
|
|
325
|
+
*/
|
|
326
|
+
export function buildScopeCapture(names, captureVar) {
|
|
327
|
+
// Filter HERE too, not just at extraction. Every other guard in this file is a
|
|
328
|
+
// runtime try/catch, but an unreadable name (`in`, `class`, `2bad`) makes the
|
|
329
|
+
// epilogue itself unparseable — and a SyntaxError takes the whole example down
|
|
330
|
+
// before a single line of it runs. This is the last line of defence, so it must not
|
|
331
|
+
// trust its caller.
|
|
332
|
+
const capturable = names.filter(isCapturableName);
|
|
333
|
+
if (capturable.length === 0)
|
|
334
|
+
return '';
|
|
335
|
+
const assigns = capturable
|
|
336
|
+
.map((n) => `try{__tosiScope.${n}=${n}}catch(_e){}`)
|
|
337
|
+
.join('');
|
|
338
|
+
return `\n;{const __tosiScope={};${assigns}${captureVar}(__tosiScope)}`;
|
|
339
|
+
}
|
|
44
340
|
/**
|
|
45
341
|
* Execute code as an async function with injected context
|
|
46
342
|
*/
|
|
@@ -61,7 +357,7 @@ export async function executeCode(code, context, transform) {
|
|
|
61
357
|
// self-contained) and is loaded only for `ts` examples; from-ts in turn fetches
|
|
62
358
|
// the TypeScript compiler lazily at runtime, so tsc is never pulled in until a TS
|
|
63
359
|
// example actually transforms. Pinned to match the dev dep.
|
|
64
|
-
const TJS_VERSION = '0.9.
|
|
360
|
+
const TJS_VERSION = '0.9.1';
|
|
65
361
|
// Where to fetch a tjs-lang browser bundle from, in priority order:
|
|
66
362
|
// 1. SAME-ORIGIN — the doc-site build copies the bundles next to the iife and
|
|
67
363
|
// sets `__TJS_LOCAL_BASE`, so the transpiler ships in lockstep with the page,
|
|
@@ -71,7 +367,8 @@ const TJS_VERSION = '0.9.0';
|
|
|
71
367
|
// it (minutes–hours) and any one can blip, so we try several. The module-cache
|
|
72
368
|
// service worker caches all three hosts.
|
|
73
369
|
function bundleUrls(file) {
|
|
74
|
-
const localBase = globalThis
|
|
370
|
+
const localBase = globalThis
|
|
371
|
+
.__TJS_LOCAL_BASE;
|
|
75
372
|
return [
|
|
76
373
|
...(typeof localBase === 'string' ? [`${localBase}${file}`] : []),
|
|
77
374
|
`https://cdn.jsdelivr.net/npm/tjs-lang@${TJS_VERSION}/dist/${file}`,
|
|
@@ -119,7 +416,9 @@ async function loadTjsTestApiImpl() {
|
|
|
119
416
|
for (const load of sources) {
|
|
120
417
|
try {
|
|
121
418
|
const m = (await load());
|
|
122
|
-
if (m &&
|
|
419
|
+
if (m &&
|
|
420
|
+
typeof m.extractTests === 'function' &&
|
|
421
|
+
typeof m.testUtils === 'string') {
|
|
123
422
|
return { extractTests: m.extractTests, testUtils: m.testUtils };
|
|
124
423
|
}
|
|
125
424
|
}
|
|
@@ -202,7 +501,9 @@ export async function loadTransform(dialect = 'js') {
|
|
|
202
501
|
if (dialect === 'ts') {
|
|
203
502
|
// async: fromTS lazy-loads the TypeScript compiler on first use.
|
|
204
503
|
return (async () => {
|
|
205
|
-
const tjsSource = fromTS
|
|
504
|
+
const tjsSource = fromTS
|
|
505
|
+
? (await fromTS(code, { emitTJS: true })).code
|
|
506
|
+
: code;
|
|
206
507
|
const result = {
|
|
207
508
|
code: tjs(tjsSource, { dialect: 'tjs', runTests: false }).code,
|
|
208
509
|
};
|
|
@@ -227,6 +227,7 @@ export declare class LiveExample extends Component<ExampleParts> {
|
|
|
227
227
|
prefix: string;
|
|
228
228
|
storageKey: string;
|
|
229
229
|
context: ExampleContext;
|
|
230
|
+
private capturedScope;
|
|
230
231
|
uuid: string;
|
|
231
232
|
remoteId: string;
|
|
232
233
|
private remoteSync?;
|
|
@@ -262,6 +263,15 @@ export declare class LiveExample extends Component<ExampleParts> {
|
|
|
262
263
|
private renderTjsTests;
|
|
263
264
|
private computeGeneratedJs;
|
|
264
265
|
private ensureProductTabs;
|
|
266
|
+
private captureScope;
|
|
267
|
+
/**
|
|
268
|
+
* Live bindings for tjs runtime-value autocomplete: the example's context modules
|
|
269
|
+
* (keyed by the identifier the rewritten code uses, e.g. `tosijs`, `tosijsui`),
|
|
270
|
+
* the currently-rendered `preview` element, and the latest run's top-level locals
|
|
271
|
+
* (so `const app = tosi(…)` gives real `app.` / `app.items.` completions, proxy
|
|
272
|
+
* members and all). Read lazily on each completion, so it reflects the latest run.
|
|
273
|
+
*/
|
|
274
|
+
private liveBindings;
|
|
265
275
|
updateUndo: () => void;
|
|
266
276
|
private updateTestResultsVisibility;
|
|
267
277
|
undo: () => void;
|
|
@@ -162,10 +162,22 @@ function step(! px: Float32Array, py: Float32Array, vx: Float32Array, vy: Float3
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
165
|
+
// 100k particles, not 6k. At 6k each step took ~0.02ms — right at the resolution floor
|
|
166
|
+
// of performance.now(), so the readout was mostly measuring the timer. At this size both
|
|
167
|
+
// paths land in the hundreds of microseconds and the comparison actually means something.
|
|
168
|
+
const W = 480, H = 300, cols = 400, rows = 250, N = cols * rows
|
|
169
|
+
|
|
170
|
+
// Allocate the particle arrays INSIDE wasm memory. This is the whole ballgame: if a
|
|
171
|
+
// typed array's buffer isn't the wasm memory, every call copies it in and out again —
|
|
172
|
+
// six 6,000-float arrays per frame — and you end up timing memcpy, not SIMD. (With
|
|
173
|
+
// plain `new Float32Array(N)` this demo measured ~4x SLOWER than its own JS fallback.)
|
|
174
|
+
// `wasmBuffer` is a bump allocator handing out views into the shared wasm memory, so
|
|
175
|
+
// the wrapper passes a byte offset and copies nothing. Fall back gracefully if the
|
|
176
|
+
// wasm block didn't compile, so the JS twin still runs.
|
|
177
|
+
const f32 = (n) => globalThis.wasmBuffer ? wasmBuffer(Float32Array, n) : new Float32Array(n)
|
|
178
|
+
const px = f32(N), py = f32(N)
|
|
179
|
+
const vx = f32(N), vy = f32(N)
|
|
180
|
+
const hx = f32(N), hy = f32(N)
|
|
169
181
|
for (let i = 0; i < N; i++) {
|
|
170
182
|
const c = i % cols, r = (i / cols) | 0
|
|
171
183
|
hx[i] = (c + 0.5) * W / cols
|
|
@@ -180,6 +192,15 @@ canvas.height = H
|
|
|
180
192
|
canvas.style.cssText = 'width:100%;max-width:480px;border-radius:8px;display:block;background:#0b0e14;touch-action:none;cursor:crosshair'
|
|
181
193
|
const ctx = canvas.getContext('2d')
|
|
182
194
|
|
|
195
|
+
// Plot straight into an ImageData buffer (one 32-bit store per particle) instead of
|
|
196
|
+
// 100k fillRect() calls — otherwise the DRAW would dominate and we'd be benchmarking
|
|
197
|
+
// canvas, not the kernel.
|
|
198
|
+
const img = ctx.createImageData(W, H)
|
|
199
|
+
const pix = new Uint32Array(img.data.buffer)
|
|
200
|
+
const BG = 0xff140e0b // #0b0e14, little-endian ABGR
|
|
201
|
+
const TEAL = 0xffc5d14f // #4fd1c5
|
|
202
|
+
const AMBER = 0xff55adf6 // #f6ad55
|
|
203
|
+
|
|
183
204
|
let tx = W / 2, ty = H / 2, idle = 0, frame = 0
|
|
184
205
|
canvas.addEventListener('pointermove', (e) => {
|
|
185
206
|
const r = canvas.getBoundingClientRect()
|
|
@@ -188,21 +209,35 @@ canvas.addEventListener('pointermove', (e) => {
|
|
|
188
209
|
idle = 0
|
|
189
210
|
})
|
|
190
211
|
|
|
191
|
-
// tjs
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
212
|
+
// tjs-lang 0.9.1+ gates every wasm call on `globalThis.__tjs_wasm_enabled`, so this
|
|
213
|
+
// flips the kernel between WASM and its JS twin without touching tjs internals.
|
|
214
|
+
// (Don't reach for `__tjs_wasm_0` — it's private AND index-keyed per transpile, so
|
|
215
|
+
// a second wasm example on the page registers the same name and you'd clobber it.)
|
|
216
|
+
let useWasm = true
|
|
195
217
|
const btn = document.createElement('button')
|
|
196
218
|
btn.style.cssText = 'margin:8px 0;padding:6px 12px;border-radius:6px;cursor:pointer'
|
|
197
219
|
const readout = document.createElement('p')
|
|
220
|
+
let acc = 0, frames = 0
|
|
198
221
|
function applyMode() {
|
|
199
|
-
|
|
200
|
-
|
|
222
|
+
globalThis.__tjs_wasm_enabled = useWasm
|
|
223
|
+
acc = 0; frames = 0 // don't average across a mode switch
|
|
201
224
|
btn.textContent = useWasm ? '⚡ WebAssembly SIMD — click for JavaScript' : '🐢 JavaScript — click for WASM SIMD'
|
|
202
225
|
}
|
|
203
226
|
btn.onclick = () => { useWasm = !useWasm; applyMode() }
|
|
204
227
|
|
|
205
|
-
|
|
228
|
+
// Warm BOTH paths before timing anything. The JS twin never executes until you click
|
|
229
|
+
// over to it, so an un-warmed comparison times cold, un-JITed JavaScript and flatters
|
|
230
|
+
// WASM (it read ~2x too good). A warmed V8 does this kernel in ~0.024 ms/step; the
|
|
231
|
+
// zero-copy SIMD kernel does ~0.015 — a real but modest ~1.6x, which is the honest
|
|
232
|
+
// number this demo should show.
|
|
233
|
+
function warmUp() {
|
|
234
|
+
for (const on of [false, true]) {
|
|
235
|
+
globalThis.__tjs_wasm_enabled = on
|
|
236
|
+
for (let i = 0; i < 150; i++) step(px, py, vx, vy, hx, hy, N, W / 2, H / 2)
|
|
237
|
+
}
|
|
238
|
+
// the warm-up perturbed the particles — put them back on the grid
|
|
239
|
+
for (let i = 0; i < N; i++) { px[i] = hx[i]; py[i] = hy[i]; vx[i] = 0; vy[i] = 0 }
|
|
240
|
+
}
|
|
206
241
|
function loop() {
|
|
207
242
|
if (!document.body.contains(canvas)) return // example removed → stop
|
|
208
243
|
frame++
|
|
@@ -215,10 +250,15 @@ function loop() {
|
|
|
215
250
|
step(px, py, vx, vy, hx, hy, N, tx, ty)
|
|
216
251
|
acc += performance.now() - t0
|
|
217
252
|
frames++
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
for (let i = 0; i < N; i++)
|
|
253
|
+
|
|
254
|
+
pix.fill(BG)
|
|
255
|
+
const dot = useWasm ? TEAL : AMBER
|
|
256
|
+
for (let i = 0; i < N; i++) {
|
|
257
|
+
const x = px[i] | 0, y = py[i] | 0
|
|
258
|
+
if (x >= 0 && x < W && y >= 0 && y < H) pix[y * W + x] = dot
|
|
259
|
+
}
|
|
260
|
+
ctx.putImageData(img, 0, 0)
|
|
261
|
+
|
|
222
262
|
if (frames >= 20) {
|
|
223
263
|
readout.textContent = N.toLocaleString() + ' particles · ' + (acc / frames).toFixed(3) +
|
|
224
264
|
' ms/step (' + (useWasm ? 'WASM SIMD' : 'JS') + ') · move the pointer'
|
|
@@ -229,12 +269,11 @@ function loop() {
|
|
|
229
269
|
}
|
|
230
270
|
|
|
231
271
|
;(async () => {
|
|
232
|
-
// The wasm bootstrap is async
|
|
233
|
-
// so
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
272
|
+
// The wasm bootstrap is async. tjs-lang 0.9.1+ exposes an awaitable ready signal,
|
|
273
|
+
// so we start only once the kernel is instantiated — otherwise the first frames
|
|
274
|
+
// silently run the JS fallback while the button claims "WebAssembly SIMD".
|
|
275
|
+
await globalThis.__tjs_wasm_ready?.()
|
|
276
|
+
warmUp()
|
|
238
277
|
applyMode()
|
|
239
278
|
requestAnimationFrame(loop)
|
|
240
279
|
})()
|
|
@@ -242,6 +281,17 @@ function loop() {
|
|
|
242
281
|
preview.append(canvas, btn, readout)
|
|
243
282
|
```
|
|
244
283
|
|
|
284
|
+
```test
|
|
285
|
+
// Guard the claim the button makes. A `wasm {}` block that fails to compile falls
|
|
286
|
+
// back to JS *silently*, so without this the demo can advertise "⚡ WebAssembly SIMD"
|
|
287
|
+
// while running the JS twin, and every test stays green.
|
|
288
|
+
test('the wasm kernel actually compiled (no silent fallback to JS)', async () => {
|
|
289
|
+
await globalThis.__tjs_wasm_ready?.()
|
|
290
|
+
expect(typeof globalThis.__tjs_wasm_ready).toBe('function')
|
|
291
|
+
expect(globalThis.__tjs_wasm_0).toBeTruthy()
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
245
295
|
## CSS Isolation with `iframe`
|
|
246
296
|
|
|
247
297
|
Add the `iframe` attribute to render the preview inside an iframe, giving the example
|
|
@@ -417,6 +467,9 @@ export class LiveExample extends Component {
|
|
|
417
467
|
prefix = 'lx';
|
|
418
468
|
storageKey = STORAGE_KEY;
|
|
419
469
|
context = {};
|
|
470
|
+
// The example's top-level locals from the latest run, captured in-run for tjs
|
|
471
|
+
// runtime-value autocomplete (see `liveBindings`). Populated via `onScope`.
|
|
472
|
+
capturedScope = {};
|
|
420
473
|
uuid = crypto.randomUUID();
|
|
421
474
|
remoteId = '';
|
|
422
475
|
remoteSync;
|
|
@@ -602,6 +655,11 @@ export class LiveExample extends Component {
|
|
|
602
655
|
// put the source editor in the dialect's mode — so a `tjs` example gets
|
|
603
656
|
// first-class tjs editing (highlighting + autocomplete), `ts` gets TypeScript.
|
|
604
657
|
this.parts.js.setAttribute('name', this.dialect);
|
|
658
|
+
// Runtime-value autocomplete: give the tjs completion source the example's live
|
|
659
|
+
// bindings (context modules + the rendered preview) so it can suggest their REAL
|
|
660
|
+
// members — including tosijs proxy members that static analysis can't see. Set
|
|
661
|
+
// before `.mode` so the tjs extension loads with the config in one shot.
|
|
662
|
+
this.parts.js.tjsAutocomplete = { getLiveBindings: () => this.liveBindings() };
|
|
605
663
|
this.parts.js.mode = this.dialect;
|
|
606
664
|
this.jsOutEditor = codeEditor({
|
|
607
665
|
name: 'JS',
|
|
@@ -619,6 +677,30 @@ export class LiveExample extends Component {
|
|
|
619
677
|
this.jsOutEditor.value = this.lastGeneratedJs;
|
|
620
678
|
this.renderTjsTests();
|
|
621
679
|
}
|
|
680
|
+
// Capture the latest run's top-level locals (arrow property so `this` is bound
|
|
681
|
+
// when passed as execution's `onScope`).
|
|
682
|
+
captureScope = (scope) => {
|
|
683
|
+
this.capturedScope = scope;
|
|
684
|
+
};
|
|
685
|
+
/**
|
|
686
|
+
* Live bindings for tjs runtime-value autocomplete: the example's context modules
|
|
687
|
+
* (keyed by the identifier the rewritten code uses, e.g. `tosijs`, `tosijsui`),
|
|
688
|
+
* the currently-rendered `preview` element, and the latest run's top-level locals
|
|
689
|
+
* (so `const app = tosi(…)` gives real `app.` / `app.items.` completions, proxy
|
|
690
|
+
* members and all). Read lazily on each completion, so it reflects the latest run.
|
|
691
|
+
*/
|
|
692
|
+
liveBindings() {
|
|
693
|
+
const bindings = {};
|
|
694
|
+
for (const [key, value] of Object.entries(this.context)) {
|
|
695
|
+
bindings[contextVarName(key)] = value;
|
|
696
|
+
}
|
|
697
|
+
const preview = this.parts.example?.querySelector('.preview');
|
|
698
|
+
if (preview)
|
|
699
|
+
bindings.preview = preview;
|
|
700
|
+
// Run locals last so they win over same-named context entries.
|
|
701
|
+
Object.assign(bindings, this.capturedScope);
|
|
702
|
+
return bindings;
|
|
703
|
+
}
|
|
622
704
|
updateUndo = () => {
|
|
623
705
|
const { activeTab } = this;
|
|
624
706
|
const { undo, redo } = this.parts;
|
|
@@ -1201,6 +1283,7 @@ export class LiveExample extends Component {
|
|
|
1201
1283
|
exampleElement: example,
|
|
1202
1284
|
widgetsElement: exampleWidgets,
|
|
1203
1285
|
onError,
|
|
1286
|
+
onScope: this.captureScope,
|
|
1204
1287
|
});
|
|
1205
1288
|
}
|
|
1206
1289
|
else {
|
|
@@ -1214,6 +1297,7 @@ export class LiveExample extends Component {
|
|
|
1214
1297
|
styleElement: styleEl,
|
|
1215
1298
|
widgetsElement: exampleWidgets,
|
|
1216
1299
|
onError,
|
|
1300
|
+
onScope: this.captureScope,
|
|
1217
1301
|
});
|
|
1218
1302
|
}
|
|
1219
1303
|
if (this.persistToDom) {
|
|
@@ -15,7 +15,23 @@ export interface ExecutionOptions {
|
|
|
15
15
|
context: ExampleContext;
|
|
16
16
|
transform: TransformFn;
|
|
17
17
|
onError?: (error: Error) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Receives the example's top-level locals after a successful run, so tjs
|
|
20
|
+
* autocomplete can introspect the REAL values (e.g. a `const app = tosi(…)`
|
|
21
|
+
* proxy) the user just created. Captured in-run — no re-execution, so no
|
|
22
|
+
* doubled side effects.
|
|
23
|
+
*/
|
|
24
|
+
onScope?: (scope: Record<string, unknown>) => void;
|
|
18
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Append a scope-capture epilogue to already-transformed example code when a
|
|
28
|
+
* consumer wants the run's locals. Returns the (possibly unchanged) code plus the
|
|
29
|
+
* extra context entry to inject. The epilogue no-ops if the example binds nothing.
|
|
30
|
+
*/
|
|
31
|
+
export declare function withScopeCapture(transformedCode: string, onScope?: (scope: Record<string, unknown>) => void): {
|
|
32
|
+
code: string;
|
|
33
|
+
extraContext: Record<string, unknown>;
|
|
34
|
+
};
|
|
19
35
|
/**
|
|
20
36
|
* Execute code inline (directly in the page)
|
|
21
37
|
*/
|