首页
/ Impeccable Live Mode 项目一次性配置详解:注入配置、框架适配器与 CSP 检测同意流程

Impeccable Live Mode 项目一次性配置详解:注入配置、框架适配器与 CSP 检测同意流程

2026-09-04 19:23:41作者:晏闻田Solitary

Impeccable 的 live 变体模式(live variant mode)允许你在浏览器中圈选元素、选择设计动作,并由 AI 生成 HTML+CSS 变体经 HMR 热切换。首次进入 live 模式前,live.mjs 引导阶段若报告 config_missing / config_invalid、存在 configDrift 或配置缺少 cspChecked,就需要执行一次性项目配置。本文以 live-setup.md 为主体,结合 live-inject.mjsdetect-csp.mjs 等源码,完整讲解配置文件的每个字段、各框架的注入目标差异、配置漂移检测与 CSP 同意补丁流程,读完即可独立为任意前端项目完成 live 模式初始化。

何时进入一次性配置流程

live-setup.md 明确说明:该流程不是每会话热路径的一部分,只在 live.md 所描述的会话启动中由以下三种情况触发:

  • live.mjs 引导输出 { ok: false, error: "config_missing", ... }config_invalid
  • 引导输出携带非空的 configDrift 需要处理;
  • 配置已存在但缺少 cspChecked 字段。

从源码看,live.mjs 的第一步就是检查 .impeccable/live/config.json(其 --help 输出写明"config_missing"时打印 { ok: false, error: "config_missing", configPath, hint }),检查路径即引导输出中报告的 path(默认 .impeccable/live/config.json)。live-inject.mjs 中的 validateConfig 定义了 config_invalid 的判定标准:files 必须是非空字符串数组、exclude 若存在必须是字符串数组、insertBeforeinsertAfter 至少有一个、commentSyntax 只能是 htmljsxcspChecked 若存在必须是布尔值。

写入配置文件

完整配置结构与字段说明

在引导报告的 path 处创建配置文件,默认位置为 .impeccable/live/config.json

{
  "files": ["<path-or-glob>", "<path-or-glob>", ...],
  "exclude": ["<optional-glob>", ...],
  "insertBefore": "</body>",
  "commentSyntax": "html",
  "cspChecked": true
}

