@ternkit/tern 1.1.0 → 1.2.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/dist/goscript/index.svelte.d.ts +44 -0
- package/dist/goscript/index.svelte.js +204 -0
- package/dist/image/Image.svelte +32 -9
- package/dist/image/Image.svelte.d.ts +10 -2
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -1
- package/dist/runtime/api.d.ts +18 -0
- package/dist/runtime/api.js +55 -0
- package/dist/runtime/base.d.ts +2 -0
- package/dist/runtime/base.js +10 -0
- package/dist/runtime/realtime.svelte.d.ts +78 -0
- package/dist/runtime/realtime.svelte.js +163 -0
- package/dist/runtime/router.svelte.js +2 -0
- package/dist/server/email.d.ts +18 -0
- package/dist/server/email.js +34 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +3 -0
- package/dist/server/intl.js +1 -1
- package/dist/server/polyfills.d.ts +1 -2
- package/dist/server/polyfills.js +29 -2
- package/dist/server/url.d.ts +56 -0
- package/dist/server/url.js +594 -0
- package/dist/server/web.d.ts +74 -0
- package/dist/server/web.js +395 -0
- package/dist/vite/goscript.d.ts +12 -0
- package/dist/vite/goscript.js +226 -0
- package/dist/vite/index.d.ts +6 -0
- package/dist/vite/index.js +83 -4
- package/package.json +9 -5
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
// Web APIs that Svelte libraries commonly touch while rendering, for the
|
|
2
|
+
// server renderer. QuickJS implements only the language; Node, which runs
|
|
3
|
+
// SvelteKit's renderer, has all of these. Each is installed only where it is
|
|
4
|
+
// missing.
|
|
5
|
+
//
|
|
6
|
+
// Browser-only objects (window, document, navigator, location) are left
|
|
7
|
+
// undefined on purpose: libraries check for them to decide whether they are
|
|
8
|
+
// running in a browser.
|
|
9
|
+
// TextEncoder and TextDecoder (UTF-8).
|
|
10
|
+
export class TextEncoderPolyfill {
|
|
11
|
+
encoding = 'utf-8';
|
|
12
|
+
encode(input = '') {
|
|
13
|
+
const out = [];
|
|
14
|
+
for (const ch of String(input)) {
|
|
15
|
+
let c = ch.codePointAt(0);
|
|
16
|
+
if (c >= 0xd800 && c <= 0xdfff)
|
|
17
|
+
c = 0xfffd;
|
|
18
|
+
if (c < 0x80)
|
|
19
|
+
out.push(c);
|
|
20
|
+
else if (c < 0x800)
|
|
21
|
+
out.push(0xc0 | (c >> 6), 0x80 | (c & 63));
|
|
22
|
+
else if (c < 0x10000)
|
|
23
|
+
out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
24
|
+
else
|
|
25
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 63), 0x80 | ((c >> 6) & 63), 0x80 | (c & 63));
|
|
26
|
+
}
|
|
27
|
+
return new Uint8Array(out);
|
|
28
|
+
}
|
|
29
|
+
encodeInto(input, dest) {
|
|
30
|
+
const bytes = this.encode(input);
|
|
31
|
+
const n = Math.min(bytes.length, dest.length);
|
|
32
|
+
dest.set(bytes.subarray(0, n));
|
|
33
|
+
return {
|
|
34
|
+
read: n === bytes.length ? input.length : new TextDecoderPolyfill().decode(bytes.subarray(0, n)).length,
|
|
35
|
+
written: n
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export class TextDecoderPolyfill {
|
|
40
|
+
encoding;
|
|
41
|
+
fatal;
|
|
42
|
+
ignoreBOM;
|
|
43
|
+
constructor(label = 'utf-8', options = {}) {
|
|
44
|
+
const l = String(label).toLowerCase();
|
|
45
|
+
if (l !== 'utf-8' && l !== 'utf8' && l !== 'unicode-1-1-utf-8')
|
|
46
|
+
throw new RangeError(`TextDecoder: unsupported encoding "${label}"`);
|
|
47
|
+
this.encoding = 'utf-8';
|
|
48
|
+
this.fatal = !!options.fatal;
|
|
49
|
+
this.ignoreBOM = !!options.ignoreBOM;
|
|
50
|
+
}
|
|
51
|
+
decode(input) {
|
|
52
|
+
if (input === undefined)
|
|
53
|
+
return '';
|
|
54
|
+
const b = input instanceof ArrayBuffer ? new Uint8Array(input) : new Uint8Array(input.buffer, input.byteOffset, input.byteLength);
|
|
55
|
+
let out = '';
|
|
56
|
+
let i = !this.ignoreBOM && b[0] === 0xef && b[1] === 0xbb && b[2] === 0xbf ? 3 : 0;
|
|
57
|
+
const bad = () => {
|
|
58
|
+
if (this.fatal)
|
|
59
|
+
throw new TypeError('TextDecoder: invalid UTF-8');
|
|
60
|
+
out += '�';
|
|
61
|
+
};
|
|
62
|
+
while (i < b.length) {
|
|
63
|
+
const x = b[i];
|
|
64
|
+
let n;
|
|
65
|
+
let c;
|
|
66
|
+
if (x < 0x80)
|
|
67
|
+
[n, c] = [0, x];
|
|
68
|
+
else if (x >= 0xc2 && x < 0xe0)
|
|
69
|
+
[n, c] = [1, x & 31];
|
|
70
|
+
else if (x >= 0xe0 && x < 0xf0)
|
|
71
|
+
[n, c] = [2, x & 15];
|
|
72
|
+
else if (x >= 0xf0 && x < 0xf5)
|
|
73
|
+
[n, c] = [3, x & 7];
|
|
74
|
+
else {
|
|
75
|
+
bad();
|
|
76
|
+
i++;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// The WHATWG decoder: a malformed sequence becomes one U+FFFD
|
|
80
|
+
// covering its lead byte and the continuation bytes that were valid.
|
|
81
|
+
let k = 1;
|
|
82
|
+
for (; k <= n; k++) {
|
|
83
|
+
const y = b[i + k];
|
|
84
|
+
const lo = k === 1 ? (x === 0xe0 ? 0xa0 : x === 0xf0 ? 0x90 : 0x80) : 0x80;
|
|
85
|
+
const hi = k === 1 ? (x === 0xed ? 0x9f : x === 0xf4 ? 0x8f : 0xbf) : 0xbf;
|
|
86
|
+
if (y === undefined || y < lo || y > hi)
|
|
87
|
+
break;
|
|
88
|
+
c = (c << 6) | (y & 63);
|
|
89
|
+
}
|
|
90
|
+
if (k <= n) {
|
|
91
|
+
bad();
|
|
92
|
+
i += k;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
out += String.fromCodePoint(c);
|
|
96
|
+
i += n + 1;
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Event, CustomEvent and EventTarget.
|
|
102
|
+
export class EventPolyfill {
|
|
103
|
+
type;
|
|
104
|
+
bubbles;
|
|
105
|
+
cancelable;
|
|
106
|
+
composed;
|
|
107
|
+
timeStamp = Date.now();
|
|
108
|
+
defaultPrevented = false;
|
|
109
|
+
target = null;
|
|
110
|
+
currentTarget = null;
|
|
111
|
+
/** @internal */
|
|
112
|
+
_stop = false;
|
|
113
|
+
constructor(type, init = {}) {
|
|
114
|
+
this.type = String(type);
|
|
115
|
+
this.bubbles = !!init.bubbles;
|
|
116
|
+
this.cancelable = !!init.cancelable;
|
|
117
|
+
this.composed = !!init.composed;
|
|
118
|
+
}
|
|
119
|
+
preventDefault() {
|
|
120
|
+
if (this.cancelable)
|
|
121
|
+
this.defaultPrevented = true;
|
|
122
|
+
}
|
|
123
|
+
stopPropagation() { }
|
|
124
|
+
stopImmediatePropagation() {
|
|
125
|
+
this._stop = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export class CustomEventPolyfill extends EventPolyfill {
|
|
129
|
+
detail;
|
|
130
|
+
constructor(type, init = {}) {
|
|
131
|
+
super(type, init);
|
|
132
|
+
this.detail = init.detail ?? null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
export class EventTargetPolyfill {
|
|
136
|
+
#listeners = new Map();
|
|
137
|
+
addEventListener(type, fn, options) {
|
|
138
|
+
if (!fn)
|
|
139
|
+
return;
|
|
140
|
+
const opts = typeof options === 'object' ? options : {};
|
|
141
|
+
if (opts.signal?.aborted)
|
|
142
|
+
return;
|
|
143
|
+
const list = this.#listeners.get(type) ?? [];
|
|
144
|
+
if (list.some((l) => l.fn === fn))
|
|
145
|
+
return;
|
|
146
|
+
list.push({ fn, once: !!opts.once });
|
|
147
|
+
this.#listeners.set(type, list);
|
|
148
|
+
opts.signal?.addEventListener('abort', () => this.removeEventListener(type, fn));
|
|
149
|
+
}
|
|
150
|
+
removeEventListener(type, fn) {
|
|
151
|
+
const list = this.#listeners.get(type);
|
|
152
|
+
if (list)
|
|
153
|
+
this.#listeners.set(type, list.filter((l) => l.fn !== fn));
|
|
154
|
+
}
|
|
155
|
+
dispatchEvent(event) {
|
|
156
|
+
event.target = this;
|
|
157
|
+
event.currentTarget = this;
|
|
158
|
+
for (const l of [...(this.#listeners.get(event.type) ?? [])]) {
|
|
159
|
+
if (l.once)
|
|
160
|
+
this.removeEventListener(event.type, l.fn);
|
|
161
|
+
if (typeof l.fn === 'function')
|
|
162
|
+
l.fn.call(this, event);
|
|
163
|
+
else
|
|
164
|
+
l.fn.handleEvent(event);
|
|
165
|
+
if (event._stop)
|
|
166
|
+
break;
|
|
167
|
+
}
|
|
168
|
+
event.currentTarget = null;
|
|
169
|
+
return !event.defaultPrevented;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export class AbortSignalPolyfill extends EventTargetPolyfill {
|
|
173
|
+
aborted = false;
|
|
174
|
+
reason = undefined;
|
|
175
|
+
onabort = null;
|
|
176
|
+
throwIfAborted() {
|
|
177
|
+
if (this.aborted)
|
|
178
|
+
throw this.reason;
|
|
179
|
+
}
|
|
180
|
+
static abort(reason) {
|
|
181
|
+
const c = new AbortControllerPolyfill();
|
|
182
|
+
c.abort(reason);
|
|
183
|
+
return c.signal;
|
|
184
|
+
}
|
|
185
|
+
static timeout() {
|
|
186
|
+
return new AbortSignalPolyfill(); // timers never fire during a render
|
|
187
|
+
}
|
|
188
|
+
static any(signals) {
|
|
189
|
+
const c = new AbortControllerPolyfill();
|
|
190
|
+
for (const s of signals) {
|
|
191
|
+
if (s.aborted) {
|
|
192
|
+
c.abort(s.reason);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
s.addEventListener('abort', () => c.abort(s.reason), { once: true });
|
|
196
|
+
}
|
|
197
|
+
return c.signal;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export class AbortControllerPolyfill {
|
|
201
|
+
signal = new AbortSignalPolyfill();
|
|
202
|
+
abort(reason) {
|
|
203
|
+
const s = this.signal;
|
|
204
|
+
if (s.aborted)
|
|
205
|
+
return;
|
|
206
|
+
s.aborted = true;
|
|
207
|
+
s.reason =
|
|
208
|
+
reason ??
|
|
209
|
+
Object.assign(new Error('This operation was aborted'), {
|
|
210
|
+
name: 'AbortError'
|
|
211
|
+
});
|
|
212
|
+
const e = new EventPolyfill('abort');
|
|
213
|
+
s.onabort?.(e);
|
|
214
|
+
s.dispatchEvent(e);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** Installs every missing API on g (globalThis in the renderer). */
|
|
218
|
+
export function install(g) {
|
|
219
|
+
const define = (name, value) => {
|
|
220
|
+
if (typeof g[name] === 'undefined') {
|
|
221
|
+
Object.defineProperty(g, name, {
|
|
222
|
+
value,
|
|
223
|
+
writable: true,
|
|
224
|
+
configurable: true,
|
|
225
|
+
enumerable: false
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
// Timers. A render is synchronous and its HTML is sent when it returns, so a
|
|
230
|
+
// timer set during rendering could not change the output; in Node it would
|
|
231
|
+
// fire after the response was sent. Callbacks are dropped, not kept, so they
|
|
232
|
+
// cannot hold memory across renders.
|
|
233
|
+
{
|
|
234
|
+
let next = 1;
|
|
235
|
+
const set = () => next++;
|
|
236
|
+
const clear = () => { };
|
|
237
|
+
define('setTimeout', set);
|
|
238
|
+
define('setInterval', set);
|
|
239
|
+
define('setImmediate', set);
|
|
240
|
+
define('clearTimeout', clear);
|
|
241
|
+
define('clearInterval', clear);
|
|
242
|
+
define('clearImmediate', clear);
|
|
243
|
+
define('requestAnimationFrame', undefined);
|
|
244
|
+
}
|
|
245
|
+
// performance.now(), for libraries that measure or seed with it.
|
|
246
|
+
{
|
|
247
|
+
const origin = Date.now();
|
|
248
|
+
define('performance', {
|
|
249
|
+
timeOrigin: origin,
|
|
250
|
+
now: () => Date.now() - origin,
|
|
251
|
+
mark: () => undefined,
|
|
252
|
+
measure: () => undefined,
|
|
253
|
+
getEntriesByName: () => [],
|
|
254
|
+
getEntriesByType: () => [],
|
|
255
|
+
clearMarks: () => { },
|
|
256
|
+
clearMeasures: () => { }
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
// atob and btoa.
|
|
260
|
+
{
|
|
261
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
262
|
+
define('btoa', (input) => {
|
|
263
|
+
const s = String(input);
|
|
264
|
+
let out = '';
|
|
265
|
+
for (let i = 0; i < s.length; i += 3) {
|
|
266
|
+
const codes = [s.charCodeAt(i), s.charCodeAt(i + 1), s.charCodeAt(i + 2)];
|
|
267
|
+
for (const c of codes)
|
|
268
|
+
if (c > 255)
|
|
269
|
+
throw new Error('btoa: the string contains characters outside of Latin1');
|
|
270
|
+
const [a, b, c] = codes;
|
|
271
|
+
const n = (a << 16) | ((b || 0) << 8) | (c || 0);
|
|
272
|
+
out +=
|
|
273
|
+
chars[n >> 18] + chars[(n >> 12) & 63] + (i + 1 < s.length ? chars[(n >> 6) & 63] : '=') + (i + 2 < s.length ? chars[n & 63] : '=');
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
276
|
+
});
|
|
277
|
+
define('atob', (input) => {
|
|
278
|
+
let s = String(input).replace(/[\t\n\f\r ]/g, '');
|
|
279
|
+
if (s.length % 4 === 0)
|
|
280
|
+
s = s.replace(/==?$/, '');
|
|
281
|
+
if (s.length % 4 === 1 || /[^A-Za-z0-9+/]/.test(s))
|
|
282
|
+
throw new Error('atob: the string is not correctly encoded');
|
|
283
|
+
let out = '';
|
|
284
|
+
let bits = 0;
|
|
285
|
+
let acc = 0;
|
|
286
|
+
for (const ch of s) {
|
|
287
|
+
acc = (acc << 6) | chars.indexOf(ch);
|
|
288
|
+
bits += 6;
|
|
289
|
+
if (bits >= 8) {
|
|
290
|
+
bits -= 8;
|
|
291
|
+
out += String.fromCharCode((acc >> bits) & 255);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return out;
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// crypto.getRandomValues and crypto.randomUUID, from Go's crypto/rand.
|
|
298
|
+
{
|
|
299
|
+
const getRandomValues = (array) => {
|
|
300
|
+
if (!array)
|
|
301
|
+
return array;
|
|
302
|
+
const bytes = new Uint8Array(array.buffer, array.byteOffset, array.byteLength);
|
|
303
|
+
if (bytes.length > 65536)
|
|
304
|
+
throw new Error('getRandomValues: at most 65536 bytes');
|
|
305
|
+
if (typeof __tern_random === 'function') {
|
|
306
|
+
const hex = __tern_random(bytes.length);
|
|
307
|
+
for (let i = 0; i < bytes.length; i++)
|
|
308
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
for (let i = 0; i < bytes.length; i++)
|
|
312
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
313
|
+
}
|
|
314
|
+
return array;
|
|
315
|
+
};
|
|
316
|
+
const randomUUID = () => {
|
|
317
|
+
const b = getRandomValues(new Uint8Array(16));
|
|
318
|
+
b[6] = (b[6] & 0x0f) | 0x40;
|
|
319
|
+
b[8] = (b[8] & 0x3f) | 0x80;
|
|
320
|
+
const h = [...b].map((x) => x.toString(16).padStart(2, '0')).join('');
|
|
321
|
+
return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20)}`;
|
|
322
|
+
};
|
|
323
|
+
define('crypto', { getRandomValues, randomUUID });
|
|
324
|
+
}
|
|
325
|
+
// structuredClone.
|
|
326
|
+
{
|
|
327
|
+
const clone = (value, seen) => {
|
|
328
|
+
if (typeof value !== 'object' || value === null) {
|
|
329
|
+
if (typeof value === 'function' || typeof value === 'symbol')
|
|
330
|
+
throw new Error(`structuredClone: ${typeof value} could not be cloned`);
|
|
331
|
+
return value;
|
|
332
|
+
}
|
|
333
|
+
if (seen.has(value))
|
|
334
|
+
return seen.get(value);
|
|
335
|
+
let out;
|
|
336
|
+
if (value instanceof Date)
|
|
337
|
+
out = new Date(value.getTime());
|
|
338
|
+
else if (value instanceof RegExp)
|
|
339
|
+
out = new RegExp(value.source, value.flags);
|
|
340
|
+
else if (value instanceof ArrayBuffer)
|
|
341
|
+
out = value.slice(0);
|
|
342
|
+
else if (ArrayBuffer.isView(value)) {
|
|
343
|
+
const C = value.constructor;
|
|
344
|
+
const buf = value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength);
|
|
345
|
+
out = value instanceof DataView ? new DataView(buf) : new C(buf, 0, value.length);
|
|
346
|
+
}
|
|
347
|
+
else if (value instanceof Map) {
|
|
348
|
+
const m = new Map();
|
|
349
|
+
seen.set(value, m);
|
|
350
|
+
for (const [k, v] of value)
|
|
351
|
+
m.set(clone(k, seen), clone(v, seen));
|
|
352
|
+
return m;
|
|
353
|
+
}
|
|
354
|
+
else if (value instanceof Set) {
|
|
355
|
+
const s = new Set();
|
|
356
|
+
seen.set(value, s);
|
|
357
|
+
for (const v of value)
|
|
358
|
+
s.add(clone(v, seen));
|
|
359
|
+
return s;
|
|
360
|
+
}
|
|
361
|
+
else if (value instanceof Error) {
|
|
362
|
+
const e = new value.constructor(value.message);
|
|
363
|
+
if (value.stack)
|
|
364
|
+
e.stack = value.stack;
|
|
365
|
+
out = e;
|
|
366
|
+
}
|
|
367
|
+
else if (Array.isArray(value)) {
|
|
368
|
+
const a = [];
|
|
369
|
+
seen.set(value, a);
|
|
370
|
+
value.forEach((v, i) => (a[i] = clone(v, seen)));
|
|
371
|
+
return a;
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
const o = {};
|
|
375
|
+
seen.set(value, o);
|
|
376
|
+
for (const k of Object.keys(value))
|
|
377
|
+
o[k] = clone(value[k], seen);
|
|
378
|
+
return o;
|
|
379
|
+
}
|
|
380
|
+
seen.set(value, out);
|
|
381
|
+
return out;
|
|
382
|
+
};
|
|
383
|
+
define('structuredClone', (value) => clone(value, new Map()));
|
|
384
|
+
}
|
|
385
|
+
define('TextEncoder', TextEncoderPolyfill);
|
|
386
|
+
define('TextDecoder', TextDecoderPolyfill);
|
|
387
|
+
define('Event', EventPolyfill);
|
|
388
|
+
define('CustomEvent', CustomEventPolyfill);
|
|
389
|
+
define('EventTarget', EventTargetPolyfill);
|
|
390
|
+
define('AbortSignal', AbortSignalPolyfill);
|
|
391
|
+
define('AbortController', AbortControllerPolyfill);
|
|
392
|
+
// fetch is not available while rendering: in Tern, data comes from Go load
|
|
393
|
+
// functions. Fail with an explanation instead of "fetch is not defined".
|
|
394
|
+
define('fetch', () => Promise.reject(new Error('fetch is not available during server rendering. Load data in a Go Load function, or fetch in onMount.')));
|
|
395
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { PreprocessorGroup } from 'svelte/compiler';
|
|
2
|
+
import type { Plugin } from 'vite';
|
|
3
|
+
export declare function goScriptEnabled(root: string): boolean;
|
|
4
|
+
/** The names a component's Go code shows in its markup. */
|
|
5
|
+
export declare function exportedNames(source: string): {
|
|
6
|
+
values: string[];
|
|
7
|
+
funcs: string[];
|
|
8
|
+
};
|
|
9
|
+
/** Replaces <script lang="go"> with the JavaScript that binds it to Go. */
|
|
10
|
+
export declare function goScriptPreprocessor(getRoot: () => string): PreprocessorGroup;
|
|
11
|
+
/** Builds the Go code at startup and on changes. */
|
|
12
|
+
export declare function goScriptPlugin(getRoot: () => string): Plugin;
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// Experimental <script lang="go"> support: Go component logic compiled to
|
|
2
|
+
// WebAssembly. Enabled with "experimental": { "goScript": true } in
|
|
3
|
+
// tern.json. See docs/experimental-go-script.md.
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
const run = promisify(execFile);
|
|
10
|
+
const DIR = '.tern/goscript';
|
|
11
|
+
/** The WebAssembly components' Go packages; live ones are written by tern gen. */
|
|
12
|
+
const WASM_DIR = '.tern/goscript/wasm';
|
|
13
|
+
const GO_SCRIPT = /<script\b([^>]*\blang=["']go["'][^>]*)>([\s\S]*?)<\/script>/;
|
|
14
|
+
export function goScriptEnabled(root) {
|
|
15
|
+
try {
|
|
16
|
+
const c = JSON.parse(fs.readFileSync(path.join(root, 'tern.json'), 'utf8'));
|
|
17
|
+
return c.experimental?.goScript === true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** A component's ID: stable for its path, valid in Go and JS. */
|
|
24
|
+
function componentID(root, file) {
|
|
25
|
+
return 'c' + createHash('sha256').update(path.relative(root, file).split(path.sep).join('/')).digest('hex').slice(0, 10);
|
|
26
|
+
}
|
|
27
|
+
function walk(dir, out = []) {
|
|
28
|
+
let entries;
|
|
29
|
+
try {
|
|
30
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
for (const e of entries) {
|
|
36
|
+
if (e.name.startsWith('.') || e.name === 'node_modules')
|
|
37
|
+
continue;
|
|
38
|
+
const p = path.join(dir, e.name);
|
|
39
|
+
if (e.isDirectory())
|
|
40
|
+
walk(p, out);
|
|
41
|
+
else if (e.name.endsWith('.svelte'))
|
|
42
|
+
out.push(p);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
function scan(root) {
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const file of walk(path.join(root, 'src'))) {
|
|
49
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
50
|
+
const m = GO_SCRIPT.exec(text);
|
|
51
|
+
if (!m)
|
|
52
|
+
continue;
|
|
53
|
+
const codeStart = m.index + m[0].indexOf('>') + 1;
|
|
54
|
+
const live = /(^|\s)live(\s|=|$)/.test(m[1]);
|
|
55
|
+
out.push({ id: componentID(root, file), live, file, source: m[2], line: text.slice(0, codeStart).split('\n').length });
|
|
56
|
+
}
|
|
57
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
58
|
+
}
|
|
59
|
+
/** The names a component's Go code shows in its markup. */
|
|
60
|
+
export function exportedNames(source) {
|
|
61
|
+
const values = [
|
|
62
|
+
...source.matchAll(/\b(?:State|Derived)(?:\[[^\]]*\])?\(\s*\w+\s*,\s*"([A-Za-z_$][\w$]*)"/g),
|
|
63
|
+
...source.matchAll(/\.Show\(\s*\w+\s*,\s*"([A-Za-z_$][\w$]*)"/g)
|
|
64
|
+
].map((m) => m[1]);
|
|
65
|
+
const funcs = [...source.matchAll(/\.Func\(\s*"([A-Za-z_$][\w$]*)"/g)].map((m) => m[1]);
|
|
66
|
+
return { values: [...new Set(values)], funcs: [...new Set(funcs)] };
|
|
67
|
+
}
|
|
68
|
+
function modulePath(root) {
|
|
69
|
+
const mod = /^module\s+(\S+)/m.exec(fs.readFileSync(path.join(root, 'go.mod'), 'utf8'));
|
|
70
|
+
if (!mod)
|
|
71
|
+
throw new Error('tern: go.mod has no module line');
|
|
72
|
+
return mod[1];
|
|
73
|
+
}
|
|
74
|
+
/** Writes the Go packages, builds the WebAssembly module and computes initial states. */
|
|
75
|
+
async function compile(root, wasm) {
|
|
76
|
+
// Live components run on the server: tern gen compiles them into the app.
|
|
77
|
+
const comps = scan(root).filter((c) => !c.live);
|
|
78
|
+
const dir = path.join(root, DIR);
|
|
79
|
+
const pkgDir = path.join(root, WASM_DIR);
|
|
80
|
+
fs.rmSync(pkgDir, { recursive: true, force: true });
|
|
81
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
82
|
+
if (comps.length === 0) {
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'initial.json'), '{}');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const mod = modulePath(root);
|
|
87
|
+
let main = '// Code generated by tern from <script lang="go">; DO NOT EDIT.\n\npackage main\n\nimport (\n\t"github.com/Unfield/Tern/goscript"\n';
|
|
88
|
+
for (const c of comps) {
|
|
89
|
+
const body = c.source.replace(/^\s*package\s+\w+\s*$/m, '');
|
|
90
|
+
fs.mkdirSync(path.join(pkgDir, c.id), { recursive: true });
|
|
91
|
+
// //line makes compile errors point at the .svelte file.
|
|
92
|
+
fs.writeFileSync(path.join(pkgDir, c.id, 'component.go'), `package ${c.id}\n\n//line ${c.file}:${c.line}\n${body}\n`);
|
|
93
|
+
main += `\t${c.id} "${mod}/${WASM_DIR}/${c.id}"\n`;
|
|
94
|
+
}
|
|
95
|
+
main += ')\n\nfunc main() {\n';
|
|
96
|
+
for (const c of comps)
|
|
97
|
+
main += `\tgoscript.Register(${JSON.stringify(c.id)}, ${c.id}.Setup)\n`;
|
|
98
|
+
main += '\tgoscript.Run()\n}\n';
|
|
99
|
+
fs.writeFileSync(path.join(pkgDir, 'main.go'), main);
|
|
100
|
+
const goErr = (e) => {
|
|
101
|
+
const err = e;
|
|
102
|
+
return new Error(`tern: compiling <script lang="go">:\n${(err.stderr || err.message).trim()}`);
|
|
103
|
+
};
|
|
104
|
+
// Setup runs natively once, so server rendering shows the initial state.
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await run('go', ['run', './' + WASM_DIR], { cwd: root, maxBuffer: 16 << 20 });
|
|
107
|
+
fs.writeFileSync(path.join(dir, 'initial.json'), stdout);
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
throw goErr(e);
|
|
111
|
+
}
|
|
112
|
+
if (!wasm)
|
|
113
|
+
return;
|
|
114
|
+
try {
|
|
115
|
+
await run('go', ['build', '-trimpath', '-ldflags=-s -w', '-o', path.join(DIR, 'app.wasm'), './' + WASM_DIR], {
|
|
116
|
+
cwd: root,
|
|
117
|
+
env: { ...process.env, GOOS: 'js', GOARCH: 'wasm' }
|
|
118
|
+
});
|
|
119
|
+
const { stdout: goroot } = await run('go', ['env', 'GOROOT']);
|
|
120
|
+
const candidates = ['lib/wasm/wasm_exec.js', 'misc/wasm/wasm_exec.js'].map((p) => path.join(goroot.trim(), p));
|
|
121
|
+
const execJS = candidates.find((p) => fs.existsSync(p));
|
|
122
|
+
if (!execJS)
|
|
123
|
+
throw new Error('tern: wasm_exec.js was not found in GOROOT');
|
|
124
|
+
fs.copyFileSync(execJS, path.join(dir, 'wasm_exec.js'));
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
throw goErr(e);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function initialState(root, id) {
|
|
131
|
+
try {
|
|
132
|
+
const all = JSON.parse(fs.readFileSync(path.join(root, DIR, 'initial.json'), 'utf8'));
|
|
133
|
+
return all[id] ?? {};
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return {};
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Replaces <script lang="go"> with the JavaScript that binds it to Go. */
|
|
140
|
+
export function goScriptPreprocessor(getRoot) {
|
|
141
|
+
return {
|
|
142
|
+
name: 'tern-goscript',
|
|
143
|
+
markup({ content }) {
|
|
144
|
+
// A live component carries its server-rendered state in the page,
|
|
145
|
+
// so hydration starts from the same values.
|
|
146
|
+
const m = GO_SCRIPT.exec(content);
|
|
147
|
+
if (!m || !/(^|\s)live(\s|=|$)/.test(m[1]))
|
|
148
|
+
return;
|
|
149
|
+
return { code: content + '\n<svelte:head>{@html __go.stateScript}</svelte:head>\n' };
|
|
150
|
+
},
|
|
151
|
+
script({ content, attributes, filename }) {
|
|
152
|
+
if (attributes.lang !== 'go' || !filename)
|
|
153
|
+
return;
|
|
154
|
+
const root = getRoot();
|
|
155
|
+
if (!goScriptEnabled(root)) {
|
|
156
|
+
throw new Error(`${filename}: <script lang="go"> is experimental; enable it with "experimental": { "goScript": true } in tern.json`);
|
|
157
|
+
}
|
|
158
|
+
const id = componentID(root, filename);
|
|
159
|
+
const { values, funcs } = exportedNames(content);
|
|
160
|
+
const { lang: _lang, live, ...rest } = attributes;
|
|
161
|
+
if (live) {
|
|
162
|
+
let code = `
|
|
163
|
+
import { useLive } from '@ternkit/tern/goscript';
|
|
164
|
+
let __goProps = $props();
|
|
165
|
+
const __goUID = $props.id();
|
|
166
|
+
const __go = useLive(${JSON.stringify(id)}, __goUID, () => ({ ...__goProps }));
|
|
167
|
+
`;
|
|
168
|
+
for (const v of values)
|
|
169
|
+
code += `\tconst ${v} = $derived(__go.state[${JSON.stringify(v)}] as any);\n`;
|
|
170
|
+
for (const f of funcs)
|
|
171
|
+
code += `\tconst ${f} = (...args: unknown[]): any => __go.call(${JSON.stringify(f)}, ...args);\n`;
|
|
172
|
+
return { code, attributes: { ...rest, lang: 'ts' } };
|
|
173
|
+
}
|
|
174
|
+
const base = path.join(root, DIR).split(path.sep).join('/');
|
|
175
|
+
let code = `
|
|
176
|
+
import { useGo } from '@ternkit/tern/goscript';
|
|
177
|
+
import __goWasm from '${base}/app.wasm?url';
|
|
178
|
+
let __goProps = $props();
|
|
179
|
+
const __go = useGo(${JSON.stringify(id)}, ${JSON.stringify(initialState(root, id))}, () => import('${base}/wasm_exec.js'), __goWasm, () => ({ ...__goProps }));
|
|
180
|
+
`;
|
|
181
|
+
for (const v of values)
|
|
182
|
+
code += `\tconst ${v} = $derived(__go.state[${JSON.stringify(v)}] as any);\n`;
|
|
183
|
+
for (const f of funcs)
|
|
184
|
+
code += `\tconst ${f} = (...args: unknown[]): any => __go.call(${JSON.stringify(f)}, ...args);\n`;
|
|
185
|
+
return { code, attributes: { ...rest, lang: 'ts' } };
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
/** Builds the Go code at startup and on changes. */
|
|
190
|
+
export function goScriptPlugin(getRoot) {
|
|
191
|
+
let config;
|
|
192
|
+
let enabled = false;
|
|
193
|
+
return {
|
|
194
|
+
name: 'tern:goscript',
|
|
195
|
+
configResolved(c) {
|
|
196
|
+
config = c;
|
|
197
|
+
enabled = goScriptEnabled(c.root);
|
|
198
|
+
},
|
|
199
|
+
async buildStart() {
|
|
200
|
+
if (!enabled)
|
|
201
|
+
return;
|
|
202
|
+
const ssr = !!config.build.ssr;
|
|
203
|
+
const swBuild = process.env.TERN_SW_BUILD === '1';
|
|
204
|
+
if (swBuild)
|
|
205
|
+
return;
|
|
206
|
+
// The server build only needs the initial state, which the client
|
|
207
|
+
// build already computed.
|
|
208
|
+
if (ssr && fs.existsSync(path.join(config.root, DIR, 'initial.json')))
|
|
209
|
+
return;
|
|
210
|
+
await compile(config.root, !ssr);
|
|
211
|
+
},
|
|
212
|
+
configureServer(server) {
|
|
213
|
+
if (!enabled)
|
|
214
|
+
return;
|
|
215
|
+
let timer;
|
|
216
|
+
server.watcher.on('change', (file) => {
|
|
217
|
+
if (!file.endsWith('.svelte') || !GO_SCRIPT.test(fs.readFileSync(file, 'utf8')))
|
|
218
|
+
return;
|
|
219
|
+
clearTimeout(timer);
|
|
220
|
+
timer = setTimeout(() => {
|
|
221
|
+
compile(config.root, true).then(() => server.ws.send({ type: 'full-reload' }), (e) => server.config.logger.error(e.message));
|
|
222
|
+
}, 50);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
package/dist/vite/index.d.ts
CHANGED
|
@@ -3,6 +3,12 @@ import type { Plugin } from 'vite';
|
|
|
3
3
|
export interface TernOptions {
|
|
4
4
|
/** Options passed to @sveltejs/vite-plugin-svelte. */
|
|
5
5
|
svelte?: Partial<SvelteOptions>;
|
|
6
|
+
/**
|
|
7
|
+
* Packages to bundle in their "browser" build for server rendering, for
|
|
8
|
+
* libraries whose default build needs Node APIs such as Buffer while the
|
|
9
|
+
* browser build is portable. nanoid is included by default.
|
|
10
|
+
*/
|
|
11
|
+
ssrBrowserBuilds?: string[];
|
|
6
12
|
}
|
|
7
13
|
/**
|
|
8
14
|
* The Vite plugin for Tern apps. It includes the Svelte plugin, so it is
|