@adep/web-container 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * 产物导出(PV-006):把浏览器内 dev server 的 **blob 文档**序列化成一份可经真实 HTTP 源服务的
3
+ * 自包含产物(`{ html, modules, paths }`),交给平台预览子域。
4
+ *
5
+ * 背景(为什么需要它):Web IDE 的前端预览由 `@adep/web-container` 的 `vite` 命令在浏览器内编译,
6
+ * 产物是一个 **`blob:` 入口文档**——模块之间的 import 说明符在编译期被改写成彼此的 blob URL。
7
+ * blob URL 只在「创建它的那个标签页」里有效:复制出去在别的浏览器打开必然失效,且 blob 是不可
8
+ * 分层 scheme,`history.pushState('/about')` 这类单页路由调用直接抛错。于是「像 CodeSandbox 那样
9
+ * 复制预览链接」这件事在 blob 形态下不可能做到。
10
+ *
11
+ * 本模块的做法:**顺着 blob 引用抓一份完整模块图,改写成内容寻址的 HTTP 路径**——
12
+ * 1. 读入口文档 → 找出其中所有 `blob:` 引用(`<script src="blob:…">`);
13
+ * 2. 逐个读取这些 blob 的源码,再从中找出它们的 `blob:` 引用(编译期已改写为依赖的 URL)→ 递归,
14
+ * 直到闭包;
15
+ * 3. 每份源码按内容哈希命名(同内容必然同路径 → 跨版本复用、天然去重、变更必然换名),
16
+ * 路径形如 `/_adep/m/<hash>.js`(与服务端 `PREVIEW_MODULE_URL_RE` 白名单一致);
17
+ * 4. 把所有源码里的 blob URL 替换成对应路径,得到自包含产物。
18
+ *
19
+ * 增量发布(`known`):每次发布都把**完整闭包读一遍**(blob 就在页内存里,读取代价近乎为零),
20
+ * 但只把「服务端还没有的模块」放进产物体 `modules`——内容寻址下「同路径 ⇔ 同内容」,判据就是
21
+ * 「路径是否出现在上一次发布的映射值里」。产物另带 `paths`(本次入口所需的**全部**模块路径,
22
+ * 含未重复上传的那些),服务端据此写清单并清理幽灵模块:**清单必须完整**,漏报会把仍在用的
23
+ * 模块当幽灵删掉(页面白屏)。
24
+ *
25
+ * 为什么不干脆「已知模块连读都跳过」(更省一层):跳读会丢掉该模块的依赖边,除非调用方保证
26
+ * `known` 是**完整闭包**——把正确性押在调用方的记性上,一旦传了半张表就静默产出缺模块的产物,
27
+ * 白屏且极难诊断。读一遍是最便宜的保险;真正贵的是网络上传,而那部分已经省掉了。
28
+ *
29
+ * 产物随后由 `POST /api/v1/projects/:id/preview` 落到项目桶,由预览子域服务:真实 HTTP 源 →
30
+ * 跨浏览器可用、SPA 路由(history 模式)原生可用、`/api/*` 走平台网关执行项目函数。
31
+ *
32
+ * 边界:只搬运**文本模块**。图片等二进制资产若出现在 VFS 里,dev server 本就不会把它编译成模块
33
+ * (解析链只认 JS/TS/CSS/JSON/Vue),故不在此处理;`readText` 拿不到内容时响亮抛错,不静默产出残缺产物。
34
+ */
35
+ /** 模块 URL 前缀(与 `server/domains/preview/store.ts` 的 `PREVIEW_MODULE_URL_PREFIX` 对齐)。 */
36
+ export declare const BUNDLE_MODULE_PATH_PREFIX = "/_adep/m/";
37
+ export interface BundleExportOptions {
38
+ /** 入口文档 URL(dev server 产出的 blob 文档)。 */
39
+ documentUrl: string;
40
+ /**
41
+ * 读取 URL 文本(默认 `fetch(url).then(r => r.text())`)。
42
+ * 注入缝:Node 单测用假实现,不依赖真实 blob 存储。
43
+ */
44
+ readText?: (url: string) => Promise<string>;
45
+ /** 模块 URL 前缀(默认 `/_adep/m/`)。 */
46
+ modulePathPrefix?: string;
47
+ /** 内容 → 模块名(不含扩展名)。默认 sha256 前 16 位 hex;注入以便单测确定化。 */
48
+ moduleName?: (code: string) => string | Promise<string>;
49
+ /** 注入到入口文档 `</body>` 前的脚本(IDE 侧用于错误中继等;默认不注入)。 */
50
+ appendScript?: string;
51
+ /**
52
+ * 上一次发布的 blob URL → 模块路径映射(即上次 `ExportedBundle.urls`)。
53
+ * 用途只有一个:**跳过重复上传**(值集 = 服务端已有的路径集)。传空/不传 = 全量发布。
54
+ */
55
+ known?: ReadonlyMap<string, string>;
56
+ }
57
+ export interface ExportedBundle {
58
+ /** 自包含入口文档(blob 引用已改写为模块路径)。 */
59
+ html: string;
60
+ /** **需要上传**的模块(路径 → 源码):已在上一次发布里存在的模块不再出现在这里。 */
61
+ modules: Record<string, string>;
62
+ /** 本次入口文档所需的**全部**模块路径(含本次未重复上传的;服务端据此写清单与清理幽灵)。 */
63
+ paths: string[];
64
+ /** 本次闭包的 blob URL → 路径映射;调用方保存,下次作为 `known` 传入。 */
65
+ urls: Map<string, string>;
66
+ }
67
+ /** 默认模块名:sha256 前 16 位 hex(浏览器均有 `crypto.subtle`)。 */
68
+ export declare function defaultModuleName(code: string): Promise<string>;
69
+ /** 找出文本里的全部 blob 引用(去重、保序)。 */
70
+ export declare function findBlobRefs(text: string): string[];
71
+ /**
72
+ * 导出产物:入口文档 + 递归闭包内全部模块(内容寻址命名 + 引用改写)。
73
+ * 读不到某个 blob(已被撤销 / 网络异常)→ 抛错(残缺产物比失败更糟:页面白屏且难以诊断)。
74
+ */
75
+ export declare function exportBundle(options: BundleExportOptions): Promise<ExportedBundle>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * IDE 预览「控制台中继」脚本:内联**经典**脚本,注入预览文档 `<head>` 首位,先于任何 module
3
+ * 脚本执行(module script 天然 defer,故必然先就位)。
4
+ *
5
+ * 职责:劫持 `console.*`(log / info / warn / error / debug)与 window 的 `error` /
6
+ * `unhandledrejection`,以 `{ source: 'adep-ide-preview', kind: 'console', level, text }` 信封
7
+ * `postMessage` 给父窗口,由 IDE 的 `FrontendPreview.onMessage` 原样消费(注入「控制台」页签)。
8
+ *
9
+ * **同步纪律**:本字符串与 `packages/vite-plugin/src/ide-proxy.ts` 的 `CONSOLE_RELAY_SCRIPT`
10
+ * (单一真相源,Nodebox 路径经 `@adep/vite-plugin` 消费)必须**逐字一致**——两张副本、无共享
11
+ * import(预览 iframe 与浏览器内 vite-dev 是两个独立加载上下文,共享源码会引入打包耦合),
12
+ * 与 `FN_FETCH_INTERCEPTOR_SCRIPT` 同款处理。改这里务必同步另一侧;
13
+ * 机检见 `app/composables/__tests__/preview-inject-scripts.test.ts`(两份副本逐字比对 + 经典脚本语法门)。
14
+ *
15
+ * 边界:
16
+ * - `window.parent === window`(预览被直接打开 / 分享链接场景)直接不发——没有父窗口可回传;
17
+ * - 序列化**不走裸 `JSON.stringify`**:`Error` 取 `stack`(含嵌在对象里的 `cause`——vue-router 的
18
+ * `VUE_ROUTER_R0120({ cause })` 正是这形态,裸 stringify 会把它压成 `{"cause":{}}` 而丢掉根因)、
19
+ * 函数 / `bigint` / 循环引用各有可读形态;整体失败才退化为 `String(value)`,绝不因「记日志本身」
20
+ * 把页面搞崩;postMessage 失败同样静默(父窗口可能已关闭或拒绝该来源);
21
+ * - 必须是**纯经典脚本语法**:本包把它内联进 `<script>`,出现 `import` / `export` 会让浏览器
22
+ * 报 `SyntaxError`(见 AGENTS.md §9 的 `Unexpected token 'export'` 一例)。
23
+ */
24
+ export declare const CONSOLE_RELAY_SCRIPT: string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * CSS `@import` 内联(浏览器内 vite 的编译期 CSS 管线)。
3
+ *
4
+ * 为什么必须有这一层:真 Vite 的 CSS 管线(postcss-import)在**编译期**按文件解析 `@import`
5
+ * 并把内容合进来;浏览器内 vite 原先只把 CSS 文本逐字注入 `<style>`,`@import` 于是留给浏览器
6
+ * 按**文档 base** 去解析——预览面上那个 URL 是「未命中 → SPA 回退入口文档」,浏览器拿到
7
+ * `text/html` 只能整条丢弃;而 SFC 的 `<style>` 块里常常**只有**这条 `@import`(全局样式全在它指向
8
+ * 的文件里),于是整份样式表失效,且**纯 CSS 层的失败不产生任何 JS 异常**(页面照常渲染、
9
+ * 控制台干净)。IDE-038 之前的现场即如此。
10
+ *
11
+ * 语义对齐 postcss-import 的核心部分(本包零第三方依赖,故自实现):
12
+ * - 支持 `@import "x"` / `@import 'x'` / `@import url(x)` / `@import url("x")` 四种形态;
13
+ * - **注释与字符串感知**:注释块内的 `@import` 与字符串内的 `@import`(如 `content: "@import"`)一律不动;
14
+ * - **条件保留**:`layer` / `layer(name)` / `supports(…)` / 媒体查询按等价 at-rule 包裹;
15
+ * - **重复与环**:同一次展开内同一文件只内联一次(postcss-import 同口径),环据此自然终止;
16
+ * - 被内联文件的 `@charset` / BOM 剥掉(内联后不在首行,留着是非法 CSS)。
17
+ *
18
+ * 解析失败**分级**处置(避免把静默失败换成误红屏):
19
+ * - **外部说明符**(`http(s)://` / `//` / `data:` / `blob:`)⇒ 原样保留,浏览器自己会取;
20
+ * - **本地路径**(`./` `../` `/`)解析不到 ⇒ 项目里确实没有这个文件,抛 `CssImportError`
21
+ * (调用方转成红屏,消息含说明符与导入方路径)——静默丢整份样式正是本模块要根除的问题;
22
+ * - **裸说明符**(`@import 'tailwindcss'`)解析不到 ⇒ 原样保留:第三方包的 CSS 入口布局超出
23
+ * 本包 resolver 的能力,不得据此判死整个预览。
24
+ *
25
+ * 边界:不做 CSS 内的相对 `url()` 资源改写(需资产管线),不支持预处理器(非 `.css` 目标一律
26
+ * 原样保留:`compileStyle` 本就不预处理,本模块不替它做半套)。
27
+ */
28
+ /** 导入解析契约(VFS 侧适配由调用方注入:`vite-dev` 与 `preview-server` 语义不同,各给各的)。 */
29
+ export interface CssImportResolver {
30
+ /** 读文件内容;不存在 / 是目录返回 null。 */
31
+ read(abs: string): string | null;
32
+ /** 说明符 → VFS 绝对路径;解析不到返回 null(**不抛**——分级处置归本模块)。 */
33
+ resolve(spec: string, importerAbs: string): string | null;
34
+ }
35
+ /** 本地路径的 `@import` 解析不到时抛出(裸说明符不抛,见文件头)。 */
36
+ export declare class CssImportError extends Error {
37
+ constructor(message: string);
38
+ }
39
+ /**
40
+ * 把 `css` 里可解析的 `@import` 全部内联(编译期),其余原样保留。
41
+ *
42
+ * `importerAbs` 是 `css` 所属文件的 VFS 绝对路径——相对说明符按它所在目录解析(对齐
43
+ * postcss-import 以文件为基准的语义;SFC 的 `<style>` 块传该 `.vue` 文件路径)。
44
+ */
45
+ export declare function inlineCssImports(css: string, importerAbs: string, resolver: CssImportResolver): string;
@@ -6,6 +6,8 @@
6
6
  * fetch 拦截器,把 `/api/*` 请求经 `window.parent.postMessage` 转发到 IDE 主线程;主线程
