/** * Conformance tests for the live-mode framework registry. * * Two jobs: * * 1. Every entry in FRAMEWORKS satisfies the contract the live scripts rely * on — a callable detect, a resolvable inject strategy, source traits * drawn from the closed sets, and undo functions for every patch kind it * can journal. A new framework module that forgets a field fails here * rather than at a user's inject. * 2. detect() lands on the expected entry for the static framework fixtures. * This is the seed of the conformance battery; behavioral conformance * (does the injected bundle actually boot in that framework) stays in the * live-e2e suite. Fixtures are read-only here. * * Plus the crash-safe injection journal, which is registry-layer code: it * heals through the entries' own undo functions. * * Run with: node --test tests/live-frameworks.test.mjs */ import { describe, it, beforeEach, afterEach } from 'node:test'; import assert from 'node:assert/strict'; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { COMMENT_SYNTAXES, FRAMEWORKS, INJECT_KINDS, PATCH_UNDOERS, PREVIEW_MODES, SOURCE_TRAIT_DEFAULTS, STYLE_MODES, TAG_PATCH_KIND, describeInjectArtifacts, frameworkIgnorePatterns, resolveFramework, resolveSourceTraits, } from '../skill/scripts/live/frameworks/index.mjs'; import { clearInjectJournal, healInjectJournal, injectJournalPath, readInjectJournal, recordInjection, } from '../skill/scripts/live/frameworks/journal.mjs'; import { insertTag } from '../skill/scripts/live/frameworks/tag-strategy.mjs'; const __dirname = dirname(fileURLToPath(import.meta.url)); const FIXTURES_DIR = join(__dirname, 'framework-fixtures'); /** Read-only: the fixture's project tree plus its live config. */ function fixtureProject(name) { const root = join(FIXTURES_DIR, name, 'files'); const manifest = join(FIXTURES_DIR, name, 'fixture.json'); if (!existsSync(root) || !existsSync(manifest)) return null; return { root, config: JSON.parse(readFileSync(manifest, 'utf-8')).config }; } // --------------------------------------------------------------------------- // Contract shape // --------------------------------------------------------------------------- describe('framework registry — entry contract', () => { it('declares the documented detection priority, terminating in static-html', () => { assert.deepEqual( FRAMEWORKS.map((f) => f.name), ['sveltekit', 'nuxt', 'tanstack-start', 'astro', 'nextjs', 'vite-generic', 'static-html'], 'detection order is injection priority; live-inject.mjs used to hard-code exactly this', ); assert.equal(FRAMEWORKS.at(-1).name, 'static-html'); assert.ok(FRAMEWORKS.at(-1).detect(process.cwd(), null), 'the terminal entry must always match'); }); it('gives every entry a unique name', () => { const names = FRAMEWORKS.map((f) => f.name); assert.equal(new Set(names).size, names.length); for (const name of names) { assert.equal(typeof name, 'string'); assert.ok(name.length > 0); } }); for (const framework of FRAMEWORKS) { describe(`entry · ${framework.name}`, () => { it('exposes a callable detect', () => { assert.equal(typeof framework.detect, 'function'); }); it('resolves its inject strategy', () => { const { inject } = framework; assert.ok(inject, 'entry declares an inject strategy'); assert.ok(INJECT_KINDS.includes(inject.kind), `unknown inject kind ${inject.kind}`); if (inject.kind === 'adapter') { assert.equal(typeof inject.apply, 'function'); assert.equal(typeof inject.remove, 'function'); assert.equal(typeof inject.artifacts, 'function', 'adapters must declare what they write'); assert.equal(typeof inject.ignorePatterns, 'function'); } else { // The tag strategy is shared; an entry declaring per-framework // apply/remove alongside kind:'tag' would silently never run. assert.equal(inject.apply, undefined); assert.equal(inject.remove, undefined); } }); it('registers an undo for every patch kind it can journal', () => { for (const key of Object.keys(framework.inject.unpatch || {})) { assert.equal(typeof PATCH_UNDOERS[key], 'function', `no undo registered for ${key}`); } }); it('declares source traits from the closed sets', () => { const { source } = framework; assert.ok(Array.isArray(source.extensions) && source.extensions.length > 0); for (const ext of source.extensions) { assert.match(ext, /^\.[a-z0-9]+$/, 'extensions are lowercase and dotted'); const traits = resolveSourceTraits(`Example${ext}`); assert.ok(PREVIEW_MODES.includes(traits.preview), `bad preview ${traits.preview}`); assert.ok(STYLE_MODES.includes(traits.styleMode), `bad styleMode ${traits.styleMode}`); assert.ok(COMMENT_SYNTAXES.includes(traits.commentSyntax), `bad commentSyntax ${traits.commentSyntax}`); assert.equal(typeof traits.styleTag, 'string'); assert.equal(typeof traits.injectScriptAttrs, 'string'); } }); }); } it('keeps shared extensions in agreement across entries', () => { // `.tsx` belongs to tanstack-start, nextjs and vite-generic. resolveSourceTraits // returns the first claimant, so a disagreement would resolve by array // position instead of by intent. const seen = new Map(); for (const framework of FRAMEWORKS) { for (const ext of framework.source.extensions) { const traits = { ...SOURCE_TRAIT_DEFAULTS, ...framework.source }; delete traits.extensions; const prior = seen.get(ext); if (!prior) { seen.set(ext, { name: framework.name, traits }); continue; } assert.deepEqual( traits, prior.traits, `${framework.name} and ${prior.name} both claim ${ext} but disagree on its traits`, ); } } }); it('falls back to the defaults for an unclaimed extension', () => { const traits = resolveSourceTraits('src/helpers.ts'); assert.equal(traits.framework, null); assert.equal(traits.preview, 'source'); assert.equal(traits.styleMode, 'scoped'); assert.equal(traits.commentSyntax, 'html'); assert.equal(traits.injectScriptAttrs, ''); }); it('routes the three authoring modes live-wrap depends on', () => { assert.equal(resolveSourceTraits('src/routes/+page.svelte').preview, 'component'); assert.equal(resolveSourceTraits('src/pages/index.astro').styleMode, 'astro-global-prefixed'); assert.equal(resolveSourceTraits('src/pages/index.astro').injectScriptAttrs, 'is:inline '); assert.equal(resolveSourceTraits('app/page.tsx').commentSyntax, 'jsx'); assert.equal(resolveSourceTraits('index.html').commentSyntax, 'html'); assert.equal(resolveSourceTraits('app/app.vue').styleMode, 'scoped'); }); it('registers the generic tag undo', () => { assert.equal(typeof PATCH_UNDOERS[TAG_PATCH_KIND], 'function'); }); }); // --------------------------------------------------------------------------- // Detection against the static fixtures (read-only) // --------------------------------------------------------------------------- describe('framework registry — fixture detection', () => { /** * `sveltekit` is deliberately absent: that fixture is a bare src/app.html + * one route with no svelte.config and no package.json, and the SvelteKit * detector has always required one of those. It resolves to static-html and * takes the generic tag path, which is exactly what live-inject did before * the registry. `vite8-sveltekit` is the fully-shaped SvelteKit fixture. */ const EXPECTED = { 'vite8-sveltekit': 'sveltekit', 'vite8-sveltekit-stateful': 'sveltekit', 'sveltekit-csp': 'sveltekit', 'nuxt-vite7': 'nuxt', 'nuxt-csp': 'nuxt', 'tanstack-start': 'tanstack-start', // A plain TanStack Router SPA has no @tanstack/react-start and a static // index.html, so it must NOT take the SSR adapter. 'tanstack-router-vite': 'vite-generic', astro: 'astro', 'astro-vite7': 'astro', 'nextjs-app': 'nextjs', 'nextjs-app-router': 'nextjs', 'vite8-react-plain': 'vite-generic', 'vite8-react-ts': 'vite-generic', 'multipage-with-generator': 'static-html', }; for (const [name, expected] of Object.entries(EXPECTED)) { it(`${name} resolves to ${expected}`, (t) => { const project = fixtureProject(name); if (!project) { t.skip(`fixture ${name} is not present`); return; } const resolved = resolveFramework(project.root, project.config); assert.ok(resolved, 'a framework always resolves'); assert.equal(resolved.framework.name, expected); assert.ok(resolved.project, 'detect returns a truthy project descriptor'); }); } it('the bare sveltekit fixture keeps the pre-registry generic path', () => { const project = fixtureProject('sveltekit'); if (!project) return; const resolved = resolveFramework(project.root, project.config); assert.equal(resolved.framework.name, 'static-html'); assert.equal(resolved.framework.inject.kind, 'tag'); }); it('adapters ask for the gitignore patterns their generated paths need', () => { const nuxt = fixtureProject('nuxt-vite7'); const resolvedNuxt = resolveFramework(nuxt.root, nuxt.config); assert.deepEqual(frameworkIgnorePatterns(resolvedNuxt), ['plugins/impeccable-live.client.ts']); const tanstack = fixtureProject('tanstack-start'); const resolvedTanstack = resolveFramework(tanstack.root, tanstack.config); assert.deepEqual( frameworkIgnorePatterns(resolvedTanstack), ['src/impeccable/ImpeccableLiveRoot.tsx'], ); const vite = fixtureProject('vite8-react-plain'); assert.deepEqual(frameworkIgnorePatterns(resolveFramework(vite.root, vite.config)), []); }); it('describes adapter artifacts as created files plus patched anchors', () => { const tanstack = fixtureProject('tanstack-start'); const resolved = resolveFramework(tanstack.root, tanstack.config); const artifacts = describeInjectArtifacts(resolved, { cwd: tanstack.root, files: [] }); assert.deepEqual(artifacts.map((a) => [a.kind, a.path]), [ ['created', 'src/impeccable/ImpeccableLiveRoot.tsx'], ['patched', 'src/routes/__root.tsx'], ]); for (const artifact of artifacts) { if (artifact.kind === 'patched') { assert.equal(typeof PATCH_UNDOERS[artifact.patch], 'function'); assert.ok(artifact.markers.length > 0, 'patched artifacts carry ownership markers'); } else { assert.ok(artifact.marker, 'created artifacts carry an ownership marker'); } } }); it('describes tag-strategy artifacts as one patch per resolved file', () => { const vite = fixtureProject('vite8-react-plain'); const resolved = resolveFramework(vite.root, vite.config); const artifacts = describeInjectArtifacts(resolved, { cwd: vite.root, files: ['index.html', 'other.html'] }); assert.deepEqual(artifacts.map((a) => a.path), ['index.html', 'other.html']); assert.ok(artifacts.every((a) => a.kind === 'patched' && a.patch === TAG_PATCH_KIND)); }); }); // --------------------------------------------------------------------------- // Crash-safe injection journal // --------------------------------------------------------------------------- const PRISTINE_HTML = '\n\n
\n