Improve Live polling responsiveness and reliability

Restore foreground/background polling as the primary harness architecture, add progressive publication and framework-safe previews, and harden quality and regression coverage. The experimental app-server runtime is intentionally excluded.\n\nPrepared with AI assistance under maintainer direction.
This commit is contained in:
Paul Bakaus
2026-07-15 17:05:30 -07:00
parent 8259c28209
commit c6ac34b929
74 changed files with 8511 additions and 333 deletions
+15
View File
@@ -118,6 +118,15 @@ export function validateEvent(msg) {
return 'checkpoint: paramValues must be an object';
}
return null;
case 'agent_phase':
if (!isValidId(msg.id)) return 'agent_phase: missing or malformed id';
if (typeof msg.phase !== 'string' || !/^[a-z][a-z0-9_]{1,63}$/.test(msg.phase)) {
return 'agent_phase: missing or malformed phase';
}
if (msg.durationMs !== undefined && (!Number.isFinite(msg.durationMs) || msg.durationMs < 0)) {
return 'agent_phase: durationMs must be a non-negative number';
}
return null;
case 'exit':
return null;
case 'prefetch':
@@ -131,6 +140,12 @@ export function validateEvent(msg) {
if (msg.message.length > 4000) return 'steer: message too long';
if (msg.pageUrl !== undefined && typeof msg.pageUrl !== 'string') return 'steer: pageUrl must be string';
return null;
case 'carbonize_cleanup':
if (!isValidId(msg.id)) return 'carbonize_cleanup: missing or malformed id';
if (!isValidId(msg.sessionId)) return 'carbonize_cleanup: missing or malformed sessionId';
if (!msg.file || typeof msg.file !== 'string') return 'carbonize_cleanup: missing file';
if (!isValidVariantId(String(msg.variantId))) return 'carbonize_cleanup: missing or malformed variantId';
return null;
default:
return 'Unknown event type: ' + msg.type;
}
@@ -0,0 +1,93 @@
import { execFileSync } from 'node:child_process';
import path from 'node:path';
const PREFLIGHT_TIMEOUT_MS = 15_000;
export function buildGenerationPreflight(event, scriptsDir, { isolated = false } = {}) {
if (!event || event.type !== 'generate' || !event.id) return null;
const isInsert = event.mode === 'insert';
const target = isInsert ? insertTarget(event) : replaceTarget(event);
if (!target.elementId && !target.classes) return null;
const script = path.join(scriptsDir, isInsert ? 'live-insert.mjs' : 'live-wrap.mjs');
const args = [script, '--id', event.id, '--count', String(event.count || 3)];
if (!isInsert && isolated) args.push('--isolated');
if (isInsert) args.push('--position', target.position);
if (target.elementId) args.push('--element-id', target.elementId);
if (target.classes) args.push('--classes', target.classes);
if (target.tag) args.push('--tag', target.tag);
if (target.text) args.push('--text', target.text);
if (!isInsert && event.pageUrl) args.push('--page-url', event.pageUrl);
return { script, args, mode: isInsert ? 'insert' : 'replace' };
}
export function runGenerationPreflight(event, {
cwd = process.cwd(),
scriptsDir,
execFileSyncImpl = execFileSync,
timeoutMs = PREFLIGHT_TIMEOUT_MS,
isolated = false,
} = {}) {
const command = buildGenerationPreflight(event, scriptsDir, { isolated });
if (!command) {
return { ok: false, skipped: true, reason: 'insufficient_locator' };
}
const startedAt = performance.now();
try {
const stdout = execFileSyncImpl(process.execPath, command.args, {
cwd,
encoding: 'utf-8',
timeout: timeoutMs,
stdio: ['ignore', 'pipe', 'pipe'],
});
const line = String(stdout).trim().split('\n').filter(Boolean).pop();
if (!line) throw new Error('preflight returned no scaffold metadata');
return {
ok: true,
mode: command.mode,
durationMs: performance.now() - startedAt,
scaffold: JSON.parse(line),
};
} catch (error) {
return {
ok: false,
mode: command.mode,
durationMs: performance.now() - startedAt,
error: compactError(error),
};
}
}
function replaceTarget(event) {
return normalizeTarget(event.element || {});
}
function insertTarget(event) {
return {
...normalizeTarget(event.insert?.anchor || {}),
position: event.insert?.position === 'before' ? 'before' : 'after',
};
}
function normalizeTarget(target) {
const classes = Array.isArray(target.classes)
? target.classes.join(' ')
: String(target.classes || '').trim();
const text = typeof target.textContent === 'string'
? target.textContent.trim().slice(0, 80)
: '';
return {
elementId: target.id || target.elementId || undefined,
classes: classes || undefined,
tag: target.tagName || target.tag || undefined,
text: text || undefined,
};
}
function compactError(error) {
const stderr = error?.stderr ? String(error.stderr).trim() : '';
const message = stderr.split('\n').filter(Boolean).pop() || error?.message || 'preflight failed';
return String(message).slice(0, 500);
}
+617
View File
@@ -0,0 +1,617 @@
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { createLiveSessionStore } from './session-store.mjs';
import { withSourceLockSync } from './source-lock.mjs';
import { getLiveDir } from '../lib/impeccable-paths.mjs';
import {
SOURCE_ARTIFACT_PREVIEW_MODE,
findSourceArtifactManifest,
} from './source-artifact.mjs';
export function sha256(value) {
return createHash('sha256').update(value).digest('hex');
}
export function reconcilePublishedSourceVariants({ current, candidate, priorArrived = 0 } = {}) {
let reconciled = String(candidate || '');
const stable = String(current || '');
for (let variant = 1; variant <= Number(priorArrived || 0); variant += 1) {
const stableBlock = extractVariantBlock(stable, variant);
const candidateBlock = extractVariantBlock(reconciled, variant);
if (!stableBlock || !candidateBlock) {
return failure('published_variant_missing', { variant });
}
const offset = reconciled.indexOf(candidateBlock);
reconciled = reconciled.slice(0, offset) + stableBlock + reconciled.slice(offset + candidateBlock.length);
}
return { ok: true, content: reconciled };
}
export function prepareGenerationArtifact({ id, sourceFile, cwd = process.cwd() } = {}) {
if (!id) return failure('missing_session_id');
if (!sourceFile) return failure('missing_file');
const requestedPath = resolveInside(cwd, sourceFile);
if (!requestedPath || !fs.existsSync(requestedPath)) return failure(requestedPath ? 'source_missing' : 'path_outside_project');
const componentTarget = readComponentPublicationTarget(requestedPath, cwd, id);
if (componentTarget?.error) return componentTarget;
const sourceArtifactTarget = componentTarget ? null : readSourceArtifactPublicationTarget(requestedPath, cwd, id);
if (sourceArtifactTarget?.error) return sourceArtifactTarget;
const sourcePath = componentTarget?.sourcePath || sourceArtifactTarget?.sourcePath || requestedPath;
try {
return withSourceLockSync(sourcePath, 'generation-prepare:' + id, () => {
const store = createLiveSessionStore({ cwd, sessionId: id });
const snapshot = store.getSnapshot(id, { includeCompleted: true });
if (!snapshot?.updatedAt) return failure('session_missing');
if (snapshot.generationCanceled === true) {
return failure('stale_generation_epoch', { canceled: true, phase: snapshot.phase });
}
const source = fs.readFileSync(sourcePath, 'utf-8');
const artifactBase = sourceArtifactTarget
? fs.readFileSync(sourceArtifactTarget.previewPath, 'utf-8')
: source;
const revision = Number(snapshot.publishedRevision || 0) + 1;
const artifactDir = path.join(getLiveDir(cwd), 'artifacts');
if (componentTarget) {
return prepareComponentArtifact({
id,
revision,
snapshot,
source,
sourcePath,
requestedPath,
target: componentTarget,
artifactDir,
cwd,
});
}
const extension = path.extname(sourcePath) || '.html';
const artifactPath = path.join(artifactDir, id + '-r' + revision + extension);
fs.mkdirSync(artifactDir, { recursive: true });
fs.writeFileSync(artifactPath, artifactBase, 'utf-8');
return {
ok: true,
id,
epoch: Number(snapshot.generationEpoch || 1),
revision,
sourceFile: relative(cwd, sourcePath),
...(sourceArtifactTarget ? {
previewFile: relative(cwd, sourceArtifactTarget.previewPath),
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
} : {}),
artifactFile: relative(cwd, artifactPath),
expectedSourceHash: sha256(source),
};
}, { cwd });
} catch (error) {
if (error?.code === 'SOURCE_LOCKED') return failure('source_locked');
return failure('prepare_failed', { message: error?.message || String(error) });
}
}
export function publishGenerationArtifact({
id,
epoch,
sourceFile,
artifactFile,
expectedSourceHash,
arrivedVariants,
expectedVariants,
publicationKind,
cwd = process.cwd(),
} = {}) {
if (!id) return failure('missing_session_id');
if (!Number.isInteger(epoch) || epoch < 1) return failure('invalid_generation_epoch');
if (!sourceFile || !artifactFile) return failure('missing_file');
if (publicationKind && !['variants', 'params'].includes(publicationKind)) {
return failure('invalid_publication_kind');
}
const requestedPath = resolveInside(cwd, sourceFile);
const artifactPath = resolveInside(cwd, artifactFile);
if (!requestedPath || !artifactPath) return failure('path_outside_project');
if (!fs.existsSync(requestedPath)) return failure('source_missing');
if (!fs.existsSync(artifactPath)) return failure('artifact_missing');
const componentTarget = readComponentPublicationTarget(requestedPath, cwd, id);
if (componentTarget?.error) return componentTarget;
const sourceArtifactTarget = componentTarget ? null : readSourceArtifactPublicationTarget(requestedPath, cwd, id);
if (sourceArtifactTarget?.error) return sourceArtifactTarget;
const artifactManifest = readJson(artifactPath);
const isComponentArtifact = isComponentPreviewMode(artifactManifest?.previewMode);
if (Boolean(componentTarget) !== isComponentArtifact) {
return failure('artifact_preview_mode_mismatch');
}
if (componentTarget && componentTarget.manifest.previewMode !== artifactManifest?.previewMode) {
return failure('artifact_preview_mode_mismatch');
}
const sourcePath = componentTarget?.sourcePath || sourceArtifactTarget?.sourcePath || requestedPath;
try {
return withSourceLockSync(sourcePath, 'generation:' + id + ':' + epoch, () => {
const store = createLiveSessionStore({ cwd, sessionId: id });
const snapshot = store.getSnapshot(id, { includeCompleted: true });
if (!snapshot?.updatedAt) return failure('session_missing');
if (snapshot.generationCanceled === true) {
return failure('stale_generation_epoch', { canceled: true, phase: snapshot.phase });
}
if (Number(snapshot.generationEpoch || 1) !== epoch) {
return failure('stale_generation_epoch', { expectedEpoch: snapshot.generationEpoch || 1 });
}
const current = fs.readFileSync(sourcePath, 'utf-8');
const currentHash = sha256(current);
if (!expectedSourceHash || currentHash !== expectedSourceHash) {
return failure('source_hash_mismatch', { actualSourceHash: currentHash });
}
if (componentTarget) {
return publishComponentArtifact({
id,
epoch,
snapshot,
target: componentTarget,
artifactManifest,
artifactPath,
sourcePath,
arrivedVariants,
expectedVariants,
publicationKind,
store,
cwd,
});
}
const stablePreview = sourceArtifactTarget
? fs.readFileSync(sourceArtifactTarget.previewPath, 'utf-8')
: current;
const artifact = fs.readFileSync(artifactPath, 'utf-8');
if (!artifact.includes('data-impeccable-variants="' + id + '"')) {
return failure('artifact_missing_session_wrapper');
}
const delivered = countDeliveredVariants(artifact);
if (delivered < 1) return failure('artifact_has_no_variants');
if (Number.isInteger(arrivedVariants) && delivered < arrivedVariants) {
return failure('artifact_variant_count_mismatch', { delivered });
}
const priorArrived = Math.max(0, Number(snapshot.arrivedVariants || 0));
for (let variant = 1; variant <= priorArrived; variant++) {
const currentVariant = extractVariantBlock(stablePreview, variant);
const artifactVariant = extractVariantBlock(artifact, variant);
if (!currentVariant || !artifactVariant) {
return failure('published_variant_missing', { variant });
}
if (sha256(withoutVariantParams(currentVariant)) !== sha256(withoutVariantParams(artifactVariant))) {
return failure('published_variant_changed', { variant });
}
}
const currentPreviewCss = extractPreviewCss(stablePreview, id);
const artifactPreviewCss = extractPreviewCss(artifact, id);
if (priorArrived > 0 && currentPreviewCss && !artifactPreviewCss.startsWith(currentPreviewCss)) {
return failure('published_variant_css_changed');
}
const commitSnapshot = store.getSnapshot(id, { includeCompleted: true });
if (commitSnapshot?.generationCanceled === true) {
return failure('stale_generation_epoch', { canceled: true, phase: commitSnapshot.phase });
}
if (Number(commitSnapshot?.generationEpoch || 1) !== epoch) {
return failure('stale_generation_epoch', { expectedEpoch: commitSnapshot?.generationEpoch || 1 });
}
const artifactHash = sha256(artifact);
const publishPath = sourceArtifactTarget?.previewPath || sourcePath;
atomicReplace(publishPath, artifact);
const revision = Number(commitSnapshot.publishedRevision || 0) + 1;
store.appendEvent({
type: 'variant_published',
id,
generationEpoch: epoch,
revision,
digest: artifactHash,
sourceFile: relative(cwd, sourcePath),
...(sourceArtifactTarget ? {
previewFile: relative(cwd, publishPath),
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
} : {}),
arrivedVariants: delivered,
expectedVariants: Number(expectedVariants || snapshot.expectedVariants || delivered),
publicationKind: publicationKind || 'variants',
at: Date.now(),
});
return {
ok: true,
id,
epoch,
revision,
digest: artifactHash,
sourceFile: relative(cwd, sourcePath),
...(sourceArtifactTarget ? {
previewFile: relative(cwd, publishPath),
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
} : {}),
arrivedVariants: delivered,
expectedVariants: Number(expectedVariants || snapshot.expectedVariants || delivered),
publicationKind: publicationKind || 'variants',
};
}, { cwd });
} catch (error) {
if (error?.code === 'SOURCE_LOCKED') return failure('source_locked');
return failure('publish_failed', { message: error?.message || String(error) });
}
}
function prepareComponentArtifact({
id,
revision,
snapshot,
source,
sourcePath,
requestedPath,
target,
artifactDir,
cwd,
}) {
const artifactComponentDir = path.join(
artifactDir,
id + '-r' + revision + '-' + target.manifest.previewMode + '-' + process.pid + '-' + Date.now(),
);
fs.mkdirSync(artifactComponentDir, { recursive: true });
copyDirectoryFiles(target.componentPath, artifactComponentDir);
const artifactPath = path.join(artifactComponentDir, 'manifest.json');
const artifactManifest = {
...target.manifest,
componentDir: relative(cwd, artifactComponentDir),
};
fs.writeFileSync(artifactPath, JSON.stringify(artifactManifest, null, 2) + '\n', 'utf-8');
return {
ok: true,
id,
epoch: Number(snapshot.generationEpoch || 1),
revision,
sourceFile: relative(cwd, requestedPath),
targetSourceFile: relative(cwd, sourcePath),
artifactFile: relative(cwd, artifactPath),
componentDir: relative(cwd, artifactComponentDir),
previewMode: target.manifest.previewMode,
expectedSourceHash: sha256(source),
};
}
function publishComponentArtifact({
id,
epoch,
snapshot,
target,
artifactManifest,
artifactPath,
sourcePath,
arrivedVariants,
expectedVariants,
publicationKind,
store,
cwd,
}) {
if (!artifactManifest || typeof artifactManifest !== 'object') {
return failure('artifact_manifest_invalid');
}
if (artifactManifest.id !== id || target.manifest.id !== id) {
return failure('artifact_session_mismatch');
}
const artifactComponentPath = resolveInside(cwd, artifactManifest.componentDir);
if (!artifactComponentPath || path.resolve(artifactComponentPath) !== path.dirname(artifactPath)) {
return failure('artifact_component_dir_mismatch');
}
if (!isDescendant(path.join(getLiveDir(cwd), 'artifacts'), artifactComponentPath)) {
return failure('artifact_not_staged');
}
const immutableMismatch = componentManifestMismatch(target.manifest, artifactManifest);
if (immutableMismatch) {
return failure('artifact_manifest_changed', { field: immutableMismatch });
}
const expected = Number(expectedVariants || target.manifest.count || snapshot.expectedVariants || 0);
const declared = optionalPositiveInteger(artifactManifest.arrivedVariants);
const delivered = Number.isInteger(arrivedVariants) ? arrivedVariants : declared;
if (!Number.isInteger(delivered) || delivered < 1) return failure('artifact_has_no_variants');
if (expected > 0 && delivered > expected) {
return failure('artifact_variant_count_mismatch', { delivered, expected });
}
if (declared !== null && declared !== delivered) {
return failure('artifact_variant_count_mismatch', { delivered: declared, expected: delivered });
}
const priorArrived = Math.max(
optionalPositiveInteger(target.manifest.arrivedVariants) || 0,
Number(snapshot.arrivedVariants || 0),
);
if (delivered < priorArrived) {
return failure('artifact_variant_count_regressed', { delivered, priorArrived });
}
const componentExtension = target.manifest.componentExtension
|| (target.manifest.previewMode === 'vue-component' ? 'vue' : 'svelte');
const variantContents = [];
for (let variant = 1; variant <= delivered; variant++) {
const artifactVariantPath = path.join(artifactComponentPath, 'v' + variant + '.' + componentExtension);
if (!regularFileInside(artifactComponentPath, artifactVariantPath)) {
return failure('artifact_variant_missing', { variant });
}
const content = fs.readFileSync(artifactVariantPath, 'utf-8');
if (!content.trim()) return failure('artifact_variant_empty', { variant });
const targetVariantPath = path.join(target.componentPath, 'v' + variant + '.' + componentExtension);
if (variant <= priorArrived && !regularFileInside(target.componentPath, targetVariantPath)) {
return failure('published_variant_missing', { variant });
}
if (variant <= priorArrived) {
const prior = fs.readFileSync(targetVariantPath, 'utf-8');
if (sha256(prior) !== sha256(content)) {
return failure('published_variant_changed', { variant });
}
}
variantContents.push({ variant, content, targetPath: targetVariantPath });
}
const artifactParamsPath = path.join(artifactComponentPath, 'params.json');
let paramsContent = null;
if (fs.existsSync(artifactParamsPath)) {
if (!regularFileInside(artifactComponentPath, artifactParamsPath)) {
return failure('artifact_params_invalid');
}
paramsContent = fs.readFileSync(artifactParamsPath, 'utf-8');
const params = parseJson(paramsContent);
if (!params || typeof params !== 'object' || Array.isArray(params)) {
return failure('artifact_params_invalid');
}
}
// Components and optional params become reachable before the manifest
// advertises them. Committing the manifest last makes publication atomic
// from the browser's point of view while the source lock excludes Accept.
fs.mkdirSync(target.componentPath, { recursive: true });
for (const variant of variantContents) {
if (variant.variant > priorArrived) atomicReplace(variant.targetPath, variant.content);
}
if (paramsContent !== null) {
atomicReplace(path.join(target.componentPath, 'params.json'), paramsContent);
}
const commitSnapshot = store.getSnapshot(id, { includeCompleted: true });
if (commitSnapshot?.generationCanceled === true) {
return failure('stale_generation_epoch', { canceled: true, phase: commitSnapshot.phase });
}
if (Number(commitSnapshot?.generationEpoch || 1) !== epoch) {
return failure('stale_generation_epoch', { expectedEpoch: commitSnapshot?.generationEpoch || 1 });
}
const publishedManifest = {
...target.manifest,
componentDir: relative(cwd, target.componentPath),
arrivedVariants: delivered,
};
delete publishedManifest.manifestPath;
const manifestContent = JSON.stringify(publishedManifest, null, 2) + '\n';
atomicReplace(target.manifestPath, manifestContent);
const digest = digestComponentPublication(manifestContent, variantContents, paramsContent);
const revision = Number(snapshot.publishedRevision || 0) + 1;
const sourceFile = relative(cwd, sourcePath);
const previewFile = relative(cwd, target.manifestPath);
store.appendEvent({
type: 'variant_published',
id,
generationEpoch: epoch,
revision,
digest,
sourceFile,
previewFile,
previewMode: target.manifest.previewMode,
arrivedVariants: delivered,
expectedVariants: expected || delivered,
publicationKind: publicationKind || 'variants',
at: Date.now(),
});
return {
ok: true,
id,
epoch,
revision,
digest,
sourceFile,
previewFile,
previewMode: target.manifest.previewMode,
componentDir: relative(cwd, target.componentPath),
arrivedVariants: delivered,
expectedVariants: expected || delivered,
publicationKind: publicationKind || 'variants',
};
}
const COMPONENT_MANIFEST_FIELDS = [
'id',
'mode',
'previewMode',
'sourceFile',
'sourceStartLine',
'sourceEndLine',
'insertLine',
'position',
'anchorStartLine',
'anchorEndLine',
'count',
'propContract',
'originalMarkup',
'anchorMarkup',
'runtimeModule',
'componentModuleBase',
'framework',
'componentExtension',
];
function readComponentPublicationTarget(manifestPath, cwd, id) {
if (path.basename(manifestPath) !== 'manifest.json') return null;
const manifest = readJson(manifestPath);
if (!manifest || !isComponentPreviewMode(manifest.previewMode)) return null;
if (manifest.id !== id) return failure('artifact_session_mismatch');
const sourcePath = resolveInside(cwd, manifest.sourceFile);
const componentPath = resolveInside(cwd, manifest.componentDir);
if (!sourcePath || !componentPath) return failure('path_outside_project');
if (!fs.existsSync(sourcePath)) return failure('source_missing');
if (path.resolve(componentPath) !== path.dirname(manifestPath)) {
return failure('manifest_component_dir_mismatch');
}
return { manifest, manifestPath, sourcePath, componentPath };
}
function readSourceArtifactPublicationTarget(requestedPath, cwd, id) {
const manifest = findSourceArtifactManifest(id, cwd);
if (!manifest) return null;
if (path.resolve(requestedPath) !== path.resolve(manifest.previewPath)) {
return failure('source_artifact_preview_mismatch');
}
return manifest;
}
function componentManifestMismatch(target, artifact) {
for (const field of COMPONENT_MANIFEST_FIELDS) {
if (JSON.stringify(target[field] ?? null) !== JSON.stringify(artifact[field] ?? null)) return field;
}
return null;
}
function isComponentPreviewMode(value) {
return value === 'svelte-component' || value === 'vue-component';
}
function copyDirectoryFiles(sourceDir, targetDir) {
for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
if (!entry.isFile() || entry.isSymbolicLink()) continue;
fs.copyFileSync(path.join(sourceDir, entry.name), path.join(targetDir, entry.name));
}
}
function regularFileInside(root, file) {
const rel = path.relative(root, file);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return false;
try {
return fs.lstatSync(file).isFile();
} catch {
return false;
}
}
function isDescendant(root, candidate) {
const rel = path.relative(root, candidate);
return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
}
function digestComponentPublication(manifestContent, variants, paramsContent) {
const hash = createHash('sha256');
hash.update(manifestContent);
for (const variant of variants) {
hash.update('\0v' + variant.variant + '\0');
hash.update(variant.content);
}
if (paramsContent !== null) hash.update('\0params\0' + paramsContent);
return hash.digest('hex');
}
function readJson(file) {
try {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch {
return null;
}
}
function parseJson(value) {
try {
return JSON.parse(value);
} catch {
return null;
}
}
function optionalPositiveInteger(value) {
const number = Number(value);
return Number.isInteger(number) && number > 0 ? number : null;
}
function countDeliveredVariants(source) {
const matches = source.match(/<div\b[^>]*\bdata-impeccable-variant=(?:"|')(?!original(?:"|'))[^"']+(?:"|')[^>]*>/g);
return matches?.length || 0;
}
function extractVariantBlock(source, variant) {
const open = /<div\b[^>]*>/gi;
let match;
let start = -1;
const attr = new RegExp("\\bdata-impeccable-variant=(?:\"" + variant + "\"|'" + variant + "')");
while ((match = open.exec(source))) {
if (attr.test(match[0])) {
start = match.index;
break;
}
}
if (start < 0) return null;
const token = /<div\b[^>]*\/\s*>|<div\b[^>]*>|<\/div\s*>/gi;
token.lastIndex = start;
let depth = 0;
while ((match = token.exec(source))) {
if (/^<\/div/i.test(match[0])) {
depth -= 1;
if (depth === 0) return source.slice(start, token.lastIndex);
} else if (!/\/\s*>$/.test(match[0])) {
depth += 1;
}
}
return null;
}
function withoutVariantParams(block) {
return String(block || '').replace(
/\sdata-impeccable-params=(?:"[^"]*"|'[^']*')/i,
'',
);
}
function extractPreviewCss(source, id) {
const escapedId = String(id).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const open = new RegExp("<style\\b[^>]*\\bdata-impeccable-css=(?:\"" + escapedId + "\"|'" + escapedId + "')[^>]*>", 'i');
const match = open.exec(source);
if (!match) return '';
const start = match.index + match[0].length;
const end = source.indexOf('</style>', start);
if (end < 0) return '';
return source.slice(start, end)
.replace(/^\s*\{\s*`\s*/, '')
.replace(/\s*`\s*\}\s*$/, '')
.trim();
}
function atomicReplace(target, content) {
let mode = 0o666;
try { mode = fs.statSync(target).mode; } catch {}
const temp = target + '.impeccable-publish-' + process.pid + '-' + Date.now();
try {
fs.writeFileSync(temp, content, { encoding: 'utf-8', mode });
fs.renameSync(temp, target);
} finally {
try { fs.unlinkSync(temp); } catch {}
}
}
function resolveInside(cwd, value) {
const resolved = path.resolve(cwd, value);
const rel = path.relative(cwd, resolved);
if (rel.startsWith('..') || path.isAbsolute(rel)) return null;
return resolved;
}
function relative(cwd, value) {
return path.relative(cwd, value).split(path.sep).join('/');
}
function failure(error, details = {}) {
return { ok: false, error, ...details };
}
+14
View File
@@ -0,0 +1,14 @@
export function eventPriority(event = {}) {
if (event.type === 'accept' || event.type === 'discard' || event.type === 'exit') return 0;
if (event.type === 'manual_edit_apply' || event.type === 'steer' || event.type === 'carbonize_cleanup') return 1;
if (event.type === 'generate') return 2;
return 3;
}
export function selectAvailablePendingEvent(entries, { now = Date.now(), types = null } = {}) {
const allowed = types instanceof Set ? types : (Array.isArray(types) ? new Set(types) : null);
return entries
.filter((entry) => !(entry.leaseUntil && entry.leaseUntil > now))
.filter((entry) => !allowed || allowed.has(entry.event?.type))
.sort((a, b) => eventPriority(a.event) - eventPriority(b.event) || a.seq - b.seq)[0] || null;
}
+137 -14
View File
@@ -3,6 +3,13 @@ import path from 'node:path';
import { getLegacyLiveSessionsDir, getLiveSessionsDir } from '../lib/impeccable-paths.mjs';
const COMPLETED_PHASES = new Set(['completed', 'discarded']);
const GENERATION_FENCED_PHASES = new Set([
'accept_requested',
'discard_requested',
'carbonize_required',
'completed',
'discarded',
]);
export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {}) {
const rootDir = getLiveSessionsDir(cwd);
@@ -38,7 +45,10 @@ export function createLiveSessionStore({ cwd = process.cwd(), sessionId } = {})
if (!fs.existsSync(journalPath) && fs.existsSync(legacyJournalPath)) {
fs.copyFileSync(legacyJournalPath, journalPath);
}
const prior = loadCachedOrRebuild(normalized.id);
// Publisher/complete helpers can append from a separate process while
// the server is alive. Rebuild here so sequence numbers and phase
// fences never come from a stale in-memory cache.
const prior = rebuildSnapshotFromJournal(getReadableJournalPath(normalized.id), normalized.id);
const seq = prior.nextSeq;
const entry = {
seq,
@@ -116,9 +126,21 @@ function baseSnapshot(id) {
pendingEvent: null,
deliveryLease: null,
checkpointRevision: 0,
browserCheckpointRevision: 0,
publicationCheckpointRevision: 0,
activeOwner: null,
sourceMarkers: {},
fallbackMode: null,
generationPhase: null,
generationTimings: {},
generationEpoch: 1,
publishedRevision: 0,
deliveredVariants: {},
variantPlan: null,
paramsPublished: false,
generationCanceled: false,
generationCanceledAt: null,
cancelReason: null,
annotationArtifacts: [],
diagnostics: [],
updatedAt: null,
@@ -158,6 +180,9 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
...snapshot,
paramValues: { ...(snapshot.paramValues || {}) },
sourceMarkers: { ...(snapshot.sourceMarkers || {}) },
generationTimings: { ...(snapshot.generationTimings || {}) },
deliveredVariants: { ...(snapshot.deliveredVariants || {}) },
variantPlan: snapshot.variantPlan || null,
annotationArtifacts: [...(snapshot.annotationArtifacts || [])],
diagnostics: [...(snapshot.diagnostics || [])],
updatedAt: entry.ts || new Date().toISOString(),
@@ -170,14 +195,81 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
switch (event.type) {
case 'generate':
next.phase = 'generate_requested';
next.generationEpoch = Number(event.generationEpoch || next.generationEpoch || 1);
next.pageUrl = event.pageUrl ?? next.pageUrl;
next.expectedVariants = event.count ?? next.expectedVariants;
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
next.pendingEvent = toPendingEvent(event);
next.variantPlan = null;
if (event.screenshotPath) upsertArtifact(next.annotationArtifacts, { type: 'screenshot', path: event.screenshotPath });
break;
case 'variant_plan':
if (!next.generationCanceled && !GENERATION_FENCED_PHASES.has(next.phase)) {
next.variantPlan = event.plan ?? next.variantPlan;
}
break;
case 'detector_waivers':
if (!next.generationCanceled && !GENERATION_FENCED_PHASES.has(next.phase)) {
next.detectorWaivers = [
...(next.detectorWaivers || []),
...(Array.isArray(event.waivers) ? event.waivers : []),
];
}
break;
case 'variant_published':
if (next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase)) {
next.diagnostics.push({
error: 'late_generation_event_ignored',
type: event.type,
phase: next.phase,
revision: event.revision ?? null,
});
break;
}
if (Number(event.generationEpoch || 0) !== Number(next.generationEpoch || 1)) {
next.diagnostics.push({
error: 'stale_generation_epoch_ignored',
epoch: event.generationEpoch ?? null,
expectedEpoch: next.generationEpoch || 1,
});
break;
}
next.phase = 'variants_progress';
next.publishedRevision = Math.max(next.publishedRevision || 0, Number(event.revision || 0));
next.arrivedVariants = Math.max(next.arrivedVariants || 0, Number(event.arrivedVariants || 0));
next.expectedVariants = Number(event.expectedVariants || next.expectedVariants || 0);
if (event.publicationKind === 'params') next.paramsPublished = true;
next.sourceFile = event.sourceFile ?? next.sourceFile;
next.previewFile = event.previewFile ?? next.previewFile;
next.previewMode = event.previewMode ?? next.previewMode;
if (event.revision) {
next.deliveredVariants[String(event.revision)] = {
digest: event.digest || null,
arrivedVariants: Number(event.arrivedVariants || 0),
publishedAt: event.at || null,
};
}
break;
case 'agent_phase':
next.generationPhase = event.phase ?? next.generationPhase;
if (event.phase) {
next.generationTimings[event.phase] = {
at: event.at ?? (Date.parse(entry.ts || '') || null),
durationMs: event.durationMs ?? null,
};
}
break;
case 'variants_ready':
case 'agent_done':
if ((next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase))
&& !(event.type === 'agent_done' && event.carbonize === true && next.phase === 'accept_requested')) {
next.diagnostics.push({
error: 'late_generation_event_ignored',
type: event.type,
phase: next.phase,
});
break;
}
next.phase = event.carbonize === true ? 'carbonize_required' : 'variants_ready';
next.sourceFile = event.sourceFile ?? event.file ?? next.sourceFile;
next.previewFile = event.previewFile ?? next.previewFile;
@@ -194,27 +286,45 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
}
break;
case 'checkpoint':
if (COMPLETED_PHASES.has(next.phase)) {
if (next.generationCanceled || GENERATION_FENCED_PHASES.has(next.phase)) {
next.diagnostics.push({ error: 'checkpoint_after_terminal_ignored', phase: event.phase ?? null, revision: event.revision ?? null });
break;
}
if ((event.revision ?? 0) >= (next.checkpointRevision ?? 0)) {
next.phase = event.phase ?? next.phase;
next.checkpointRevision = event.revision ?? next.checkpointRevision;
next.activeOwner = event.owner ?? next.activeOwner;
next.arrivedVariants = event.arrivedVariants ?? next.arrivedVariants;
next.visibleVariant = event.visibleVariant ?? next.visibleVariant;
next.sourceFile = event.sourceFile ?? next.sourceFile;
next.previewFile = event.previewFile ?? next.previewFile;
next.previewMode = event.previewMode ?? next.previewMode;
if (event.paramValues) next.paramValues = { ...event.paramValues };
} else {
next.diagnostics.push({ error: 'stale_checkpoint_ignored', revision: event.revision });
{
const revisionDomain = event.revisionDomain === 'publication'
|| (event.reason === 'variants_progress' && !event.owner)
? 'publication'
: 'browser';
const revisionField = revisionDomain === 'publication'
? 'publicationCheckpointRevision'
: 'browserCheckpointRevision';
const currentRevision = next[revisionField]
?? (revisionDomain === 'browser' ? next.checkpointRevision : 0)
?? 0;
if ((event.revision ?? 0) >= currentRevision) {
next.phase = event.phase ?? next.phase;
next[revisionField] = event.revision ?? currentRevision;
if (revisionDomain === 'browser') {
next.checkpointRevision = event.revision ?? next.checkpointRevision;
next.activeOwner = event.owner ?? next.activeOwner;
}
next.arrivedVariants = event.arrivedVariants ?? next.arrivedVariants;
if (revisionDomain === 'browser') next.visibleVariant = event.visibleVariant ?? next.visibleVariant;
next.sourceFile = event.sourceFile ?? next.sourceFile;
next.previewFile = event.previewFile ?? next.previewFile;
next.previewMode = event.previewMode ?? next.previewMode;
if (revisionDomain === 'browser' && event.paramValues) next.paramValues = { ...event.paramValues };
} else {
next.diagnostics.push({ error: 'stale_checkpoint_ignored', revision: event.revision, revisionDomain });
}
}
break;
case 'accept':
case 'accept_intent':
next.phase = 'accept_requested';
next.generationCanceled = true;
next.generationCanceledAt = event.at ?? (Date.parse(entry.ts || '') || Date.now());
next.cancelReason = 'accept';
next.visibleVariant = Number(event.variantId ?? next.visibleVariant);
if (event.paramValues) next.paramValues = { ...event.paramValues };
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
@@ -232,6 +342,12 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
next.pendingEvent = toPendingEvent(event);
break;
case 'carbonize_cleanup':
next.phase = 'carbonize_cleanup_requested';
next.sourceFile = event.file ?? next.sourceFile;
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
next.pendingEvent = toPendingEvent(event);
break;
case 'steer_done':
next.phase = 'steer_done';
next.sourceFile = event.sourceFile ?? event.file ?? next.sourceFile;
@@ -243,6 +359,9 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
break;
case 'discard':
next.phase = 'discard_requested';
next.generationCanceled = true;
next.generationCanceledAt = event.at ?? (Date.parse(entry.ts || '') || Date.now());
next.cancelReason = 'discard';
next.pendingEventSeq = entry.seq ?? next.pendingEventSeq;
next.pendingEvent = toPendingEvent(event);
break;
@@ -260,6 +379,10 @@ function applyEvent(snapshot, entry, inheritedDiagnostics = []) {
next.pendingEvent = null;
break;
case 'agent_error':
if (next.generationCanceled && event.sourceEventType === 'generate') {
next.diagnostics.push({ error: 'late_generation_event_ignored', type: event.type, phase: next.phase });
break;
}
next.phase = 'agent_error';
next.pendingEventSeq = null;
next.pendingEvent = null;
+76
View File
@@ -0,0 +1,76 @@
import fs from 'node:fs';
import path from 'node:path';
import { getLiveDir } from '../lib/impeccable-paths.mjs';
export const SOURCE_ARTIFACT_PREVIEW_MODE = 'source-artifact';
export function scaffoldSourceArtifactSession({
id,
count,
sourceFile,
sourceStartLine,
sourceEndLine,
originalSource,
previewContent,
cwd = process.cwd(),
} = {}) {
if (!/^[A-Za-z0-9_-]{1,128}$/.test(String(id || ''))) {
throw new Error('invalid source artifact session id');
}
const sourcePath = resolveInside(cwd, sourceFile);
if (!sourcePath || !fs.existsSync(sourcePath)) throw new Error('source artifact target missing');
const sessionDir = path.join(getLiveDir(cwd), 'previews', id);
const extension = path.extname(sourcePath) || '.html';
const previewPath = path.join(sessionDir, 'preview' + extension);
const manifestPath = path.join(sessionDir, 'manifest.json');
fs.mkdirSync(sessionDir, { recursive: true });
const manifest = {
id,
count: Number(count || 1),
previewMode: SOURCE_ARTIFACT_PREVIEW_MODE,
sourceFile: relative(cwd, sourcePath),
previewFile: relative(cwd, previewPath),
sourceStartLine: Number(sourceStartLine),
sourceEndLine: Number(sourceEndLine),
originalSource: String(originalSource || ''),
};
fs.writeFileSync(previewPath, String(previewContent || ''), 'utf-8');
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
return { ...manifest, manifestFile: relative(cwd, manifestPath), sessionDir: relative(cwd, sessionDir) };
}
export function findSourceArtifactManifest(id, cwd = process.cwd()) {
if (!/^[A-Za-z0-9_-]{1,128}$/.test(String(id || ''))) return null;
const manifestPath = path.join(getLiveDir(cwd), 'previews', id, 'manifest.json');
let manifest;
try { manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8')); } catch { return null; }
if (manifest?.id !== id || manifest?.previewMode !== SOURCE_ARTIFACT_PREVIEW_MODE) return null;
const sourcePath = resolveInside(cwd, manifest.sourceFile);
const previewPath = resolveInside(cwd, manifest.previewFile);
if (!sourcePath || !previewPath || !fs.existsSync(sourcePath) || !fs.existsSync(previewPath)) return null;
return { ...manifest, manifestPath, sourcePath, previewPath };
}
export function removeSourceArtifactSession(id, cwd = process.cwd()) {
if (!/^[A-Za-z0-9_-]{1,128}$/.test(String(id || ''))) return false;
const sessionDir = path.join(getLiveDir(cwd), 'previews', id);
if (!fs.existsSync(sessionDir)) return false;
fs.rmSync(sessionDir, { recursive: true, force: true });
return true;
}
function resolveInside(cwd, value) {
if (!value || typeof value !== 'string') return null;
const root = path.resolve(cwd);
const resolved = path.resolve(root, value);
const rel = path.relative(root, resolved);
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
return resolved;
}
function relative(cwd, value) {
return path.relative(cwd, value).split(path.sep).join('/');
}
+56
View File
@@ -0,0 +1,56 @@
import fs from 'node:fs';
import path from 'node:path';
import { createHash } from 'node:crypto';
import { getLiveDir } from '../lib/impeccable-paths.mjs';
const STALE_LOCK_MS = 60_000;
export function sourceLockPath(file, cwd = process.cwd()) {
const digest = createHash('sha256').update(path.resolve(cwd, file)).digest('hex').slice(0, 24);
return path.join(getLiveDir(cwd), 'locks', digest + '.lock');
}
export function withSourceLockSync(file, owner, fn, {
cwd = process.cwd(),
waitMs = 0,
retryMs = 5,
} = {}) {
const lockPath = sourceLockPath(file, cwd);
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
const deadline = Date.now() + Math.max(0, Number(waitMs) || 0);
let fd;
while (fd === undefined) {
clearStaleLock(lockPath);
try {
fd = fs.openSync(lockPath, 'wx');
fs.writeFileSync(fd, JSON.stringify({ owner, pid: process.pid, at: Date.now(), file: path.resolve(cwd, file) }) + '\n');
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
if (Date.now() >= deadline) {
const locked = new Error('source_locked');
locked.code = 'SOURCE_LOCKED';
locked.lockPath = lockPath;
throw locked;
}
sleepSync(Math.max(1, Math.min(Number(retryMs) || 5, deadline - Date.now())));
}
}
try {
return fn();
} finally {
try { if (fd !== undefined) fs.closeSync(fd); } catch {}
try { fs.unlinkSync(lockPath); } catch {}
}
}
function sleepSync(ms) {
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
}
function clearStaleLock(lockPath) {
try {
const stat = fs.statSync(lockPath);
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) fs.unlinkSync(lockPath);
} catch {}
}
+343
View File
@@ -0,0 +1,343 @@
/**
* Nuxt/Vue live-mode component previews.
*
* Generation writes real Vue SFCs into a generated app-local module tree.
* Nuxt/Vite compiles those modules without touching the active route; Accept
* is the only operation that writes the user's .vue source.
*/
import fs from 'node:fs';
import path from 'node:path';
const NUXT_CONFIG_RE = /^nuxt\.config\.(?:js|mjs|cjs|ts|mts|cts)$/;
export function detectNuxtVueProject(cwd = process.cwd()) {
const configFile = fs.readdirSync(cwd, { withFileTypes: true })
.find((entry) => entry.isFile() && NUXT_CONFIG_RE.test(entry.name))?.name;
if (!configFile) return null;
const config = fs.readFileSync(path.join(cwd, configFile), 'utf-8');
const srcDirMatch = config.match(/\bsrcDir\s*:\s*(['"])([^'"]+)\1/);
let appDir = fs.existsSync(path.join(cwd, 'app')) ? 'app' : '';
if (srcDirMatch) {
const candidate = path.posix.normalize(srcDirMatch[2].replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, ''));
if (candidate !== '..' && !candidate.startsWith('../') && !path.isAbsolute(candidate)) {
appDir = candidate === '.' ? '' : candidate;
}
}
const componentRoot = [appDir, '.impeccable-live'].filter(Boolean).join('/');
return { configFile, appDir, componentRoot };
}
export function shouldUseVueComponentInjection(filePath, cwd = process.cwd()) {
if (/^(0|false|no)$/i.test(process.env.IMPECCABLE_LIVE_VUE_COMPONENT || '')) return false;
return path.extname(filePath).toLowerCase() === '.vue' && !!detectNuxtVueProject(cwd);
}
export function vueComponentSessionDir(id, cwd = process.cwd()) {
const project = detectNuxtVueProject(cwd);
if (!project) throw new Error('Nuxt project not found');
return path.join(cwd, project.componentRoot, id);
}
export function vueManifestPathForSession(id, cwd = process.cwd()) {
return path.join(vueComponentSessionDir(id, cwd), 'manifest.json');
}
function ensureVueRuntime(cwd = process.cwd()) {
const project = detectNuxtVueProject(cwd);
if (!project) throw new Error('Nuxt project not found');
const rel = `${project.componentRoot}/__runtime.js`;
const file = path.join(cwd, rel);
fs.mkdirSync(path.dirname(file), { recursive: true });
const source = `import { createApp } from 'vue';\n\nexport function mount(Component, options = {}) {\n const app = createApp(Component, options.props || {});\n app.mount(options.target);\n return app;\n}\n\nexport async function unmount(app) {\n app?.unmount?.();\n}\n`;
if (!fs.existsSync(file) || fs.readFileSync(file, 'utf-8') !== source) fs.writeFileSync(file, source, 'utf-8');
return nuxtViteFsModulePath(file, cwd);
}
/**
* Nuxt mounts Vite beneath its build-assets base (normally `/_nuxt/`).
* Keep the manifest path base-agnostic and let the browser prepend the
* runtime's actual buildAssetsDir. A page-route URL such as
* `/app/.impeccable-live/x.vue` is handled by Nitro and returns HTML.
*/
export function nuxtViteFsModulePath(file, cwd = process.cwd()) {
const absolute = path.resolve(cwd, file).split(path.sep).join('/');
const relative = path.relative(cwd, absolute);
if (relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error('Nuxt live module must stay inside the project root');
}
return '/@fs/' + absolute.replace(/^\/+/, '');
}
export function extractVueExpressions(markup) {
const out = [];
const seen = new Set();
const re = /\{\{\s*([^{}]+?)\s*\}\}/g;
let match;
while ((match = re.exec(String(markup || '')))) {
const expr = match[1].trim();
if (!expr || seen.has(expr)) continue;
seen.add(expr);
out.push({ expr, token: match[0] });
}
return out;
}
function buildVuePropContract(expressions) {
return expressions.map(({ expr, token }, index) => ({
prop: derivePropName(expr, index),
expr,
placeholder: token,
// DOMParser sees Vue interpolation `{{ user.name }}` as text containing
// the inner `{ user.name }` token; preserve its whitespace for the
// browser's source-text → rendered-text map.
previewToken: token.slice(1, -1),
}));
}
function derivePropName(expr, index) {
const tail = expr.match(/(?:^|\.|\[)([A-Za-z_$][\w$]*)\s*\]?$/);
return tail?.[1] || `prop${index}`;
}
function substituteVueExpressions(markup, contract) {
let out = String(markup || '');
for (const entry of contract) out = out.split(entry.placeholder).join(`{{ ${entry.prop} }}`);
return out;
}
function buildVueVariantStub(variant, markup, contract) {
const props = contract.length > 0
? `<script setup>\ndefineProps({\n${contract.map((entry) => ` ${entry.prop}: { default: '' },`).join('\n')}\n});\n</script>\n\n`
: '';
return `${props}<template>\n${markup.trim()}\n</template>\n\n<style scoped>\n/* Variant ${variant}: add scoped CSS here */\n</style>\n`;
}
export function scaffoldVueComponentSession({
id,
count,
sourceFile,
sourceStartLine,
sourceEndLine,
originalLines,
cwd = process.cwd(),
}) {
const runtimeModule = ensureVueRuntime(cwd);
const dir = vueComponentSessionDir(id, cwd);
fs.mkdirSync(dir, { recursive: true });
const originalMarkup = originalLines.join('\n');
const propContract = buildVuePropContract(extractVueExpressions(originalMarkup));
const previewMarkup = substituteVueExpressions(originalMarkup, propContract);
const manifest = {
id,
previewMode: 'vue-component',
framework: 'vue',
componentExtension: 'vue',
sourceFile: sourceFile.split(path.sep).join('/'),
sourceStartLine,
sourceEndLine,
count,
propContract,
originalMarkup,
componentDir: path.relative(cwd, dir).split(path.sep).join('/'),
componentModuleBase: nuxtViteFsModulePath(dir, cwd),
runtimeModule,
};
fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
for (let variant = 1; variant <= count; variant++) {
const file = path.join(dir, `v${variant}.vue`);
if (!fs.existsSync(file)) fs.writeFileSync(file, buildVueVariantStub(variant, previewMarkup, propContract), 'utf-8');
}
return {
manifest,
manifestFile: path.relative(cwd, path.join(dir, 'manifest.json')).split(path.sep).join('/'),
componentDir: manifest.componentDir,
propContract,
};
}
export function findVueComponentManifest(id, cwd = process.cwd()) {
let direct;
try { direct = vueManifestPathForSession(id, cwd); } catch { return null; }
if (!fs.existsSync(direct)) return null;
try {
const manifest = JSON.parse(fs.readFileSync(direct, 'utf-8'));
return manifest?.id === id && manifest?.previewMode === 'vue-component'
? { ...manifest, manifestPath: direct }
: null;
} catch {
return null;
}
}
function parseVueSfc(source) {
const text = String(source || '');
const template = text.match(/<template\b[^>]*>([\s\S]*?)<\/template\s*>/i)?.[1]?.trim() || '';
const style = text.match(/<style\b[^>]*>([\s\S]*?)<\/style\s*>/i)?.[1]?.trim() || '';
return { template, cssLines: style ? style.split('\n').map((line) => line.trimEnd()) : [] };
}
function restoreVueExpressions(markup, contract) {
let out = String(markup || '');
for (const entry of contract || []) {
out = out.replace(new RegExp(`\\{\\{\\s*${escapeRegExp(entry.prop)}\\s*\\}\\}`, 'g'), entry.placeholder);
}
return out;
}
export function inlineVueComponentAccept(manifest, variantNum, cwd = process.cwd()) {
const sourcePath = resolveInside(cwd, manifest.sourceFile);
const componentDir = resolveInside(cwd, manifest.componentDir);
const variantPath = componentDir && path.join(componentDir, `v${variantNum}.vue`);
const resultBase = {
file: manifest.sourceFile,
sourceFile: manifest.sourceFile,
previewMode: 'vue-component',
componentDir: manifest.componentDir,
carbonize: false,
};
if (!sourcePath || !componentDir || !variantPath || !fs.existsSync(sourcePath) || !fs.existsSync(variantPath)) {
return { handled: false, error: `Variant ${variantNum} not found`, ...resultBase };
}
const { template, cssLines } = parseVueSfc(fs.readFileSync(variantPath, 'utf-8'));
if (!template) return { handled: false, error: 'Accepted Vue variant has no template', ...resultBase };
if (/\bdata-impeccable-[\w-]*\s*=/.test(template)) {
return { handled: false, error: 'Accepted Vue variant contains preview-only attributes', ...resultBase };
}
const sourceLines = fs.readFileSync(sourcePath, 'utf-8').split('\n');
const start = Number(manifest.sourceStartLine) - 1;
const end = Number(manifest.sourceEndLine) - 1;
if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end < start || end >= sourceLines.length) {
return { handled: false, error: 'Invalid source line range for ' + manifest.sourceFile, ...resultBase };
}
const indent = sourceLines[start].match(/^(\s*)/)?.[1] || '';
const mergedTemplate = mergeOriginalVueAttrs(template, manifest.originalMarkup || '');
const markupLines = restoreVueExpressions(mergedTemplate, manifest.propContract)
.split('\n')
.map((line) => line.trim() ? indent + line.trimStart() : '');
let next = [...sourceLines.slice(0, start), ...markupLines, ...sourceLines.slice(end + 1)];
const meaningfulCss = cssLines.filter((line) => line.trim() && !/^\/\*\s*Variant \d+:/.test(line.trim()));
if (meaningfulCss.length > 0) next = appendVueStyle(next, meaningfulCss);
fs.writeFileSync(sourcePath, next.join('\n'), 'utf-8');
retireVueComponentSession(manifest.id, cwd);
return { handled: true, ...resultBase };
}
function appendVueStyle(lines, cssLines) {
let close = -1;
for (let index = lines.length - 1; index >= 0; index--) {
if (/<\/style\s*>/.test(lines[index])) { close = index; break; }
}
const block = ['', ...cssLines.map((line) => line.trim() ? ' ' + line.trimStart() : '')];
if (close < 0) return [...lines, '', '<style scoped>', ...block.slice(1), '</style>'];
return [...lines.slice(0, close), ...block, ...lines.slice(close)];
}
function mergeOriginalVueAttrs(markup, originalMarkup) {
const variant = matchOpeningTag(markup);
const original = matchOpeningTag(originalMarkup);
if (!variant || !original || variant.tag.toLowerCase() !== original.tag.toLowerCase()) return markup;
const variantAttrs = parseStaticAttrs(variant.attrs);
const originalAttrs = parseStaticAttrs(original.attrs);
const additions = [];
let attrs = variant.attrs;
const originalClass = originalAttrs.get('class');
const variantClass = variantAttrs.get('class');
if (originalClass && variantClass) {
const classes = [
...variantClass.value.split(/\s+/),
...originalClass.value.split(/\s+/),
].filter(Boolean);
const replacement = `class=${variantClass.quote}${[...new Set(classes)].join(' ')}${variantClass.quote}`;
attrs = attrs.slice(0, variantClass.start) + replacement + attrs.slice(variantClass.end);
} else if (originalClass) {
additions.push(originalClass.raw);
}
for (const [name, attr] of originalAttrs) {
if (name === 'class' || variantAttrs.has(name)) continue;
additions.push(attr.raw);
}
const open = `<${variant.tag}${attrs}${additions.map((attr) => ' ' + attr.trim()).join('')}${variant.close}`;
return markup.slice(0, variant.index) + open + markup.slice(variant.index + variant.raw.length);
}
function matchOpeningTag(markup) {
const match = String(markup || '').match(/<([A-Za-z][\w:-]*)([^>]*?)(\/?>)/);
return match ? {
raw: match[0],
tag: match[1],
attrs: match[2] || '',
close: match[3],
index: match.index || 0,
} : null;
}
function parseStaticAttrs(attrs) {
const out = new Map();
const re = /([A-Za-z_:][\w:.-]*)\s*=\s*(["'])(.*?)\2/g;
let match;
while ((match = re.exec(attrs))) {
out.set(match[1], {
raw: match[0],
value: match[3],
quote: match[2],
start: match.index,
end: match.index + match[0].length,
});
}
return out;
}
export function removeVueComponentSession(id, cwd = process.cwd()) {
try { fs.rmSync(vueComponentSessionDir(id, cwd), { recursive: true, force: true }); } catch { /* best effort */ }
}
/**
* Make an accepted/discarded session undiscoverable immediately while keeping
* Vue modules that Vite has in its graph alive until Live shuts down. Deleting
* an imported SFC mid-session makes Nuxt's HMR client attempt to reload a
* missing module and emit a console error. The generated directory remains
* ignored and removeAllVueComponentSessions removes it on server shutdown.
*/
export function retireVueComponentSession(id, cwd = process.cwd()) {
let dir;
try { dir = vueComponentSessionDir(id, cwd); } catch { return; }
for (const name of ['manifest.json', 'params.json']) {
try { fs.rmSync(path.join(dir, name), { force: true }); } catch { /* best effort */ }
}
}
export function removeAllVueComponentSessions(cwd = process.cwd()) {
const project = detectNuxtVueProject(cwd);
if (!project) return;
const root = path.join(cwd, project.componentRoot);
if (!fs.existsSync(root)) return;
fs.rmSync(root, { recursive: true, force: true });
}
export function buildVueComponentCssAuthoring(count) {
return {
mode: 'vue-component',
count,
requirements: [
'Write each variant as a real Vue SFC in componentDir/vN.vue.',
'Keep one root element inside <template> and put variant CSS in <style scoped>.',
'Keep propContract bindings as {{ propName }} instead of snapshot text.',
'Do not add data-impeccable-* attributes.',
],
forbidden: ['Rewriting sourceFile during preview', 'data-impeccable-* attributes', 'Off-brand replacement content'],
};
}
function resolveInside(cwd, value) {
if (!value || path.isAbsolute(value)) return null;
const full = path.resolve(cwd, value);
const rel = path.relative(cwd, full);
return !rel || rel.startsWith('..') || path.isAbsolute(rel) ? null : full;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}