diff --git a/package.json b/package.json index 0e5cba6e8..ee253fc6e 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "dev": "bun run server/index.js", "preview": "bun run build && wrangler pages dev", "deploy": "bun run build && wrangler pages deploy build/", - "test": "bun test tests/build.test.js tests/detect-antipatterns.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-server.test.mjs && node --test tests/framework-fixtures.test.mjs", + "test": "bun test tests/build.test.js tests/detect-antipatterns.test.js && node --test tests/detect-antipatterns-fixtures.test.mjs && node --test tests/detect-antipatterns-browser.test.mjs && node --test tests/cleanup-deprecated.test.mjs && node --test tests/live-wrap.test.mjs && node --test tests/live-accept.test.mjs && node --test tests/live-inject.test.mjs && node --test tests/live-server.test.mjs && node --test tests/framework-fixtures.test.mjs", "prepack": "cp README.md README.repo.md && cp README.npm.md README.md", "postpack": "cp README.repo.md README.md && rm README.repo.md", "screenshot": "bun run scripts/screenshot-antipatterns.js", diff --git a/source/skills/impeccable/scripts/live-inject.mjs b/source/skills/impeccable/scripts/live-inject.mjs index 0a08e0a7f..9bbc345ac 100644 --- a/source/skills/impeccable/scripts/live-inject.mjs +++ b/source/skills/impeccable/scripts/live-inject.mjs @@ -167,15 +167,21 @@ function insertTag(content, config, port) { /** * Remove the live script block. Matches either HTML or JSX comment markers * regardless of config (so stale tags from a wrong config can still be cleaned). + * + * Indent-preserving: captures any whitespace immediately preceding the opener + * marker and re-emits it in place of the removed block. `insertTag` inserted + * the block *after* the original line's indent and *before* the anchor (e.g. + * `
+`), which moved the indent onto the opener line and left the anchor + * unindented. Replacing the whole block (plus its trailing newline) with just + * the captured indent hands the indent back to the anchor that follows. */ function removeTag(content, _syntax) { - // Two patterns: HTML comment markers or JSX comment markers, with any content between. const patterns = [ - /\n?[\s\S]*?\n?/, - /\n?\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}\n?/, + /([ \t]*)[\s\S]*?[ \t]*\n/, + /([ \t]*)\{\/\*\s*impeccable-live-start\s*\*\/\}[\s\S]*?\{\/\*\s*impeccable-live-end\s*\*\/\}[ \t]*\n/, ]; for (const pat of patterns) { - const next = content.replace(pat, '\n'); + const next = content.replace(pat, '$1'); if (next !== content) return next; } return content; diff --git a/tests/live-inject.test.mjs b/tests/live-inject.test.mjs new file mode 100644 index 000000000..49f8d2c62 --- /dev/null +++ b/tests/live-inject.test.mjs @@ -0,0 +1,154 @@ +/** + * Tests for live-inject.mjs — script-tag insert/remove round-trip. + * Run with: node --test tests/live-inject.test.mjs + */ + +import { describe, it, beforeEach, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; +import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; +import { dirname, join, resolve } from 'node:path'; +import { tmpdir } from 'node:os'; +import { fileURLToPath } from 'node:url'; +import { execFileSync } from 'node:child_process'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const INJECT = resolve(__dirname, '..', 'source/skills/impeccable/scripts/live-inject.mjs'); + +function runInject(cwd, configPath, args) { + try { + const out = execFileSync('node', [INJECT, ...args], { + cwd, + encoding: 'utf-8', + env: { ...process.env, IMPECCABLE_LIVE_CONFIG: configPath }, + stdio: ['ignore', 'pipe', 'pipe'], + }); + return JSON.parse(out.trim()); + } catch (err) { + const body = err.stdout?.toString().trim() || err.stderr?.toString().trim() || ''; + return JSON.parse(body || '{}'); + } +} + +describe('live-inject — insert/remove round-trip preserves file bytes', () => { + let tmp; + beforeEach(() => { tmp = mkdtempSync(join(tmpdir(), 'impeccable-inject-test-')); }); + afterEach(() => { rmSync(tmp, { recursive: true, force: true }); }); + + it('round-trips an HTML file without mangling indentation', () => { + const original = ` + +
+
+