/** * The variant components live mode scaffolds must survive the app's own Svelte * compiler AND parse as JavaScript afterwards. Run with: * node --test tests/live-svelte-props-script.test.mjs * * `compileCheckVariants` already compiles each variant with `generate: false`, * which proves the .svelte source parses. It cannot prove the emitted module * parses, and that is the gap this suite covers. In issue #580, esrap 2.3.3 * (Svelte's JS printer, pulled in by `esrap: ^2.2.12`) printed a * `/** @type {...} *\/` written directly before a destructuring declaration as * JSDoc cast syntax on the template's own declaration, * * var /** @type {{ title: string; }} *\/ (h1) = root(); * * which compiles without complaint and then dies in the browser's dynamic * import with "Unexpected token '('". Nothing rendered, and the failure * surfaced two layers away from the comment that caused it. * * Two things follow, and both shape this file: * * 1. The assertion is on the emitted JavaScript, never on the comment style. A * future printer that mangles some other construct fails here too, which a * test pinned to `@typedef` would not. * 2. The compiler is imported as ESM, because that is what the dev server * resolves. Svelte's export map sends `require` to a prebuilt CJS compiler * and `import` to `src/compiler`, and only the latter goes through the * installed esrap. Reaching for `createRequire` here (as `loadSvelteCompiler` * does) compiles with a different printer than the browser ever sees, and * the guard passes while the product is broken. That is not hypothetical: * the first draft of this suite did exactly that and reported green. */ import { describe, it, before, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; import os from 'node:os'; import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; import { buildPropsScriptV2 } from '../skill/scripts/live/svelte-ast.mjs'; import { scaffoldSvelteComponentSession } from '../skill/scripts/live/svelte-component.mjs'; const require = createRequire(import.meta.url); const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); let compile; let acornParse; let svelteVersion; let printerVersion; before(async () => { // ESM import, not createRequire: see note 2 in the header. This is the build // a Vite dev server loads, and the only one that uses the installed esrap. const compiler = await import('svelte/compiler'); compile = compiler.compile; svelteVersion = compiler.VERSION; acornParse = require('acorn').parse; try { printerVersion = require('esrap/package.json').version; } catch { printerVersion = 'unknown'; } }); function assertEmittedJsParses(svelteSource, label) { const { js } = compile(svelteSource, { generate: 'client', // dev:true is what a dev server uses, and it is the mode that carries the // defect: the extra location metadata is where the stray annotation lands. dev: true, filename: 'v1.svelte', }); try { acornParse(js.code, { ecmaVersion: 'latest', sourceType: 'module' }); } catch (err) { const line = js.code.split('\n')[(err.loc?.line ?? 1) - 1] || ''; assert.fail( `${label}: svelte ${svelteVersion} (printer esrap ${printerVersion}) emitted JavaScript that does not parse.\n` + ` ${err.message}\n` + ` offending line: ${line.trim()}\n` + ` The browser reports this as a mount failure, not as a compile error, ` + `because the .svelte source is valid and only the emitted module is not.\n` + ` source:\n${svelteSource}`, ); } } describe('scaffolded props scripts emit parseable JavaScript', () => { // The empty contract is the shape the CI fixture hit: a picked element with // no dynamic values at all. const CONTRACTS = { 'no props': [], 'one text prop': [{ prop: 'title', expr: 'title', kind: 'text' }], 'every prop kind': [ { prop: 'title', expr: 'title', kind: 'text' }, { prop: 'body', expr: 'post.body', kind: 'raw' }, { prop: 'isOpen', expr: 'open', kind: 'condition' }, { prop: 'items', expr: 'stages', kind: 'collection' }, { prop: 'onSelect', expr: 'select', kind: 'handler' }, ], }; for (const [label, contract] of Object.entries(CONTRACTS)) { it(`buildPropsScriptV2: ${label}`, () => { const source = `${buildPropsScriptV2(contract)}\n