7
7
  * 用 `OfflineRunner` 执行**当前项目的云函数草稿**(改函数即生效,因为每次都实时取草稿),
8
8
  * 再把执行结果 postMessage 回 iframe。与 origin 无关:blob URL 与异源 Nodebox 隧道都能用。
9
+ * 新窗口打开预览(window.open)时 window.parent === window,回退到 window.opener.postMessage,
10
+ * 主线程经 event.source 单播回响应——新窗口与 iframe 共用同一套协议。
9
11
  *
10
12
  * 消息协议(两侧逐字对齐;改任一侧必须同步另一侧——见 web-project-template.ts 的
11
13
  * web/vite.config.ts 模板里那份逐字复制的 FN_PROXY_SCRIPT):
@@ -37,6 +39,12 @@ export interface OfflineRunner {
37
39
  * `new URL('/api/x', 'blob:…')` 抛 TypeError → 拦截器退化成原生 fetch 报
38
40
  * "Failed to parse URL"。首次解析失败时用 `window.location.origin`(blob 继承创建者
39
41
  * origin)兜底再解析一次。
42
+ *
43
+ * 新窗口预览(2026-09-12):`window.open` 打开的新标签页 `window.parent === window`,
44
+ * 回退到 `window.opener.postMessage` 转发 /api/* 请求(不能用 noopener,否则 opener 为 null)。
45
+ * blob URL SPA 路由:patch `history.pushState/replaceState`,对相对路径直接吞掉(blob 不可
46
+ * 分层,原生 pushState 解析相对路径抛 TypeError),`location.pathname` 覆盖为当前路由路径,
47
+ * sessionStorage 持久化,刷新后恢复。
40
48
  */
41
49
  export declare const FN_FETCH_INTERCEPTOR_SCRIPT: string;
42
50
  /** IDE 主线程侧的消息处理器类型:直接挂到 `window.addEventListener('message', handler)`。 */
package/dist/index.d.ts CHANGED
@@ -31,6 +31,8 @@ export type { ModuleCache, RequireOptions } from './module-loader';
31
31
  export { createViteServer, scanEsmSpecifiers, ViteDevError } from './vite-dev';
32
32
  export type { SfcCompiler, ViteServer, ViteServerOptions, ViteCommandPort, ViteCommandOutcome, EsmSpecifierHit, } from './vite-dev';
33
33
  export * as posix from './path';
34
+ export { exportBundle, findBlobRefs, defaultModuleName, BUNDLE_MODULE_PATH_PREFIX, } from './bundle-export';
35
+ export type { BundleExportOptions, ExportedBundle } from './bundle-export';
34
36
  export { snapshotVfs, restoreVfs, entriesToTree, createMemoryFsBackend, createIndexedDbFsBackend, pickDefaultFsBackend, createFsPersistence, createMemoryKvStorage, createCommandHistory, } from './persistence';
35
37
  export type { FsPersistEntry, FsPersistenceBackend, FsPersistence, FsPersistenceOptions, CommandHistory, KvStorage, } from './persistence';
36
38
  export { SHELL_COMMANDS, EXTRA_COMMANDS, COMPLETABLE_COMMANDS, parseCompletionContext, completeWithListing, completeShellInput, completeContainerInput, applyCompletion, candidateLabel, } from './completion';
@@ -44,4 +46,5 @@ export * from './offline';
44
46
  export * from './sim';
45
47
  export { FN_FETCH_INTERCEPTOR_SCRIPT, createFunctionFetchHandler } from './function-fetch-proxy';
46
48
  export type { FunctionFetchHandler, FunctionFetchHandlerOptions, OfflineRunner, } from './function-fetch-proxy';
49
+ export { CONSOLE_RELAY_SCRIPT } from './console-relay';
47
50
  export type { WebContainer, WebContainerDirectoryTree, WebContainerEventMap, WebContainerFsEntry, WebContainerProcess, } from '@adep/types';