mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 15:46:30 +03:00
Refresh the Impeccable product experience
Rework the landing page proof, steering demo, feature grid, slop catalog, detector coverage, theming, Live workflow, and responsive behavior.\n\nAI-assisted implementation by OpenAI Codex.
This commit is contained in:
@@ -27,8 +27,6 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_PATH = resolveLiveConfigPath({ cwd: process.cwd(), scriptsDir: __dirname });
|
||||
const MARKER_OPEN_TEXT = 'impeccable-live-start';
|
||||
const MARKER_CLOSE_TEXT = 'impeccable-live-end';
|
||||
const NUXT_PLUGIN_MARKER = 'impeccable-live-nuxt-plugin';
|
||||
const NUXT_PLUGIN_NAME = 'impeccable-live.client.ts';
|
||||
const IGNORE_MARKER_OPEN = '# impeccable-live-ignore-start';
|
||||
const IGNORE_MARKER_CLOSE = '# impeccable-live-ignore-end';
|
||||
|
||||
@@ -37,14 +35,9 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
|
||||
'.impeccable/hook.pending.json',
|
||||
'.impeccable/config.local.json',
|
||||
'.impeccable/live/server.json',
|
||||
'.impeccable/live/codex-worker.json',
|
||||
'.impeccable/live/codex-worker.log',
|
||||
'.impeccable/live/sessions/',
|
||||
'.impeccable/live/previews/',
|
||||
'.impeccable/live/annotations/',
|
||||
'.impeccable/live/artifacts/',
|
||||
'.impeccable/live/accept-receipts/',
|
||||
'.impeccable/live/locks/',
|
||||
'.impeccable/live/cache/',
|
||||
'.impeccable/live/manual-edit-apply-transaction.json',
|
||||
'.impeccable/live/manual-edit-events.jsonl',
|
||||
@@ -53,15 +46,10 @@ export const LIVE_IGNORE_PATTERNS = Object.freeze([
|
||||
'.impeccable/live/deferred-svelte-component-accepts.json',
|
||||
'.impeccable-live.json',
|
||||
'.impeccable-live/',
|
||||
'app/.impeccable-live/',
|
||||
'src/.impeccable-live/',
|
||||
'node_modules/.impeccable-live/',
|
||||
'src/lib/impeccable/ImpeccableLiveRoot.svelte',
|
||||
'src/lib/impeccable/__runtime.js',
|
||||
'src/lib/impeccable/[0-9a-f]*/',
|
||||
'plugins/impeccable-live.client.ts',
|
||||
'app/plugins/impeccable-live.client.ts',
|
||||
'src/plugins/impeccable-live.client.ts',
|
||||
]);
|
||||
|
||||
/**
|
||||
@@ -125,7 +113,6 @@ Output (JSON):
|
||||
|
||||
const resolvedFiles = resolveFiles(process.cwd(), config);
|
||||
const svelteKit = detectSvelteKitProject(process.cwd(), config);
|
||||
const nuxt = detectNuxtProject(process.cwd());
|
||||
|
||||
if (args.includes('--remove')) {
|
||||
if (svelteKit) {
|
||||
@@ -133,12 +120,6 @@ Output (JSON):
|
||||
console.log(JSON.stringify({ ok: true, adapter: 'sveltekit', results: [adapterResult] }));
|
||||
return;
|
||||
}
|
||||
if (nuxt) {
|
||||
const adapterResult = removeNuxtLiveAdapter({ cwd: process.cwd(), project: nuxt });
|
||||
console.log(JSON.stringify({ ok: !adapterResult.error, adapter: 'nuxt', results: [adapterResult] }));
|
||||
if (adapterResult.error) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
const results = resolvedFiles.map((relFile) => {
|
||||
const absFile = path.resolve(process.cwd(), relFile);
|
||||
if (!fs.existsSync(absFile)) return { file: relFile, error: 'file_not_found' };
|
||||
@@ -164,28 +145,13 @@ Output (JSON):
|
||||
console.error(JSON.stringify({ ok: false, error: 'missing_port' }));
|
||||
process.exit(1);
|
||||
}
|
||||
const gitIgnore = ensureLiveGitIgnores(
|
||||
process.cwd(),
|
||||
nuxt ? [nuxt.pluginFile] : [],
|
||||
);
|
||||
const gitIgnore = ensureLiveGitIgnores(process.cwd());
|
||||
|
||||
if (svelteKit) {
|
||||
const adapterResult = applySvelteKitLiveAdapter({ cwd: process.cwd(), port, config });
|
||||
console.log(JSON.stringify({ ok: true, port, adapter: 'sveltekit', gitIgnore, results: [adapterResult] }));
|
||||
return;
|
||||
}
|
||||
if (nuxt) {
|
||||
const adapterResult = applyNuxtLiveAdapter({ cwd: process.cwd(), port, project: nuxt });
|
||||
console.log(JSON.stringify({
|
||||
ok: !adapterResult.error,
|
||||
port,
|
||||
adapter: 'nuxt',
|
||||
gitIgnore,
|
||||
results: [adapterResult],
|
||||
}));
|
||||
if (adapterResult.error) process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const results = resolvedFiles.map((relFile) => {
|
||||
const absFile = path.resolve(process.cwd(), relFile);
|
||||
@@ -209,12 +175,12 @@ Output (JSON):
|
||||
if (!anyInserted) process.exit(1);
|
||||
}
|
||||
|
||||
export function ensureLiveGitIgnores(cwd = process.cwd(), extraPatterns = []) {
|
||||
export function ensureLiveGitIgnores(cwd = process.cwd()) {
|
||||
const target = resolveIgnoreTarget(cwd);
|
||||
const existing = fs.existsSync(target.path) ? fs.readFileSync(target.path, 'utf-8') : '';
|
||||
const block = [
|
||||
IGNORE_MARKER_OPEN,
|
||||
...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns]),
|
||||
...LIVE_IGNORE_PATTERNS,
|
||||
IGNORE_MARKER_CLOSE,
|
||||
].join('\n');
|
||||
const markerRe = new RegExp(`${escapeRegExp(IGNORE_MARKER_OPEN)}[\\s\\S]*?${escapeRegExp(IGNORE_MARKER_CLOSE)}`);
|
||||
@@ -236,119 +202,10 @@ export function ensureLiveGitIgnores(cwd = process.cwd(), extraPatterns = []) {
|
||||
file: path.relative(cwd, target.path).split(path.sep).join('/'),
|
||||
mode: target.mode,
|
||||
changed: updated !== existing,
|
||||
patterns: [...new Set([...LIVE_IGNORE_PATTERNS, ...extraPatterns])],
|
||||
patterns: [...LIVE_IGNORE_PATTERNS],
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Nuxt adapter
|
||||
//
|
||||
// A script element placed in app.vue is compiled as Vue-rendered DOM and is
|
||||
// not executed. Nuxt instead auto-discovers client plugins. Keep the adapter
|
||||
// generated, dev-only, and outside user-authored source: Live creates one
|
||||
// marked .client.ts plugin on start and removes it on stop.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function detectNuxtProject(cwd = process.cwd()) {
|
||||
const configFile = fs.readdirSync(cwd, { withFileTypes: true })
|
||||
.find((entry) => entry.isFile() && /^nuxt\.config\.(?:js|mjs|cjs|ts|mts|cts)$/.test(entry.name))
|
||||
?.name;
|
||||
if (!configFile) return null;
|
||||
|
||||
const config = fs.readFileSync(path.join(cwd, configFile), 'utf-8');
|
||||
const literalSrcDir = config.match(/\bsrcDir\s*:\s*(['"])([^'"]+)\1/);
|
||||
let appDir = '';
|
||||
if (literalSrcDir) {
|
||||
const candidate = literalSrcDir[2]
|
||||
.replace(/\\/g, '/')
|
||||
.replace(/^\.\//, '')
|
||||
.replace(/\/+$/, '');
|
||||
const normalized = path.posix.normalize(candidate);
|
||||
if (normalized !== '..' && !normalized.startsWith('../') && !path.isAbsolute(normalized)) {
|
||||
appDir = normalized === '.' ? '' : normalized;
|
||||
}
|
||||
} else if (
|
||||
fs.existsSync(path.join(cwd, 'app', 'app.vue'))
|
||||
|| fs.existsSync(path.join(cwd, 'app', 'pages'))
|
||||
) {
|
||||
appDir = 'app';
|
||||
}
|
||||
|
||||
const pluginFile = [appDir, 'plugins', NUXT_PLUGIN_NAME].filter(Boolean).join('/');
|
||||
return { configFile, appDir, pluginFile };
|
||||
}
|
||||
|
||||
export function buildNuxtPlugin(port) {
|
||||
return `/* ${NUXT_PLUGIN_MARKER} */
|
||||
const liveSrc = 'http://localhost:${port}/live.js';
|
||||
const liveSelector = 'script[data-impeccable-live-nuxt]';
|
||||
|
||||
export default defineNuxtPlugin(() => {
|
||||
if (!import.meta.dev || typeof document === 'undefined') return;
|
||||
|
||||
const expectedSrc = new URL(liveSrc, window.location.href).href;
|
||||
let script = document.querySelector(liveSelector);
|
||||
if (script?.src === expectedSrc) return;
|
||||
script?.remove();
|
||||
|
||||
script = document.createElement('script');
|
||||
script.src = liveSrc;
|
||||
script.async = true;
|
||||
script.dataset.impeccableLiveNuxt = '';
|
||||
document.head.appendChild(script);
|
||||
|
||||
import.meta.hot?.dispose(() => {
|
||||
if (script?.isConnected) script.remove();
|
||||
});
|
||||
});
|
||||
/* /${NUXT_PLUGIN_MARKER} */
|
||||
`;
|
||||
}
|
||||
|
||||
export function applyNuxtLiveAdapter({ cwd = process.cwd(), port, project = detectNuxtProject(cwd) }) {
|
||||
if (!project) return { error: 'nuxt_not_detected' };
|
||||
const absFile = path.join(cwd, project.pluginFile);
|
||||
const existing = fs.existsSync(absFile) ? fs.readFileSync(absFile, 'utf-8') : null;
|
||||
if (existing !== null && !existing.includes(NUXT_PLUGIN_MARKER)) {
|
||||
return {
|
||||
file: project.pluginFile,
|
||||
error: 'nuxt_plugin_conflict',
|
||||
hint: `${project.pluginFile} already exists and is not managed by Impeccable Live`,
|
||||
};
|
||||
}
|
||||
|
||||
const content = buildNuxtPlugin(port);
|
||||
fs.mkdirSync(path.dirname(absFile), { recursive: true });
|
||||
if (content !== existing) fs.writeFileSync(absFile, content, 'utf-8');
|
||||
return {
|
||||
file: project.pluginFile,
|
||||
inserted: true,
|
||||
changed: content !== existing,
|
||||
devOnly: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function removeNuxtLiveAdapter({ cwd = process.cwd(), project = detectNuxtProject(cwd) }) {
|
||||
if (!project) return { error: 'nuxt_not_detected' };
|
||||
const absFile = path.join(cwd, project.pluginFile);
|
||||
if (!fs.existsSync(absFile)) {
|
||||
return { file: project.pluginFile, removed: false, note: 'no adapter present' };
|
||||
}
|
||||
const content = fs.readFileSync(absFile, 'utf-8');
|
||||
if (!content.includes(NUXT_PLUGIN_MARKER)) {
|
||||
return {
|
||||
file: project.pluginFile,
|
||||
removed: false,
|
||||
error: 'nuxt_plugin_conflict',
|
||||
hint: `${project.pluginFile} is not managed by Impeccable Live`,
|
||||
};
|
||||
}
|
||||
fs.unlinkSync(absFile);
|
||||
const pluginDir = path.dirname(absFile);
|
||||
if (fs.readdirSync(pluginDir).length === 0) fs.rmdirSync(pluginDir);
|
||||
return { file: project.pluginFile, removed: true };
|
||||
}
|
||||
|
||||
function resolveIgnoreTarget(cwd) {
|
||||
const gitExcludePath = resolveGitInfoExcludePath(cwd);
|
||||
if (gitExcludePath) {
|
||||
|
||||
Reference in New Issue
Block a user