@skgo/sveltekit-adapter 0.3.1 → 0.3.3
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 +1 -1
- package/package.json +1 -1
- package/skgo-adapter/env.js +124 -0
- package/skgo-adapter.js +72 -9
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ the Go process renders pages with, and a manifest naming the routes, remote
|
|
|
9
9
|
functions and server loads Go must answer.
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
pnpm add -D @skgo/sveltekit-adapter@0.3.
|
|
12
|
+
pnpm add -D @skgo/sveltekit-adapter@0.3.3
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Use the same version as the `github.com/tylergannon/skgo` requirement in your
|
package/package.json
CHANGED
package/skgo-adapter/env.js
CHANGED
|
@@ -61,6 +61,7 @@ async function fromApp(specifier) {
|
|
|
61
61
|
|
|
62
62
|
const { rolldown } = await fromApp('vite/rolldown');
|
|
63
63
|
const { transformSync } = await fromApp('vite/rolldown/experimental');
|
|
64
|
+
const { parseAst } = await fromApp('vite/rolldown/parseAst');
|
|
64
65
|
// `fetchModule` operates on the app's own `DevEnvironment`, so it has to be
|
|
65
66
|
// the app's copy for the same reason rolldown is: a second module realm's
|
|
66
67
|
// vite does not recognise this one's environments.
|
|
@@ -480,6 +481,7 @@ function engineTransform(code, id, { lowered, helpers } = {}) {
|
|
|
480
481
|
'=> Promise.reject(new Error("skgo: no dynamic import in the SSR engine"))'
|
|
481
482
|
);
|
|
482
483
|
}
|
|
484
|
+
out = wrapFieldInitialisers(out, id);
|
|
483
485
|
if (!UNPARSEABLE.test(out)) {
|
|
484
486
|
return out === code ? null : { code: out, map: null };
|
|
485
487
|
}
|
|
@@ -495,6 +497,128 @@ function engineTransform(code, id, { lowered, helpers } = {}) {
|
|
|
495
497
|
return { code: lower, map: null };
|
|
496
498
|
}
|
|
497
499
|
|
|
500
|
+
/**
|
|
501
|
+
* Wraps every class field initialiser that closes over `this` in a plain
|
|
502
|
+
* function called with the instance, so the initialiser itself only reads
|
|
503
|
+
* `this` and the closure is made inside an ordinary call.
|
|
504
|
+
*
|
|
505
|
+
* goja runs field initialisers with the argument count of whichever function
|
|
506
|
+
* is executing `new`, and an initialiser that closes over `this` derives its
|
|
507
|
+
* stack base from that count. Constructed inside a function with parameters —
|
|
508
|
+
* every component is one — such a class panics the render or throws "Value is
|
|
509
|
+
* not an Object". Svelte emits exactly that for a class-field `$derived`:
|
|
510
|
+
* `#graph = derived(() => this.doc.toGraph())`. The engine fix is
|
|
511
|
+
* dop251/goja#737; once skgo requires a goja carrying it, this goes.
|
|
512
|
+
*
|
|
513
|
+
* `(function () { return VALUE; }).call(this)` evaluates VALUE at the same
|
|
514
|
+
* moment with the same `this` and `new.target`, and an initialiser can hold no
|
|
515
|
+
* `arguments`, `await` or `yield` for the function to capture instead. An
|
|
516
|
+
* anonymous function keeps the name the field would have given it by being
|
|
517
|
+
* defined as a property of that name. `super` cannot move into a plain
|
|
518
|
+
* function, so an initialiser that reaches it is left alone.
|
|
519
|
+
*
|
|
520
|
+
* @param {string} code
|
|
521
|
+
* @param {string} id
|
|
522
|
+
*/
|
|
523
|
+
function wrapFieldInitialisers(code, id) {
|
|
524
|
+
const evals = code.includes('eval(');
|
|
525
|
+
if (!evals && !(code.includes('=>') && /\bthis\b/.test(code))) return code;
|
|
526
|
+
|
|
527
|
+
/** @type {Array<{ at: number, text: string, closes: boolean, order: number }>} */
|
|
528
|
+
const inserts = [];
|
|
529
|
+
visit(parseAst(code, undefined, id.replace(/\0/g, '_')), (node) => {
|
|
530
|
+
if (node.type !== 'PropertyDefinition' || !node.value) return;
|
|
531
|
+
const found = captures(node.value);
|
|
532
|
+
if (!found.this || found.super) return;
|
|
533
|
+
const { start, end } = node.value;
|
|
534
|
+
const order = inserts.length;
|
|
535
|
+
if (!node.computed && anonymousDefinition(node.value)) {
|
|
536
|
+
const name = JSON.stringify(node.key.type === 'PrivateIdentifier' ? '#' + node.key.name : String(node.key.name ?? node.key.value));
|
|
537
|
+
inserts.push({ at: start, text: `(function () { return { ${name}: `, closes: false, order });
|
|
538
|
+
inserts.push({ at: end, text: ` }[${name}]; }).call(this)`, closes: true, order });
|
|
539
|
+
} else {
|
|
540
|
+
inserts.push({ at: start, text: '(function () { return (', closes: false, order });
|
|
541
|
+
inserts.push({ at: end, text: '); }).call(this)', closes: true, order });
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
if (!inserts.length) return code;
|
|
545
|
+
|
|
546
|
+
// Fields nest (a class inside an initialiser), so one offset can take several
|
|
547
|
+
// inserts: whatever closes goes before whatever opens, an inner field closes
|
|
548
|
+
// before its outer one, and an outer one opens before its inner one. Visit
|
|
549
|
+
// order is outer first.
|
|
550
|
+
inserts.sort((a, b) => a.at - b.at || Number(b.closes) - Number(a.closes) || (a.closes ? b.order - a.order : a.order - b.order));
|
|
551
|
+
let out = '';
|
|
552
|
+
let from = 0;
|
|
553
|
+
for (const { at, text } of inserts) {
|
|
554
|
+
out += code.slice(from, at) + text;
|
|
555
|
+
from = at;
|
|
556
|
+
}
|
|
557
|
+
return out + code.slice(from);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Whether an initialiser closes over `this` — inside an arrow, or through a
|
|
562
|
+
* direct `eval` — and whether it reaches `super`. Neither crosses an ordinary
|
|
563
|
+
* function, which has its own of both.
|
|
564
|
+
*
|
|
565
|
+
* @param {any} value
|
|
566
|
+
* @returns {{ this: boolean, super: boolean }}
|
|
567
|
+
*/
|
|
568
|
+
function captures(value) {
|
|
569
|
+
const found = { this: false, super: false };
|
|
570
|
+
(function walk(node, arrow) {
|
|
571
|
+
switch (node.type) {
|
|
572
|
+
case 'ThisExpression':
|
|
573
|
+
if (arrow) found.this = true;
|
|
574
|
+
return;
|
|
575
|
+
case 'Super':
|
|
576
|
+
found.super = true;
|
|
577
|
+
return;
|
|
578
|
+
case 'FunctionExpression':
|
|
579
|
+
case 'FunctionDeclaration':
|
|
580
|
+
return;
|
|
581
|
+
case 'CallExpression':
|
|
582
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'eval') found.this = true;
|
|
583
|
+
break;
|
|
584
|
+
case 'ArrowFunctionExpression':
|
|
585
|
+
arrow = true;
|
|
586
|
+
break;
|
|
587
|
+
}
|
|
588
|
+
for (const child of children(node)) walk(child, arrow);
|
|
589
|
+
})(value, false);
|
|
590
|
+
return found;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** @param {any} node */
|
|
594
|
+
function anonymousDefinition(node) {
|
|
595
|
+
return (
|
|
596
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
597
|
+
((node.type === 'FunctionExpression' || node.type === 'ClassExpression') && !node.id)
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* @param {any} node
|
|
603
|
+
* @param {(node: any) => void} fn
|
|
604
|
+
*/
|
|
605
|
+
function visit(node, fn) {
|
|
606
|
+
fn(node);
|
|
607
|
+
for (const child of children(node)) visit(child, fn);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** @param {any} node */
|
|
611
|
+
function* children(node) {
|
|
612
|
+
for (const key in node) {
|
|
613
|
+
const value = node[key];
|
|
614
|
+
if (Array.isArray(value)) {
|
|
615
|
+
for (const item of value) if (item && typeof item.type === 'string') yield item;
|
|
616
|
+
} else if (value && typeof value.type === 'string') {
|
|
617
|
+
yield value;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
498
622
|
/**
|
|
499
623
|
* Where oxc's lowering helpers live, resolved the way dev has to resolve them:
|
|
500
624
|
* from vite rather than from the app. Under pnpm `@oxc-project/runtime` is a
|
package/skgo-adapter.js
CHANGED
|
@@ -104,6 +104,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
104
104
|
},
|
|
105
105
|
`${out}/ssr/bundle.js`
|
|
106
106
|
);
|
|
107
|
+
writeAppManifest(`${out}/ssr/bundle.js`, builder.manifest);
|
|
107
108
|
|
|
108
109
|
// Kit's own `builder.compress` writes a `.br` and a `.gz` beside every
|
|
109
110
|
// file whose extension it compresses, and Go chooses one per request
|
|
@@ -114,7 +115,7 @@ export default function skgo({ out = 'build', precompress = true } = {}) {
|
|
|
114
115
|
await builder.compress(`${out}/prerendered`);
|
|
115
116
|
}
|
|
116
117
|
|
|
117
|
-
// `builder` has no writeJson in kit 3.0.0-next.
|
|
118
|
+
// `builder` has no writeJson in kit 3.0.0-next.27 (it existed on the
|
|
118
119
|
// kit 2 line); write the manifest ourselves.
|
|
119
120
|
write(
|
|
120
121
|
`${out}/skgo.manifest.json`,
|
|
@@ -323,11 +324,41 @@ function readPrerendered(builder) {
|
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
/**
|
|
326
|
-
* Kit's
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
327
|
+
* Kit replaces `$app/manifest`'s generated identifiers in its client and
|
|
328
|
+
* server environments. The Goja renderer is an adapter-owned fourth
|
|
329
|
+
* environment, so apply the same values after its chunks have been folded into
|
|
330
|
+
* the one script Go evaluates.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} file
|
|
333
|
+
* @param {typeof import('$app/manifest')} manifest
|
|
334
|
+
*/
|
|
335
|
+
function writeAppManifest(file, manifest) {
|
|
336
|
+
let source = readFileSync(file, 'utf8');
|
|
337
|
+
const replacements = {
|
|
338
|
+
__SVELTEKIT_MANIFEST_ASSETS__: manifest.assets,
|
|
339
|
+
__SVELTEKIT_MANIFEST_IMMUTABLE__: manifest.immutable,
|
|
340
|
+
__SVELTEKIT_MANIFEST_PRERENDERED__: manifest.prerendered,
|
|
341
|
+
__SVELTEKIT_MANIFEST_ROUTES__: manifest.routes
|
|
342
|
+
};
|
|
343
|
+
for (const [identifier, value] of Object.entries(replacements)) {
|
|
344
|
+
source = source.replaceAll(identifier, JSON.stringify(value));
|
|
345
|
+
}
|
|
346
|
+
if (source.includes('__SVELTEKIT_MANIFEST_')) {
|
|
347
|
+
throw new Error(
|
|
348
|
+
'skgo: the Goja bundle contains a SvelteKit app-manifest value the adapter did not replace'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
writeFileSync(file, source);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Kit's own server manifest, as an object *and* as text. Kit 3 generates a
|
|
356
|
+
* complete server-instance module instead of returning manifest source. We
|
|
357
|
+
* remove its eager Server import and constructor and export the manifest it
|
|
358
|
+
* wrote; every remaining import sits inside a lazy thunk, so importing the
|
|
359
|
+
* result resolves nothing and runs no application code. The source is worth
|
|
360
|
+
* keeping because those thunks carry information the imported object has
|
|
361
|
+
* already hidden inside closures. See readNodes.
|
|
331
362
|
*
|
|
332
363
|
* @param {import('@sveltejs/kit').Builder} builder
|
|
333
364
|
* @returns {Promise<{ manifest: any, source: string }>}
|
|
@@ -335,12 +366,44 @@ function readPrerendered(builder) {
|
|
|
335
366
|
async function readKitManifest(builder) {
|
|
336
367
|
const dir = builder.getBuildDirectory('skgo');
|
|
337
368
|
const file = join(dir, 'kit-manifest.js');
|
|
338
|
-
const source = builder.generateManifest({ relativePath: '.' });
|
|
339
369
|
mkdirSync(dir, { recursive: true });
|
|
340
|
-
|
|
370
|
+
builder.generateServerInstance(file);
|
|
371
|
+
let source = readFileSync(file, 'utf8');
|
|
372
|
+
const original = source;
|
|
373
|
+
source = source
|
|
374
|
+
.replace(/^import \{ Server \} from '[^']+';\n/, '')
|
|
375
|
+
.replace(/^const manifest = /m, 'export const manifest = ')
|
|
376
|
+
.replace(/\nexport const server = new Server\(manifest\);\n?$/, '\n');
|
|
377
|
+
if (source === original || !source.includes('export const manifest = ')) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
'skgo: kit generated a server instance in an unknown shape; refusing to guess where its manifest is'
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
writeFileSync(file, source);
|
|
341
383
|
try {
|
|
342
384
|
// The cache buster matters: `vp build` can run twice in one process.
|
|
343
|
-
|
|
385
|
+
const raw = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).manifest;
|
|
386
|
+
return {
|
|
387
|
+
// Kit 3.0.0-next.27 made SSRManifest private and flattened its
|
|
388
|
+
// previous `_` payload. Keep the adapter's consumers on one shape so
|
|
389
|
+
// their fail-closed checks remain readable while the public generation
|
|
390
|
+
// API changes underneath them.
|
|
391
|
+
manifest: raw._
|
|
392
|
+
? raw
|
|
393
|
+
: {
|
|
394
|
+
appDir: raw.app_dir,
|
|
395
|
+
appPath: raw.app_path,
|
|
396
|
+
assets: raw.assets,
|
|
397
|
+
mimeTypes: raw.mime_types,
|
|
398
|
+
_: {
|
|
399
|
+
client: raw.client,
|
|
400
|
+
nodes: raw.nodes,
|
|
401
|
+
remotes: raw.remotes,
|
|
402
|
+
routes: raw.routes
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
source
|
|
406
|
+
};
|
|
344
407
|
} finally {
|
|
345
408
|
rmSync(file, { force: true });
|
|
346
409
|
}
|