import assert from 'node:assert/strict';
import { afterEach, beforeEach, describe, it } from 'node:test';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { createLiveSessionStore } from '../skill/scripts/live/session-store.mjs';
import {
inlineVueComponentAccept,
nuxtViteFsModulePath,
removeAllVueComponentSessions,
scaffoldVueComponentSession,
} from '../skill/scripts/live/vue-component.mjs';
describe('Nuxt Vue component preview', () => {
let tmp;
let source;
beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), 'impeccable-vue-component-'));
source = join(tmp, 'app', 'pages', 'index.vue');
mkdirSync(join(tmp, 'app', 'pages'), { recursive: true });
writeFileSync(join(tmp, 'nuxt.config.ts'), 'export default defineNuxtConfig({ ssr: false });\n');
writeFileSync(source, [
'',
' ',
' Hello {{ user.name }}
',
' ',
'',
'',
'',
'',
].join('\n'));
});
afterEach(() => rmSync(tmp, { recursive: true, force: true }));
it('stages real Vue SFCs without rewriting the active route', () => {
const before = readFileSync(source, 'utf-8');
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 3,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: ['
Hello {{ user.name }}
'],
cwd: tmp,
});
assert.equal(readFileSync(source, 'utf-8'), before);
assert.equal(result.manifest.previewMode, 'vue-component');
assert.equal(result.manifest.componentExtension, 'vue');
assert.match(result.manifestFile, /^app\/\.impeccable-live\/vue12345\/manifest\.json$/);
const variant = readFileSync(join(tmp, result.componentDir, 'v1.vue'), 'utf-8');
assert.match(variant, //);
assert.match(variant, /Hello \{\{ name \}\}/);
assert.equal(existsSync(join(tmp, 'app/.impeccable-live/__runtime.js')), true);
assert.equal(
result.manifest.runtimeModule,
nuxtViteFsModulePath(join(tmp, 'app/.impeccable-live/__runtime.js'), tmp),
);
assert.equal(
result.manifest.componentModuleBase,
nuxtViteFsModulePath(join(tmp, result.componentDir), tmp),
);
assert.match(result.manifest.runtimeModule, /^\/@fs\//);
assert.doesNotMatch(result.manifest.runtimeModule, /^\/app\//);
assert.match(result.manifest.componentModuleBase, /^\/@fs\//);
});
it('keeps Vite module URLs valid for literal Nuxt srcDir projects', () => {
writeFileSync(join(tmp, 'nuxt.config.ts'), "export default defineNuxtConfig({ srcDir: 'client/' });\n");
const clientSource = join(tmp, 'client', 'pages', 'index.vue');
mkdirSync(join(tmp, 'client', 'pages'), { recursive: true });
writeFileSync(clientSource, 'Client app
\n');
const result = scaffoldVueComponentSession({
id: 'clientsrc',
count: 1,
sourceFile: 'client/pages/index.vue',
sourceStartLine: 1,
sourceEndLine: 1,
originalLines: ['Client app
'],
cwd: tmp,
});
assert.match(result.manifestFile, /^client\/\.impeccable-live\/clientsrc\/manifest\.json$/);
assert.match(result.manifest.runtimeModule, /^\/@fs\/.*\/client\/\.impeccable-live\/__runtime\.js$/);
assert.match(result.manifest.componentModuleBase, /^\/@fs\/.*\/client\/\.impeccable-live\/clientsrc$/);
});
it('accepts one generated SFC into clean Vue source and restores route expressions', () => {
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 3,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: [' Hello {{ user.name }}
'],
cwd: tmp,
});
writeFileSync(join(tmp, result.componentDir, 'v1.vue'), [
'',
'',
' Welcome {{ name }}
',
'',
'',
'',
].join('\n'));
const accepted = inlineVueComponentAccept(result.manifest, 1, tmp);
assert.equal(accepted.handled, true);
const next = readFileSync(source, 'utf-8');
assert.match(next, /Welcome \{\{ user\.name \}\}/);
assert.match(next, /class="hero-title variant-one"|class="variant-one hero-title"/);
assert.match(next, /\.variant-one \{ letter-spacing: 0\.02em; \}/);
assert.doesNotMatch(next, /data-impeccable/);
assert.equal(existsSync(join(tmp, result.componentDir, 'manifest.json')), false);
assert.equal(existsSync(join(tmp, result.componentDir, 'v1.vue')), true, 'imported SFC remains until Live shutdown');
});
it('preserves original root directives and valueless attrs a variant omits', () => {
const originalRoot = ' ';
writeFileSync(source, [
'',
' ',
originalRoot,
' ',
'',
'',
].join('\n'));
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 1,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: [originalRoot],
cwd: tmp,
});
// A restyle variant that keeps only class: every behavior attribute must survive Accept.
writeFileSync(join(tmp, result.componentDir, 'v1.vue'), [
'',
' ',
'',
'',
'',
].join('\n'));
assert.equal(inlineVueComponentAccept(result.manifest, 1, tmp).handled, true);
const next = readFileSync(source, 'utf-8');
assert.match(next, /@click="submit"/, 'v-on shorthand must not degrade to a literal click attribute');
assert.doesNotMatch(next, /\sclick="submit"/, 'sigil-stripped event handler leaked into source');
assert.match(next, /:aria-label="label"/);
assert.match(next, /v-bind:title="tip"/);
assert.match(next, /\bdisabled\b/, 'valueless boolean attr dropped');
assert.match(next, /\bv-cloak\b/, 'valueless directive dropped');
assert.match(next, /class="cta cta--bold"|class="cta--bold cta"/);
});
it('does not duplicate an attribute the variant wrote in the other shorthand form', () => {
const originalRoot = ' ';
writeFileSync(source, ['', ' ', originalRoot, ' ', '', ''].join('\n'));
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 1,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: [originalRoot],
cwd: tmp,
});
writeFileSync(join(tmp, result.componentDir, 'v1.vue'), [
'',
' ',
'',
'',
].join('\n'));
assert.equal(inlineVueComponentAccept(result.manifest, 1, tmp).handled, true);
const next = readFileSync(source, 'utf-8');
assert.doesNotMatch(next, /:aria-label="label"[^>]*v-bind:aria-label|v-bind:aria-label="label"[^>]*:aria-label/,
'shorthand and longhand of one attr both emitted, which is a Vue compile error');
});
it('removes deferred SFCs, the shared runtime, and the generated root on Live shutdown', () => {
const result = scaffoldVueComponentSession({
id: 'vue12345',
count: 1,
sourceFile: 'app/pages/index.vue',
sourceStartLine: 3,
sourceEndLine: 3,
originalLines: [' Hello {{ user.name }}
'],
cwd: tmp,
});
inlineVueComponentAccept(result.manifest, 1, tmp);
const root = join(tmp, 'app/.impeccable-live');
assert.equal(existsSync(join(root, '__runtime.js')), true);
assert.equal(existsSync(join(tmp, result.componentDir, 'v1.vue')), true);
removeAllVueComponentSessions(tmp);
assert.equal(existsSync(join(root, '__runtime.js')), false);
assert.equal(existsSync(root), false);
});
});