各字段的语义(含源码级印证):

  • files(注入目标):浏览器实际加载的 HTML 文件列表,条目可以是字面路径或 glob 模式。关键认知是:这里指向的是"浏览器实际加载的文件",而不是"源码文件"——跟踪 vs 生成在这里无关紧要,wrap 流程有自己的生成文件保护。live-inject.mjsresolveFiles 展示了展开逻辑:不含 * ? [ 的条目按字面路径直接透传(即使文件尚不存在也保留,由调用方逐条报告 file_not_found,且 exclude 列表对显式字面条目不生效——用户既然点名了就该尊重);含通配符的条目通过 fs.globSync 展开,逐条套用排除规则并去重,顺序按首次出现保持。
  • exclude(可选):跳过 files glob 本会包含的文件(如邮件模板、演示 fixture)。
  • insertBefore / insertAfter:注入锚点。insertBefore 在锚点行之前插入,insertAfter 在锚点行之后插入,二者必配其一(与 validateConfig 的校验一致)。</body> 几乎在所有文件中都存在,是最稳的默认锚点。
  • commentSyntax:取值 htmljsx,决定注入的标记注释使用哪种语法(HTML 注释 <!-- ... --> 还是 JSX 兼容形式),由目标文件的编写环境决定。
  • cspChecked:记录后文的 CSP 检测步骤已经执行过;首次配置时该字段缺省,检测流程走完后补写为 true

硬性排除路径与 glob 语法

两条硬性排除规则不可被配置覆盖**/node_modules/****/.git/**——向第三方代码注入脚本是不可接受的。这与 live-inject.mjs 中的 HARD_EXCLUDES 常量一一对应,源码注释强调"这些永远不是用户页面,匹配它们会悄悄向第三方代码注入跟踪脚本,用户无法通过配置关闭它们——这是底线"。

Glob 语法约定:** 匹配任意数量的路径段(含零段)、* 在单个段内匹配、? 匹配单个字符;所有路径相对于项目根目录,且统一使用正斜杠。

各框架的 files 配置对照表

原文档给出的框架对照表是实操核心,完整保留如下:

Framework files insertBefore commentSyntax
SPA with single shell (Vite / React / Plain HTML) ["index.html"] </body> html
Next.js (App Router) ["app/layout.tsx"] </body> jsx
Next.js (Pages) ["pages/_document.tsx"] </body> jsx
Nuxt ["app.vue"] </body> html
Svelte / SvelteKit ["src/app.html"] </body> html
TanStack Router (SPA, Vite) ["index.html"] </body> html
TanStack Start (SSR) ["src/routes/__root.tsx"] <Scripts jsx
Astro ["<root layout .astro>"] </body> html
Multi-page (separate HTML per route) ["public/**/*.html"] glob over the served dir </body> html

锚点选择原则:选一个在每个目标文件中都存在的锚点(</body> 几乎总能工作);多页站点优先使用 glob,这样新增页面会被自动纳入。一个重要的时效性限制:如果页面的 HTML 由生成器重建(generator rebuild),注入只会存活到下一次重新生成为止——每次构建后重新运行 live.mjs 即可;这不影响 accept 流程,因为 accept 走 fallback 流程写入的是真正的源码。

框架适配器:注入时自动检测

每一次注入都会把写入了什么记录到 .impeccable/live/inject-journal.json;下一次注入或移除会借此修复崩溃或"错误目录退出"留下的残留。live-inject.mjs 的注入路径印证了这一点:写入前先 healInjectJournal(保留本次将要拥有的工件,使重复注入保持字节级幂等),成功后 recordInjection 落盘。

SvelteKit、Nuxt、TanStack Start 三种框架在服务端渲染整个文档外壳,在入口模板里放一个裸 <script> 不能可靠执行。live-inject.mjs 会检测到它们并路由到专用适配器:

  • SvelteKit:从 +layout.svelte 派生的 dev-only 根组件;
  • Nuxt:dev-only 的 .client.ts 插件;
  • TanStack Start:生成一个 dev-only 的 ImpeccableLiveRoot 组件挂到 __root

在适配器路径下,files 值仍是一个有效的检测/CSP 提示(detection/CSP hint),但不再是字面的插入位置。普通 TanStack Router SPA(纯客户端)则走基线 Vite 路径。

框架检测的优先级在 live/frameworks/index.mjs 中显式声明:SvelteKit → Nuxt → TanStack Start → Astro → Next.js → Vite 通用 → 静态 HTML,首个匹配的 detect() 胜出;由于静态 HTML 永远匹配,resolveFramework 实际上不会返回空。

配置漂移(Config Drift)检测

每次启动时,live.mjsscanForDrift 会扫描常见页面根目录(public/src/app/pages/)下的 HTML 文件,找出未被解析后的 files 列表覆盖的"孤儿"文件,以 configDrift.orphans 加上 hint 的形式上报;没有漂移时 configDriftnull。源码中的实现细节值得注意:

  • 扫描跳过 node_modules.next.nuxt.svelte-kit.astrodistbuild 等构建产物目录;
  • 匹配用户 exclude glob 的文件视为有意排除,不算漂移,保证孤儿列表保持信号密度;
  • 孤儿列表最多展示 20 条(orphans.slice(0, 20)),并附 orphanCount 与 hint 文案(建议追加条目或改用 "public/**/*.html" 这类 glob)。

处理原则与原文档一致:每个会话只向用户说明一次哪些文件未被覆盖,并提供"把它们加入 files 或把 files 切换为 glob"两个选项;绝不自动更新配置——决定权在用户手中。

CSP 检测(仅首次执行)

若配置中 cspChecked === true,跳过整个 CSP 小节——用户已经被问过这一次了。首次检测运行:

node .agent/skills/impeccable/scripts/detect-csp.mjs

输出 { shape, signals }。注意 shape 命名的是补丁机制而非框架来源,因此一个模板可以覆盖多个框架。detect-csp.mjs 的头部注释完整定义了五种形态:

  • null:未检测到 CSP 信号;把配置写成 cspChecked: true 并结束。
  • append-arrays:CSP 以结构化指令数组表达,可自动补丁(monorepo helper 的 additionalScriptSrc/additionalConnectSrc、SvelteKit 的 kit.csp.directives、Nuxt 的 nuxt-security)。
  • append-string:CSP 以字面量字符串表达,可自动补丁(next.config.* 中内联的 headers()、Nuxt routeRules)。
  • middleware / meta-tag:能检测到但不自动补丁。展示检测到的文件,请用户手动把 http://localhost:8400 加入 script-srcconnect-src,然后标记 cspChecked: true 继续。

检测器的实现边界

detect-csp.mjs 源码看,这是一次纯机械的 grep 式扫描:无网络、无 dev server、无 JS 求值。它递归遍历项目树(跳过 .next.turbo.svelte-kit.nuxt.astrodistbuildout.vercel 等目录,MAX_DEPTH = 6,每个文件最多读 64KB),仅检查 .js/.mjs/.cjs/.ts/.mts/.cts/.tsx/.jsx 等配置类扩展名与 .tsx/.jsx/.astro/.vue/.svelte/.html 等布局类扩展名。各类信号由正则组表达,例如 monorepo helper 信号包含 buildCSPConfigadditionalScriptSrccreateBaseNextConfig 等;middleware 形态匹配 headers.set("Content-Security-Policy"),meta-tag 形态匹配 http-equiv="Content-Security-Policy"

判定的优先级写死在 detect-csp.mjsappend-arrays > append-string > middleware > meta-tag。源码注释解释了理由:结构化补丁比字符串拼接更安全,运行时与 HTML 注入式补丁可靠性差,v1 不自动应用。

同意提示词(Consent Prompt)

对可自动补丁的形态,向用户展示如下固定措辞:

CSP patch needed. I detected a Content Security Policy in your project that blocks http://localhost:8400: the live picker won't load without an allowance. Here's the change I'd make:

[file: <patchTarget>]
[exact diff, 2-5 lines]

It's guarded by NODE_ENV === "development" so the extra entry only appears in dev and never reaches production. You can remove it any time by reverting this file. Apply? [y/n]

用户答 "no":跳过补丁,注明在手动加入放行前 live 模式无法工作,但仍然写入 cspChecked: true(问题已经问过,不应重复询问)。用户答 "yes":应用对应形态的补丁,然后写入 cspChecked: true

形态一:append-arrays

在持有 CSP 数组的文件顶部附近声明一个 dev-only 常量,然后在 script-src 与 connect-src 数组中追加 ...__impeccableLiveDev

// Dev-only allowance so impeccable live mode can load. Guarded by NODE_ENV.
const __impeccableLiveDev =
  process.env.NODE_ENV === "development" ? ["http://localhost:8400"] : [];

各框架落点:

  • Next.js + monorepo helper:编辑应用自己的 next.config.*(不是共享 helper),向 additionalScriptSrc / additionalConnectSrc 追加;
  • SvelteKitsvelte.config.jskit.csp.directives['script-src']['connect-src']
  • Nuxt + nuxt-securitynuxt.config.*security.headers.contentSecurityPolicy['script-src']['connect-src']

仓库中附带了补丁后的参考输出,可直接比对目标形态:tests/framework-fixtures/nextjs-turborepo/expected-after-patch.ts 展示了 monorepo 场景下 additionalScriptSrc: [posthogHost, ...__impeccableLiveDev] 的写法;tests/framework-fixtures/sveltekit-csp/expected-after-patch.js 展示了 'script-src': ['self', 'unsafe-inline', ...__impeccableLiveDev] 的数组展开。

幂等性:若 __impeccableLiveDev 已存在于该文件,仍应用补丁流程,只需标记 cspChecked: true(即不重复追加、不重复询问)。

形态二:append-string

两点补丁(two-point patch):声明一个 dev-only 字符串,并在两条指令的 CSP 值中插值(字符串带前导空格以便干净拼接;把字面量改写成模板字符串是编辑的一部分):

// Dev-only allowance so impeccable live mode can load.
const __impeccableLiveDev =
  process.env.NODE_ENV === "development" ? " http://localhost:8400" : "";

转换示例:

  • script-src 'self' 'unsafe-inline' 变为 `script-src 'self' 'unsafe-inline'${__impeccableLiveDev}`
  • connect-src 'self' 变为 `connect-src 'self'${__impeccableLiveDev}`

各框架落点:Next.jsnext.config.* 内联 headers()Nuxtnuxt.config.*routeRules['/**'].headers['Content-Security-Policy']。参考输出见 tests/framework-fixtures/nextjs-inline-csp/expected-after-patch.jstests/framework-fixtures/nuxt-csp/expected-after-patch.ts——前者展示 next.config.jsheaders() 的模板字符串拼接,后者展示 Nuxt routeRules 场景(原字符串是 + 拼接时,需把 script-srcconnect-src 两行改写成含 ${__impeccableLiveDev} 的模板字符串)。

故障排查与收尾

原文档给出的两条排查规则:

  1. 用户拒绝 CSP 补丁后 live 不工作:其 dev CSP 仍然在拦截 http://localhost:8400。处理方式是删除 .impeccable/live/config.json 中的 cspChecked 字段并重新运行 live.mjs,setup 会重新发起询问。
  2. setup 完成后:重新运行 live.mjs 进入正常会话流程(引导成功输出 serverPortserverTokenpageFiles 等 JSON,随后按 live.md 的契约进入轮询循环)。

补充两个源码层面值得了解的行为:注入时 live-inject.mjs 会把 live 模式的运行时产物(inject-journal.jsonsessions/previews/ 等,见 LIVE_IGNORE_PATTERNS)写入一个由标记注释包裹的 gitignore 块,优先落在 .git/info/exclude,无 .git 时回退 .gitignore,确保临时状态不会污染版本库;移除注入(--remove)时同样会先 healInjectJournalclearInjectJournal,与注入路径形成完整的自愈闭环。这些机制共同保证了本文所描述的 setup 是一个低摩擦、可重入、可撤销的一次性过程。

登录后查看全文
热门项目推荐
相关项目推荐