@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 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.1
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skgo/sveltekit-adapter",
3
- "version": "0.3.1",
3
+ "version": "0.3.3",
4
4
  "description": "SvelteKit adapter for skgo: builds a frontend that a Go binary renders and answers",
5
5
  "keywords": [
6
6
  "adapter",
@@ -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.25 (it existed on the
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 own server manifest, as an object *and* as text. `generateManifest`
327
- * returns the source of a module whose only imports sit inside lazy thunks, so
328
- * importing it resolves nothing and runs no application code — but the source
329
- * is worth keeping, because the thunks carry information the imported object
330
- * has already hidden inside closures. See readNodes.
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
- writeFileSync(file, `export const manifest = ${source};\n`);
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
- return { manifest: (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).manifest, source };
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
  }