mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(client): address package asset review findings
This commit is contained in:
1 parent
c1d9f141b9
commit
299741d819
19 files changed
+122
-58
No files matched your search
+4
-4
@@ -15,14 +15,14 @@
|
||||
en: 69ca2e4ba27b2841
|
||||
zh: cb7d7c1bec1adf7b
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/build:
|
||||
en: 1751930093d55cea
|
||||
zh: 8e92e2de062edb96
|
||||
en: da95b904bd60815f
|
||||
zh: 3a598bacc159abc4
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/host-route:
|
||||
en: 151b7300f5b27a9f
|
||||
zh: fae9adff942c2ce8
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/webworker-preview:
|
||||
en: 54acdc86819cee80
|
||||
zh: 28abe4517cf14182
|
||||
en: 019003f4e09ffc60
|
||||
zh: ecabc5118b107079
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/changes-and-package-manifests:
|
||||
en: eedff52d04763c50
|
||||
zh: 203bfcf81242b8fe
|
||||
|
||||
@@ -30,7 +30,7 @@ The supported extensions are `.svg`, `.png`, `.webp`, `.jpg`, and `.jpeg`. [`src
|
||||
|
||||
### Build
|
||||
|
||||
The `dsh-package-asset` plugin of the dynamic browser face resolves an import against its source location and fails the build when the file's real path is outside the package's `src/client/assets/` (which also rejects another package's or a dependency's image), when `exports["./assets/*"]` is not exactly `./lib/assets/*`, when the extension is unsupported, or when a path segment contains `%`, `#`, `?`, `\`, or NUL, which the Host route cannot serve. It emits each file once per build and registers it as a watch input. The three CSS pipelines (CSS Modules, global, and `?inline`) rewrite relative `url()` values through a lightningcss visitor in stylesheets inside the package's own `src/`; `data:`, fragment, scheme, `/`-prefixed, and empty URLs stay unchanged, and dependency stylesheets are not rewritten. The Node Client face of a dual-face package rejects asset imports. The browser face is the only writer of `lib/assets/`: a one-shot build deletes files it did not emit, and a watch rebuild keeps them, because the Host still serves the previous bundle generation and pages running it request those names.
|
||||
The `dsh-package-asset` plugin of the dynamic browser face resolves an import against its source location and fails the build when the file's real path is outside the package's `src/client/assets/` (which also rejects another package's or a dependency's image), when `exports["./assets/*"]` is not exactly `./lib/assets/*`, when the extension is unsupported, when a path segment is `node_modules` or contains `%`, `#`, `?`, `\`, or NUL, which the Host route cannot serve, or when the extension is not lowercase, as the import types are. It emits each file once per build and registers it as a watch input. The three CSS pipelines (CSS Modules, global, and `?inline`) rewrite relative `url()` values through a lightningcss visitor in stylesheets inside the package's own `src/`; `data:`, fragment, scheme, `/`-prefixed, and empty URLs stay unchanged, a relative file reference with a fragment or query fails the build, and dependency stylesheets are not rewritten. The rewrite writes an identifier placeholder that lightningcss never escapes, so a file name with parentheses or quotes still becomes its resolver call. The Node Client face of a dual-face package rejects asset imports. The browser face is the only writer of `lib/assets/`: a one-shot build deletes files it did not emit, and a watch rebuild keeps them, because the Host still serves the previous bundle generation and pages running it request those names.
|
||||
|
||||
### Host route
|
||||
|
||||
@@ -40,7 +40,7 @@ A served asset streams from disk on every request; the Host keeps neither its by
|
||||
|
||||
### WebWorker preview
|
||||
|
||||
The preview page runs its Host in a Worker, and a page request for a reference never reaches it. Before any Client bundle runs, the page asks the Worker for `GET /__assets__`, answered by tunnel glue like `GET /__boot__`: an uncompressed tar of every file under `lib/assets/` of each image package that declares `"./assets/*": "./lib/assets/*"`, named by its reference. The page turns each entry into a Blob URL with the media type of its extension, and its `resolveAsset` returns that URL, or the reference when the image holds no such file. The base image already carries these files, so the archive crosses only the thread boundary.
|
||||
The preview page runs its Host in a Worker, and a page request for a reference never reaches it. Before any Client bundle runs, the page asks the Worker for `GET /__assets__`, answered by tunnel glue like `GET /__boot__`: an uncompressed tar of every file under `lib/assets/` of each image package that declares `"./assets/*": "./lib/assets/*"`, named by its reference. The page turns each entry into a Blob URL with the media type of its extension, and its `resolveAsset` returns that URL, or the reference when the image holds no such file. The base image already carries these files, so no network request is added; each boot still walks the image's packages, archives every asset, and copies each entry into a Blob, enabled or not.
|
||||
|
||||
### Changes and package manifests
|
||||
|
||||
|
||||
+2
-2
@@ -30,7 +30,7 @@ plugins/@deepseek-ai/dsh-client-ui-chat/assets/running-whale%402x-06738fa29b2f.p
|
||||
|
||||
### 构建
|
||||
|
||||
动态浏览器 face 的 `dsh-package-asset` 插件按源码位置解析 import。以下情况构建失败:文件真实路径不在本包 `src/client/assets/` 内(他包或依赖里的图片也因此被拒绝);`exports["./assets/*"]` 不是恰好 `./lib/assets/*`;扩展名不受支持;路径段含 Host 路由无法提供的 `%`、`#`、`?`、`\` 或 NUL。每个文件每次构建只产出一次,并登记为监听输入。三条 CSS 管线(CSS Modules、全局样式、`?inline`)对本包 `src/` 内的样式表,经 lightningcss visitor 改写相对 `url()`;`data:`、片段、带协议、以 `/` 开头和空的 URL 保持不变,依赖包的样式表不改写。双 face 包的 Node Client face 拒绝资源 import。浏览器 face 是 `lib/assets/` 的唯一写入方:一次性构建会删除本次没有产出的文件,watch 重建则保留它们,因为 Host 仍在提供上一代 bundle,运行它的页面会请求那些文件名。
|
||||
动态浏览器 face 的 `dsh-package-asset` 插件按源码位置解析 import。以下情况构建失败:文件真实路径不在本包 `src/client/assets/` 内(他包或依赖里的图片也因此被拒绝);`exports["./assets/*"]` 不是恰好 `./lib/assets/*`;扩展名不受支持;路径段为 `node_modules` 或含 Host 路由无法提供的 `%`、`#`、`?`、`\` 或 NUL;扩展名不是小写(导入类型只声明小写扩展名)。每个文件每次构建只产出一次,并登记为监听输入。三条 CSS 管线(CSS Modules、全局样式、`?inline`)对本包 `src/` 内的样式表,经 lightningcss visitor 改写相对 `url()`;`data:`、片段、带协议、以 `/` 开头和空的 URL 保持不变,带片段或查询串的相对文件引用会让构建失败,依赖包的样式表不改写。改写时先写入 lightningcss 从不转义的标识符占位,因此含括号或引号的文件名仍会变成对应的解析调用。双 face 包的 Node Client face 拒绝资源 import。浏览器 face 是 `lib/assets/` 的唯一写入方:一次性构建会删除本次没有产出的文件,watch 重建则保留它们,因为 Host 仍在提供上一代 bundle,运行它的页面会请求那些文件名。
|
||||
|
||||
### Host 路由
|
||||
|
||||
@@ -40,7 +40,7 @@ plugins/@deepseek-ai/dsh-client-ui-chat/assets/running-whale%402x-06738fa29b2f.p
|
||||
|
||||
### WebWorker 预览
|
||||
|
||||
预览页的 Host 运行在 Worker 中,页面对引用发出的请求到达不了它。在任何 Client bundle 运行之前,页面向 Worker 请求 `GET /__assets__`,它和 `GET /__boot__` 一样由隧道胶水层回答:镜像中每个声明了 `"./assets/*": "./lib/assets/*"` 的包,其 `lib/assets/` 下的所有文件打成一个未压缩的 tar,条目以引用命名。页面把每个条目按扩展名的媒体类型转成 Blob URL,它的 `resolveAsset` 返回这个 URL;镜像里没有对应文件时返回引用本身。基础镜像本来就带着这些文件,所以这个归档只跨越线程边界。
|
||||
预览页的 Host 运行在 Worker 中,页面对引用发出的请求到达不了它。在任何 Client bundle 运行之前,页面向 Worker 请求 `GET /__assets__`,它和 `GET /__boot__` 一样由隧道胶水层回答:镜像中每个声明了 `"./assets/*": "./lib/assets/*"` 的包,其 `lib/assets/` 下的所有文件打成一个未压缩的 tar,条目以引用命名。页面把每个条目按扩展名的媒体类型转成 Blob URL,它的 `resolveAsset` 返回这个 URL;镜像里没有对应文件时返回引用本身。基础镜像本来就带着这些文件,所以不增加网络请求;但每次启动仍会遍历镜像中的包、打包全部资源,并把每个条目复制成 Blob,不论包是否启用。
|
||||
|
||||
### 变化与包 manifest
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
en: 811c0e8a3a370399
|
||||
zh: 4a49a3999d35503a
|
||||
/client-modules/the-bundle-route-and-index-injection:
|
||||
en: d653be500d3a0d67
|
||||
zh: fffb1901ce8ba7d9
|
||||
en: 82c089fbd9ffd535
|
||||
zh: 30ac52d01b4fc074
|
||||
/client-modules/the-service:
|
||||
en: 3208a452c2d86581
|
||||
zh: f6373dd660c687c1
|
||||
en: b1df1bf2b8ee3ad1
|
||||
zh: 6373d1b8cae22d02
|
||||
@@ -99,7 +99,7 @@ Each reconciliation reads the package manifest and the bundle metadata again. A
|
||||
|
||||
## The bundle route and index injection
|
||||
|
||||
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` addresses one generated combo script; a one-resource request uses the same form and is the HMR path. The script is concatenated once on its first `GET` and ends with a `sourceMappingURL` that carries the combo query alone — `??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`, resolved against the script's own directory rather than the document. The map files are not read by startup, index rendering, script `GET`, or `HEAD`; the first map `GET` reads and validates them, composes one Indexed Source Map v3, and caches that body. An authored component map supplies its section; a component without one receives an identity section whose `sourcesContent` is the captured bundle and whose source name is its packaged `sourceURL` or plugin route. Every startup request URL is at most 3 KiB measured as UTF-8 bytes; partitioning uses the longer map form. All application URLs are preloaded, and all bootstrap URLs execute before the graph global and Vite entry. Materialized responses use long-lived immutable caching. Unknown or altered resource lists, missing revisions, and stale revisions answer 404 rather than serving different bytes or letting the SPA fallback return HTML as JavaScript; other methods are 405. The injection rows carry the current graph on every index render, so a reload always boots against the live composition.
|
||||
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` addresses one generated combo script; a one-resource request uses the same form and is the HMR path. `/plugins/<package>/assets/<path>` serves a package's published images; the [package README](../../packages/client/modules/README.md#package-assets) owns that route. The script is concatenated once on its first `GET` and ends with a `sourceMappingURL` that carries the combo query alone — `??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`, resolved against the script's own directory rather than the document. The map files are not read by startup, index rendering, script `GET`, or `HEAD`; the first map `GET` reads and validates them, composes one Indexed Source Map v3, and caches that body. An authored component map supplies its section; a component without one receives an identity section whose `sourcesContent` is the captured bundle and whose source name is its packaged `sourceURL` or plugin route. Every startup request URL is at most 3 KiB measured as UTF-8 bytes; partitioning uses the longer map form. All application URLs are preloaded, and all bootstrap URLs execute before the graph global and Vite entry. Materialized responses use long-lived immutable caching. Unknown or altered resource lists, missing revisions, and stale revisions answer 404 rather than serving different bytes or letting the SPA fallback return HTML as JavaScript; other methods are 405. The injection rows carry the current graph on every index render, so a reload always boots against the live composition.
|
||||
|
||||
## The service
|
||||
|
||||
@@ -117,7 +117,7 @@ interface ClientArtifactBaseline {
|
||||
}
|
||||
```
|
||||
|
||||
`ClientModuleRegistry` (`ctx.clientModules`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodules--clientmoduleregistry). `graph()` returns the current composed graph (a stable object between changes), `clientPath(id)` returns the bundle's absolute path, and `artifactBaseline(id)` returns the bundle stat values captured before the current snapshot was read. `fetchBundle()` resolves the same lazy response used by the HTTP route. `rebuilt(id)` publishes a bundle rebuilt without an entry restart: it derives the revision from filesystem metadata, and only a revision change reads the new bytes, recomposes the graph, and notifies; an entry restart publishes through reconciliation instead. `onRebuilt` fires per changed bundle with the new revision; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt revision change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
|
||||
`ClientModuleRegistry` (`ctx.clientModules`, defined in [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts)) exposes reads and the rebuild face; signatures are in the generated [service catalog](#ctxclientmodules--clientmoduleregistry). `graph()` returns the current composed graph (a stable object between changes), `clientPath(id)` returns the bundle's absolute path, and `artifactBaseline(id)` returns the bundle stat values captured before the current snapshot was read. `fetchBundle()` returns the same responses as the HTTP route: the lazy bundle responses, and package assets read on each request. `rebuilt(id)` publishes a bundle rebuilt without an entry restart: it derives the revision from filesystem metadata, and only a revision change reads the new bytes, recomposes the graph, and notifies; an entry restart publishes through reconciliation instead. `onRebuilt` fires per changed bundle with the new revision; `onGraphChanged` fires after any flush that recomposed the graph (row added or removed, or a rebuilt revision change) and is pull-model — listeners re-read `graph()`. Both notification paths contain listener exceptions so one throwing subscriber cannot skip later subscribers or kill whatever triggered the flush.
|
||||
|
||||
[`dsh-client-hmr`](../../packages/client/hmr/README.md) delivers live graph snapshots in the shipped Web composition. The Host forwards existing graph-change notifications immediately, and reconnect sends the current full graph. A graph describes desired browser entries without asserting that Host cleanup has completed. Its artifact poll separately reports rebuilt revisions. Source-map changes alone do not trigger a reload; a new combo-map URL appears only after a bundle revision changes, and each map body is fixed by its first `GET`. Client Modules validates snapshots and serializes reconciliation with those rebuilds; it owns the boot-created entry map, single-resource arrivals, asynchronous removal, unused-module/style cleanup and page-local retry status. Static platform modules and the bootstrap retain their page lifetime; Electron installation is a separate flow.
|
||||
|
||||
@@ -155,7 +155,8 @@ clientPath(id: string): string | undefined
|
||||
* 405, and `HEAD` returns the same immutable headers without materializing a
|
||||
* body. Each bundle body is built once on its first `GET`; script
|
||||
* construction never reads maps. A package asset is read on every request
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag.
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag;
|
||||
* its body holds the file open until the caller consumes or cancels it.
|
||||
* @param request - shell-carrier request for a `/plugins` resource.
|
||||
* @returns the exact response also exposed by the optional Web route.
|
||||
*/
|
||||
@@ -181,7 +182,8 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
|
||||
rebuilt(id: string): string | undefined
|
||||
|
||||
/**
|
||||
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
|
||||
* Subscribe to bundle rebuilds; fires when {@link rebuilt} changes a rev. A rev an
|
||||
* entry restart publishes reaches only {@link onGraphChanged} listeners.
|
||||
* @param listener - receives the entry id and its new bundle rev.
|
||||
* @returns the unsubscriber.
|
||||
*/
|
||||
|
||||
@@ -99,7 +99,7 @@ interface WebBootGraph {
|
||||
|
||||
## bundle 路由与 index 注入
|
||||
|
||||
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 寻址一份生成的 combo 脚本;单资源请求采用同一形式,也是 HMR 路径。脚本在首次 `GET` 时只拼接一次,并以 `sourceMappingURL` 结尾,该引用只携带 combo 查询串——`??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`,按脚本自身目录而非文档解析。启动、index 渲染、脚本 `GET` 和 `HEAD` 都不会读取 map 文件;首次 map `GET` 才会读取并校验这些文件、组合一份 Indexed Source Map v3,并缓存该 body。组件有自带 map 时直接用于对应 section;没有时则获得 identity section,其 `sourcesContent` 是捕获的 bundle,source 名取打包后的 `sourceURL` 或插件路由。每条启动请求 URL 按 UTF-8 字节计算都不超过 3 KiB;切分按更长的 map 形式计算。所有 application URL 都会预加载,所有 bootstrap URL 都会在图全局量与 Vite entry 之前执行。已物化响应使用长期 immutable 缓存。未知或被修改的资源列表、缺少 revision 及陈旧 revision 都返回 404,绝不提供其他字节,也不会让 SPA fallback 把 HTML 当作 JavaScript 返回;其他方法返回 405。注入行在每次 index 渲染时携带当前图,因此重新加载总是基于实时组合启动。
|
||||
`GET`/`HEAD /plugins/??<package-a>/client.js,<package-b>/client.js&rev=<rev>` 寻址一份生成的 combo 脚本;单资源请求采用同一形式,也是 HMR 路径。`/plugins/<package>/assets/<path>` 提供包发布的图片,该路由由[包 README](../../packages/client/modules/README.zh.md#package-assets) 说明。脚本在首次 `GET` 时只拼接一次,并以 `sourceMappingURL` 结尾,该引用只携带 combo 查询串——`??<package-a>/client.js.map,<package-b>/client.js.map&rev=<rev>`,按脚本自身目录而非文档解析。启动、index 渲染、脚本 `GET` 和 `HEAD` 都不会读取 map 文件;首次 map `GET` 才会读取并校验这些文件、组合一份 Indexed Source Map v3,并缓存该 body。组件有自带 map 时直接用于对应 section;没有时则获得 identity section,其 `sourcesContent` 是捕获的 bundle,source 名取打包后的 `sourceURL` 或插件路由。每条启动请求 URL 按 UTF-8 字节计算都不超过 3 KiB;切分按更长的 map 形式计算。所有 application URL 都会预加载,所有 bootstrap URL 都会在图全局量与 Vite entry 之前执行。已物化响应使用长期 immutable 缓存。未知或被修改的资源列表、缺少 revision 及陈旧 revision 都返回 404,绝不提供其他字节,也不会让 SPA fallback 把 HTML 当作 JavaScript 返回;其他方法返回 405。注入行在每次 index 渲染时携带当前图,因此重新加载总是基于实时组合启动。
|
||||
|
||||
## 服务
|
||||
|
||||
@@ -117,7 +117,7 @@ interface ClientArtifactBaseline {
|
||||
}
|
||||
```
|
||||
|
||||
`ClientModuleRegistry`(`ctx.clientModules`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodules--clientmoduleregistry)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回 bundle 的绝对路径,`artifactBaseline(id)` 返回读取当前快照前捕获的 bundle stat 值。`fetchBundle()` 解析 HTTP 路由所使用的同一份惰性响应。`rebuilt(id)` 发布没有伴随条目重启的 bundle 重建:它从文件系统元数据派生 revision,只有 revision 变化才会读取新字节、重新组合图并发出通知;条目重启则经对账发布。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 revision;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 revision 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
|
||||
`ClientModuleRegistry`(`ctx.clientModules`,定义于 [`packages/client/modules/src/index.ts`](../../packages/client/modules/src/index.ts))暴露读取面与重建面;签名见生成的[服务目录](#ctxclientmodules--clientmoduleregistry)。`graph()` 返回当前组合出的图(两次变更之间是同一个稳定对象),`clientPath(id)` 返回 bundle 的绝对路径,`artifactBaseline(id)` 返回读取当前快照前捕获的 bundle stat 值。`fetchBundle()` 返回与 HTTP 路由相同的响应:惰性的 bundle 响应,以及每次请求都读取的包资源。`rebuilt(id)` 发布没有伴随条目重启的 bundle 重建:它从文件系统元数据派生 revision,只有 revision 变化才会读取新字节、重新组合图并发出通知;条目重启则经对账发布。`onRebuilt` 按发生变化的 bundle 逐个触发并携带新 revision;`onGraphChanged` 在任何一次重新组合了图的 flush 之后触发(行的增删,或 rebuilt 带来的 revision 变化),并采用拉取模型——监听器自行重读 `graph()`。两条通知路径都会兜住监听器异常,因此一个抛错的订阅者既不能让后续订阅者被跳过,也不能杀死触发这次 flush 的一方。
|
||||
|
||||
随包提供的 Web 组合通过 [`dsh-client-hmr`](../../packages/client/hmr/README.zh.md) 交付动态图快照。Host 立即转发现有图变化通知,重连会发送当前完整图。图描述浏览器的目标条目,不声明 Host 清理已经完成。产物轮询另外报告重建 revision。仅 source map 变化不会触发重载;新 combo-map URL 只会在 bundle revision 变化后出现,每份 map body 由其首次 `GET` 固定。Client Modules 校验快照,并将对账与重建串行协调;它持有启动创建的条目映射,负责单资源到达、异步移除、未使用模块与样式清理,以及页面本地重试状态。静态平台模块与 bootstrap 保持页面生命周期;Electron 安装属于独立流程。
|
||||
|
||||
@@ -155,7 +155,8 @@ clientPath(id: string): string | undefined
|
||||
* 405, and `HEAD` returns the same immutable headers without materializing a
|
||||
* body. Each bundle body is built once on its first `GET`; script
|
||||
* construction never reads maps. A package asset is read on every request
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag.
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag;
|
||||
* its body holds the file open until the caller consumes or cancels it.
|
||||
* @param request - shell-carrier request for a `/plugins` resource.
|
||||
* @returns the exact response also exposed by the optional Web route.
|
||||
*/
|
||||
@@ -181,7 +182,8 @@ artifactBaseline(id: string): ClientArtifactBaseline | undefined
|
||||
rebuilt(id: string): string | undefined
|
||||
|
||||
/**
|
||||
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
|
||||
* Subscribe to bundle rebuilds; fires when {@link rebuilt} changes a rev. A rev an
|
||||
* entry restart publishes reaches only {@link onGraphChanged} listeners.
|
||||
* @param listener - receives the entry id and its new bundle rev.
|
||||
* @returns the unsubscriber.
|
||||
*/
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
en: dc8c41fbf9869179
|
||||
zh: e45976a585cb9c86
|
||||
/web-ui-style-reference/component-rules:
|
||||
en: e82cec5fb79a9efd
|
||||
zh: e4ea1a58d80a1548
|
||||
en: 6715806a6f84d141
|
||||
zh: 94b7a6ede4919512
|
||||
/web-ui-style-reference/corner-radii-and-settings-cards:
|
||||
en: 118972f6c2bd6ce4
|
||||
zh: 85f8416b03e4dd8b
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ Global style sheets belong in `ui-theme/src/styles/`. Component styles live besi
|
||||
- Pair font sizes with line heights and use the theme typography variables when an existing role matches.
|
||||
- Keep source text, terminal output, and diff lines unwrapped when their component contract requires column preservation; use the shared scrollbar styles rather than component-specific scrollbar selectors.
|
||||
- Put presentation in CSS. Inline React styles may pass component-local custom-property values but must not encode theme branches.
|
||||
- A relative `url()` in a plugin stylesheet points to a file in the package's `src/client/assets/`, and the build rewrites it to the [package asset](../packages/client/AGENTS.md#dependency-declaration) URL. `data:`, `#fragment`, `/`-rooted, scheme, and empty `url('')` references stay unchanged; stylesheets from dependencies are not rewritten.
|
||||
- A relative `url()` in a plugin stylesheet points to a file in the package's `src/client/assets/`, and the build rewrites it to the [package asset](../packages/client/AGENTS.md#dependency-declaration) URL. `data:`, `#fragment`, `/`-rooted, scheme, and empty `url('')` references stay unchanged, a relative file reference with a fragment or query fails the build, and stylesheets from dependencies are not rewritten.
|
||||
- Preserve keyboard focus visibility and reduced-motion behavior when adding transitions or hover-only controls.
|
||||
- Rounded corners inherit the global superellipse smoothing from ui-theme's `corner-shape.css` on supporting engines. Pair `corner-shape: round` with every full-round `border-radius` (`50%`, `100%`, or a pill radius) so circles and capsules keep circular arcs; the ui-theme corner-shape spec enforces the pairing.
|
||||
- Elevated surfaces (menus, popovers, modals, panels, floating buttons, the composer) set `border: 0` and take `box-shadow: var(--dsw-elevation-panel)`, `var(--dsw-elevation-prominent)`, or the composer's `var(--dsw-elevation-soft)` (larger blur at lower alpha): the 0.5px hairline stroke is the first shadow layer, and `--dsw-elevation-stroke-color` rebinds or suppresses it per surface or state. Never pair a `--dsw-alias-border-*` border with an lv/elevation shadow — the ui-theme elevation spec rejects the pairing; state-colored borders (warn panels) stay real borders.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
- 字体大小必须与行高配对;已有角色匹配时使用主题排版变量。
|
||||
- 当组件约定要求保留列结构时,源码文本、终端输出和 diff 行不得换行;使用共享滚动条样式,不得定义组件专用滚动条选择器。
|
||||
- 呈现规则写在 CSS 中。React 内联样式可以传递组件局部自定义属性值,但不得编码主题分支。
|
||||
- 插件样式表中的相对 `url()` 指向本包 `src/client/assets/` 中的文件,构建会将其改写为[包资源](../packages/client/AGENTS.md#dependency-declaration) URL。`data:`、`#fragment`、以 `/` 开头、带 scheme 的引用以及空 `url('')` 引用保持不变;依赖包中的样式表不会被改写。
|
||||
- 插件样式表中的相对 `url()` 指向本包 `src/client/assets/` 中的文件,构建会将其改写为[包资源](../packages/client/AGENTS.md#dependency-declaration) URL。`data:`、`#fragment`、以 `/` 开头、带 scheme 的引用以及空 `url('')` 引用保持不变,带片段或查询串的相对文件引用会让构建失败,依赖包中的样式表不会被改写。
|
||||
- 添加过渡动画或仅悬停可见的控件时,保留清晰可见的键盘焦点和减少动态效果行为。
|
||||
- 支持的引擎上,圆角继承 ui-theme `corner-shape.css` 的全局超级椭圆平滑。每个正圆 `border-radius`(`50%`、`100%` 或胶囊半径)必须配对 `corner-shape: round`,使圆形与胶囊保持圆弧;ui-theme 的 corner-shape spec 强制这一配对。
|
||||
- 高层级表面(菜单、浮层、对话框、面板、悬浮按钮、输入框)设 `border: 0` 并使用 `box-shadow: var(--dsw-elevation-panel)`、`var(--dsw-elevation-prominent)` 或输入框专用的 `var(--dsw-elevation-soft)`(更大模糊、更低透明度):0.5px 发丝描边是第一层投影,`--dsw-elevation-stroke-color` 可按表面或状态重绑或抑制描边。不得将 `--dsw-alias-border-*` border 与 lv/elevation 投影配对——ui-theme 的 elevation spec 会拒绝;状态色 border(warn 面板)保持真 border。
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
en: 25f536f156ec0bcc
|
||||
zh: a522355b3e0edeae
|
||||
/deepseek-ai-dsh-client-modules/use-this-package/declaring-a-client-plugin:
|
||||
en: d4a7f865f4593724
|
||||
zh: 4313eb8fe9cd5c98
|
||||
en: cc0eb2c35a7a0998
|
||||
zh: 0e1df23869410c7a
|
||||
/deepseek-ai-dsh-client-modules/use-this-package/package-assets:
|
||||
en: 010c46ed0b12279a
|
||||
zh: 2a04edbcd2e59508
|
||||
|
||||
@@ -33,6 +33,7 @@ Use it when you compose or build a browser client plugin: the package turns a pa
|
||||
|
||||
A browser plugin package declares `dsh.client` in its `package.json` with `platform: 'web'`, exports a `./client` bundle, and lists any non-baseline module requests under `dsh.client.external`. The host half turns each declaration into a served bundle under `/plugins`, ordered so dynamic providers load before their consumers.
|
||||
|
||||
<a id="package-assets"></a>
|
||||
### Package assets
|
||||
|
||||
The same `/plugins` route serves the images a client package publishes through its `"./assets/*": "./lib/assets/*"` export. The build emits each referenced asset with its content revision, the first 12 hexadecimal digits of the SHA-1 digest of its bytes, inserted before the extension, and compiles the import, or the stylesheet `url()`, into a synchronous `require.asset("plugins/<package>/assets/<emitted path>")` call on the bundle's module loader, so one URL always names one content. The call returns the document-relative reference unless the page's carrier supplies `resolveAsset`: the WebWorker preview, whose page cannot request the reference, maps it to a Blob URL of the bytes its Worker image holds. Any `/plugins/<package>/assets/` path is answered by the asset route alone: for an active client row, the Host percent-decodes the path once and resolves `<package>/assets/<path>` with Node's `require.resolve` from the package's own manifest, so the package's exports decide which files exist. A path that does not resolve, a resolved file whose extension is not `.svg`, `.png`, `.webp`, `.jpg`, or `.jpeg`, a malformed escape, and an inactive package answer 404; the query selects nothing. A served asset streams from disk on every request without caching and carries its media type, the bundle `cache-control: public, max-age=31536000, immutable`, `etag: "<requested path>"`, and `x-content-type-options: nosniff`; an SVG additionally carries `content-security-policy: script-src 'none'`, so an SVG opened directly runs no script. An `If-None-Match` equal to that tag answers 304 without opening the file, and `HEAD` answers without a body. `fetchBundle()` returns the same responses, with a streamed body that closes the file when cancelled.
|
||||
|
||||
@@ -33,6 +33,7 @@ kind: "package-reference"
|
||||
|
||||
浏览器插件包在其 `package.json` 中以 `platform: 'web'` 声明 `dsh.client`,导出 `./client` bundle,并在 `dsh.client.external` 下列出任何基座之外的模块请求。宿主半侧把每份声明变成 `/plugins` 下提供的 bundle,并让动态提供方先于其消费方加载。
|
||||
|
||||
<a id="package-assets"></a>
|
||||
### 包资源
|
||||
|
||||
同一个 `/plugins` 路由也提供客户端包通过其 `"./assets/*": "./lib/assets/*"` 导出发布的图片。构建会产出每个被引用的资源,在扩展名之前插入其内容 revision(资源字节 SHA-1 摘要的前 12 位十六进制数字),并把 import 或样式表 `url()` 编译成对 bundle 模块加载器的同步调用 `require.asset("plugins/<package>/assets/<emitted path>")`,因此一个 URL 始终对应一份内容。除非页面的载体提供了 `resolveAsset`,这个调用都返回文档相对引用;WebWorker 预览的页面无法请求该引用,它把引用映射为 Worker 镜像中对应字节的 Blob URL。任何 `/plugins/<package>/assets/` 路径都只由资源路由响应:对于活动的客户端 row,Host 对路径做一次百分号解码,并从该包自身的 manifest(元数据清单)出发,用 Node 的 `require.resolve` 解析 `<package>/assets/<path>`,因此由包的导出决定哪些文件存在。无法解析的路径、扩展名不是 `.svg`、`.png`、`.webp`、`.jpg` 或 `.jpeg` 的已解析文件、格式错误的转义以及未激活的包都返回 404;查询串不参与选择。提供的资源每次请求都从磁盘流式读取且不缓存,并携带其媒体类型、与 bundle 相同的 `cache-control: public, max-age=31536000, immutable`、`etag: "<requested path>"` 和 `x-content-type-options: nosniff`;SVG 还额外携带 `content-security-policy: script-src 'none'`,因此直接打开的 SVG 不会运行脚本。`If-None-Match` 等于该 etag 时返回 304 且不打开文件,`HEAD` 的响应不带 body。`fetchBundle()` 返回相同的响应,其流式 body 在取消时关闭文件。
|
||||
|
||||
@@ -722,7 +722,8 @@ export class ClientModuleRegistry extends Service {
|
||||
* 405, and `HEAD` returns the same immutable headers without materializing a
|
||||
* body. Each bundle body is built once on its first `GET`; script
|
||||
* construction never reads maps. A package asset is read on every request
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag.
|
||||
* and answers 304 without a body when `If-None-Match` equals its entity tag;
|
||||
* its body holds the file open until the caller consumes or cancels it.
|
||||
* @param request - shell-carrier request for a `/plugins` resource.
|
||||
* @returns the exact response also exposed by the optional Web route.
|
||||
*/
|
||||
@@ -781,7 +782,8 @@ export class ClientModuleRegistry extends Service {
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.
|
||||
* Subscribe to bundle rebuilds; fires when {@link rebuilt} changes a rev. A rev an
|
||||
* entry restart publishes reaches only {@link onGraphChanged} listeners.
|
||||
* @param listener - receives the entry id and its new bundle rev.
|
||||
* @returns the unsubscriber.
|
||||
*/
|
||||
|
||||
@@ -524,20 +524,29 @@ function packageAssetCall(reference: string): string {
|
||||
return `${PACKAGE_ASSET_CALL}(${JSON.stringify(reference)})`
|
||||
}
|
||||
|
||||
/**
|
||||
* Placeholder the `url()` rewrite writes into stylesheet text instead of a
|
||||
* reference: letters, digits, and underscores, which lightningcss never escapes,
|
||||
* so a file name with parentheses or quotes still maps back to its reference.
|
||||
*/
|
||||
const STYLESHEET_ASSET_PLACEHOLDER = /__dshPackageAsset\d+__/gu
|
||||
|
||||
/**
|
||||
* Compile stylesheet text into a JavaScript expression that passes each package-asset
|
||||
* reference the `url()` rewrite inserted through the resolver call.
|
||||
* @param css - stylesheet text after the rewrite.
|
||||
* @param references - the references the rewrite inserted into this stylesheet.
|
||||
* @param references - reference of each placeholder the rewrite inserted into this stylesheet.
|
||||
* @returns a string literal, or a concatenation of literals and resolver calls.
|
||||
*/
|
||||
function stylesheetSource(css: string, references: ReadonlySet<string>): string {
|
||||
function stylesheetSource(css: string, references: ReadonlyMap<string, string>): string {
|
||||
if (references.size === 0) return JSON.stringify(css)
|
||||
const inserted = new RegExp([...references].map(escapeSpecifier).join('|'), 'gu')
|
||||
const parts: string[] = []
|
||||
let last = 0
|
||||
for (const match of css.matchAll(inserted)) {
|
||||
parts.push(JSON.stringify(css.slice(last, match.index)), packageAssetCall(match[0]))
|
||||
for (const match of css.matchAll(STYLESHEET_ASSET_PLACEHOLDER)) {
|
||||
const reference = references.get(match[0])
|
||||
/* v8 ignore next -- only the rewrite writes placeholders, and it records each one. */
|
||||
if (reference === undefined) throw new Error(`client bundle compiler: unknown stylesheet asset placeholder ${match[0]}`)
|
||||
parts.push(JSON.stringify(css.slice(last, match.index)), packageAssetCall(reference))
|
||||
last = match.index + match[0].length
|
||||
}
|
||||
parts.push(JSON.stringify(css.slice(last)))
|
||||
@@ -548,8 +557,8 @@ function stylesheetSource(css: string, references: ReadonlySet<string>): string
|
||||
interface StylesheetAssets {
|
||||
/** lightningcss options; empty when the stylesheet lies outside the package's own `src`. */
|
||||
readonly options: { readonly visitor?: Visitor<CustomAtRules> }
|
||||
/** References inserted during the transform that uses {@link options}. */
|
||||
readonly references: ReadonlySet<string>
|
||||
/** Reference of each placeholder inserted during the transform that uses {@link options}. */
|
||||
readonly references: ReadonlyMap<string, string>
|
||||
}
|
||||
|
||||
/** Browser-face package assets: the import plugin and the stylesheet `url()` rewrite share one per-build emission record. */
|
||||
@@ -599,8 +608,12 @@ function packageAssets(manifest: PackageManifest, packageDir: string): PackageAs
|
||||
const below = pathBelow(realDirectory(assetsDir), realPath)
|
||||
if (below === undefined) throw new Error(`tsdown: ${name}: ${file} is outside src/client/assets; move the asset there`)
|
||||
const assetPath = below.split(sep).join('/')
|
||||
if (assetPath.split('/').some(segment => UNSERVABLE_ASSET_SEGMENT.test(segment))) {
|
||||
throw new Error(`tsdown: ${name}: ${file} has a path segment containing %, #, ?, \\ or NUL, which the asset route cannot serve; rename it`)
|
||||
if (assetPath.split('/').some(segment => UNSERVABLE_ASSET_SEGMENT.test(segment) || segment.toLowerCase() === 'node_modules')) {
|
||||
throw new Error(`tsdown: ${name}: ${file} has a node_modules path segment or one containing %, #, ?, \\ or NUL, which the asset route cannot serve; rename it`)
|
||||
}
|
||||
const extension = posix.extname(assetPath)
|
||||
if (extension !== extension.toLowerCase()) {
|
||||
throw new Error(`tsdown: ${name}: ${file} has an uppercase extension; asset imports are typed by lowercase extensions, so rename it`)
|
||||
}
|
||||
if (manifest.exports?.[PACKAGE_ASSET_EXPORT] !== PACKAGE_ASSET_EXPORT_TARGET) {
|
||||
throw new Error(
|
||||
@@ -651,7 +664,7 @@ function packageAssets(manifest: PackageManifest, packageDir: string): PackageAs
|
||||
},
|
||||
},
|
||||
urlRewrite(context, stylesheet) {
|
||||
const inserted = new Set<string>()
|
||||
const inserted = new Map<string, string>()
|
||||
if (pathBelow(realDirectory(sourceDir), realpathSync(stylesheet)) === undefined) return { options: {}, references: inserted }
|
||||
const base = dirname(stylesheet)
|
||||
return {
|
||||
@@ -660,9 +673,12 @@ function packageAssets(manifest: PackageManifest, packageDir: string): PackageAs
|
||||
Url(url) {
|
||||
// Empty values, fragments, root-relative paths, and schemes such as `data:` name no package file.
|
||||
if (url.url === '' || url.url.startsWith('#') || url.url.startsWith('/') || URL_SCHEME.test(url.url)) return
|
||||
const resolved = reference(context, resolvePath(base, url.url))
|
||||
inserted.add(resolved)
|
||||
return { ...url, url: resolved }
|
||||
if (/[#?]/u.test(url.url)) {
|
||||
throw new Error(`tsdown: ${name}: ${stylesheet} references ${JSON.stringify(url.url)}; a package asset url() takes no fragment or query`)
|
||||
}
|
||||
const placeholder = `__dshPackageAsset${String(inserted.size)}__`
|
||||
inserted.set(placeholder, reference(context, resolvePath(base, url.url)))
|
||||
return { ...url, url: placeholder }
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -155,8 +155,7 @@ export async function connectWorkerHost(worker: Worker, options?: WorkerHostConn
|
||||
new URL(options?.image ?? IMAGE_FILE_NAME, document.baseURI).href,
|
||||
(options?.overlays ?? []).map(overlay => new URL(overlay, document.baseURI).href),
|
||||
)
|
||||
const payload = await tunnel.bootPayload()
|
||||
const resolveAsset = await tunnel.packageAssets()
|
||||
const [payload, resolveAsset] = await Promise.all([tunnel.bootPayload(), tunnel.packageAssets()])
|
||||
;(globalThis as ClientTransportGlobal).__DSH_TRANSPORT__ = {
|
||||
fetch: (input, init) => tunnel.fetch(input, init),
|
||||
openStream: (endpoint, payload, signal, uplink) => tunnel.open(endpoint, payload, signal, uplink),
|
||||
|
||||
@@ -47,7 +47,8 @@ function packageDirectories(vfs: MemoryVfs, modules: string): string[] {
|
||||
* Archive every file a package of the image publishes through `"./assets/*": "./lib/assets/*"`.
|
||||
* @param vfs - Mounted image filesystem.
|
||||
* @param root - Image root holding `node_modules`.
|
||||
* @returns An uncompressed tar whose entries are named by their document-relative reference.
|
||||
* @returns An uncompressed tar whose entries are named `plugins/<package>/assets/<path>` with the path unencoded,
|
||||
* so every name fits the ustar limits the base image already met.
|
||||
*/
|
||||
export function packPackageAssets(vfs: MemoryVfs, root: string): Uint8Array {
|
||||
const files: Record<string, Uint8Array> = {}
|
||||
@@ -60,9 +61,7 @@ export function packPackageAssets(vfs: MemoryVfs, root: string): Uint8Array {
|
||||
const assets = join(directory, 'lib', 'assets')
|
||||
if (!vfs.existsSync(assets)) continue
|
||||
for (const path of filesBelow(vfs, assets)) {
|
||||
// The same per-segment encoding the Client build writes into each reference.
|
||||
const reference = `plugins/${manifest.name}/assets/${path.split('/').map(encodeURIComponent).join('/')}`
|
||||
files[reference] = vfs.readFileSync(join(assets, path)) as Uint8Array
|
||||
files[`plugins/${manifest.name}/assets/${path}`] = vfs.readFileSync(join(assets, path)) as Uint8Array
|
||||
}
|
||||
}
|
||||
return packTar(files)
|
||||
@@ -77,9 +76,13 @@ export function packPackageAssets(vfs: MemoryVfs, root: string): Uint8Array {
|
||||
export function packageAssetResolver(archive: Uint8Array): (reference: string) => string {
|
||||
const urls = new Map<string, string>()
|
||||
for (const entry of parseTar(archive)) {
|
||||
const extension = entry.name.slice(entry.name.lastIndexOf('.')).toLowerCase()
|
||||
const [, name, path] = /^plugins\/((?:@[^/]+\/)?[^/]+)\/assets\/(.+)$/u.exec(entry.name) ?? []
|
||||
if (name === undefined || path === undefined) continue
|
||||
// The same per-segment encoding the Client build writes into each reference.
|
||||
const reference = `plugins/${name}/assets/${path.split('/').map(encodeURIComponent).join('/')}`
|
||||
const extension = path.slice(path.lastIndexOf('.')).toLowerCase()
|
||||
// A copy detaches the Blob from the archive buffer the entry views.
|
||||
urls.set(entry.name, URL.createObjectURL(new Blob([new Uint8Array(entry.bytes)], { type: ASSET_MEDIA_TYPES.get(extension) ?? '' })))
|
||||
urls.set(reference, URL.createObjectURL(new Blob([new Uint8Array(entry.bytes)], { type: ASSET_MEDIA_TYPES.get(extension) ?? '' })))
|
||||
}
|
||||
return reference => urls.get(reference) ?? reference
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
// @vitest-environment jsdom
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
PACKAGE_ASSET_EXPORT, PACKAGE_ASSET_EXPORT_TARGET, PACKAGE_ASSET_MEDIA_TYPES, packageAssetReference,
|
||||
} from '@deepseek-ai/dsh-client-modules/src/assets.ts'
|
||||
import { packPackageAssets, packageAssetResolver } from '../src/package-assets.ts'
|
||||
import { MemoryVfs } from '../src/storage/memory.ts'
|
||||
import { parseTar } from '../src/storage/tar.ts'
|
||||
@@ -15,9 +18,9 @@ function seedPackage(vfs: MemoryVfs, name: string, exports: Record<string, strin
|
||||
for (const [path, body] of Object.entries(files)) vfs.seed(`${directory}/${path}`, body)
|
||||
}
|
||||
|
||||
it('archives every file of the packages that publish ./assets/* under its document-relative reference', () => {
|
||||
it('archives every file of the packages that publish ./assets/* under its unencoded page path', () => {
|
||||
const vfs = new MemoryVfs()
|
||||
seedPackage(vfs, '@fixture/ui', { './client': './lib/client.js', './assets/*': './lib/assets/*' }, {
|
||||
seedPackage(vfs, '@fixture/ui', { './client': './lib/client.js', [PACKAGE_ASSET_EXPORT]: PACKAGE_ASSET_EXPORT_TARGET }, {
|
||||
'lib/client.js': '',
|
||||
'lib/assets/mark-0123456789ab.svg': '<svg/>',
|
||||
'lib/assets/icons/whale@2x-0123456789ab.png': 'png',
|
||||
@@ -30,16 +33,19 @@ it('archives every file of the packages that publish ./assets/* under its docume
|
||||
|
||||
expect(Object.fromEntries(entries.map(entry => [entry.name, new TextDecoder().decode(entry.bytes)]))).toEqual({
|
||||
'plugins/@fixture/ui/assets/mark-0123456789ab.svg': '<svg/>',
|
||||
'plugins/@fixture/ui/assets/icons/whale%402x-0123456789ab.png': 'png',
|
||||
'plugins/@fixture/ui/assets/icons/whale@2x-0123456789ab.png': 'png',
|
||||
})
|
||||
expect(parseTar(packPackageAssets(new MemoryVfs(), ROOT))).toEqual([])
|
||||
})
|
||||
|
||||
it('maps each archived reference to a typed Blob URL and leaves other references unchanged', async () => {
|
||||
it('maps each archived file to a Blob URL under the reference and media type the Client build and route use', async () => {
|
||||
const vfs = new MemoryVfs()
|
||||
seedPackage(vfs, '@fixture/ui', { './assets/*': './lib/assets/*' }, {
|
||||
const long = `${'汉'.repeat(10)}-0123456789ab.png`
|
||||
seedPackage(vfs, '@fixture/ui', { [PACKAGE_ASSET_EXPORT]: PACKAGE_ASSET_EXPORT_TARGET }, {
|
||||
'lib/assets/mark-0123456789ab.svg': '<svg/>',
|
||||
'lib/assets/data-0123456789ab.bin': 'raw',
|
||||
[`lib/assets/${long}`]: 'png',
|
||||
...Object.fromEntries([...PACKAGE_ASSET_MEDIA_TYPES.keys()].map(extension => [`lib/assets/types/a${extension}`, extension])),
|
||||
})
|
||||
const blobs: Blob[] = []
|
||||
const NativeURL = URL
|
||||
@@ -52,7 +58,13 @@ it('maps each archived reference to a typed Blob URL and leaves other references
|
||||
vi.stubGlobal('URL', StubURL)
|
||||
|
||||
const resolve = packageAssetResolver(packPackageAssets(vfs, ROOT))
|
||||
const blobOf = (url: string): Blob | undefined => blobs[Number(url.slice('blob:fixture-'.length)) - 1]
|
||||
|
||||
for (const [extension, type] of PACKAGE_ASSET_MEDIA_TYPES) {
|
||||
expect(blobOf(resolve(packageAssetReference('@fixture/ui', `types/a${extension}`)))?.type).toBe(type)
|
||||
}
|
||||
// A name whose encoded reference exceeds a ustar name segment still archives and resolves.
|
||||
expect(await blobOf(resolve(packageAssetReference('@fixture/ui', long)))?.text()).toBe('png')
|
||||
const svg = resolve('plugins/@fixture/ui/assets/mark-0123456789ab.svg')
|
||||
const svgBlob = blobs[Number(svg.slice('blob:fixture-'.length)) - 1]
|
||||
expect(svgBlob?.type).toBe('image/svg+xml')
|
||||
|
||||
@@ -618,7 +618,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'async fetchBundle(request: Request): Promise<Response>',
|
||||
description: 'Serve an advertised revisioned bundle, source map, chunk, or package asset without a Web server. Unknown URLs return 404, unsupported methods return 405, and `HEAD` returns the same immutable headers without materializing a body. Each bundle body is built once on its first `GET`; script construction never reads maps. A package asset is read on every request and answers 304 without a body when `If-None-Match` equals its entity tag.',
|
||||
description: 'Serve an advertised revisioned bundle, source map, chunk, or package asset without a Web server. Unknown URLs return 404, unsupported methods return 405, and `HEAD` returns the same immutable headers without materializing a body. Each bundle body is built once on its first `GET`; script construction never reads maps. A package asset is read on every request and answers 304 without a body when `If-None-Match` equals its entity tag; its body holds the file open until the caller consumes or cancels it.',
|
||||
parameters: [{ name: 'request', description: 'shell-carrier request for a `/plugins` resource.' }],
|
||||
returns: 'the exact response also exposed by the optional Web route.',
|
||||
},
|
||||
@@ -636,7 +636,7 @@ export const SERVICE_API: readonly ServiceApiEntry[] = [
|
||||
},
|
||||
{
|
||||
signature: 'onRebuilt(listener: (id: string, rev: string) => void): () => void',
|
||||
description: 'Subscribe to bundle rebuilds; fires only when artifact metadata changes the rev.',
|
||||
description: 'Subscribe to bundle rebuilds; fires when rebuilt changes a rev. A rev an entry restart publishes reaches only onGraphChanged listeners.',
|
||||
parameters: [{ name: 'listener', description: 'receives the entry id and its new bundle rev.' }],
|
||||
returns: 'the unsubscriber.',
|
||||
},
|
||||
|
||||
@@ -280,12 +280,38 @@ describe('client bundle package asset imports', () => {
|
||||
expect(() => load.call(recordingContext(), asset)).toThrow(`${PACKAGE_NAME}: cannot resolve asset ${asset}`)
|
||||
})
|
||||
|
||||
it.each(['src/client/assets/100%.png', 'src/client/assets/icons#1/whale.png'])('rejects %s, which the asset route cannot serve', async (path) => {
|
||||
it.each([
|
||||
'src/client/assets/100%.png', 'src/client/assets/icons#1/whale.png', 'src/client/assets/Node_Modules/whale.png',
|
||||
])('rejects %s, which the asset route cannot serve', async (path) => {
|
||||
const root = await assetPackage({ [path]: WHALE })
|
||||
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
|
||||
|
||||
expect(() => load.call(recordingContext(), join(root, path)))
|
||||
.toThrow(`${join(root, path)} has a path segment containing %, #, ?, \\ or NUL, which the asset route cannot serve; rename it`)
|
||||
.toThrow(`${join(root, path)} has a node_modules path segment or one containing %, #, ?, \\ or NUL, which the asset route cannot serve; rename it`)
|
||||
})
|
||||
|
||||
it('rejects an uppercase extension, which the asset import types do not declare', async () => {
|
||||
const root = await assetPackage({ 'src/client/assets/whale.PNG': WHALE })
|
||||
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
|
||||
const asset = join(root, 'src/client/assets/whale.PNG')
|
||||
|
||||
expect(() => load.call(recordingContext(), asset)).toThrow(`${asset} has an uppercase extension`)
|
||||
})
|
||||
|
||||
it('passes a file name lightningcss escapes through the resolver call, and rejects a fragment or query', async () => {
|
||||
const root = await assetPackage({
|
||||
'src/client/assets/whale(2x).png': WHALE,
|
||||
'src/client/View.module.css': '.a { mask: url(\'./assets/whale(2x).png\'); }',
|
||||
'src/client/Bad.module.css': '.b { mask: url(\'./assets/whale(2x).png#part\'); }',
|
||||
})
|
||||
const plugin = browserPlugins(root)('dsh-css-modules-inline')
|
||||
const importer = join(root, 'src/client/index.ts')
|
||||
|
||||
const output = await loadImport(plugin, recordingContext(), './View.module.css', importer)
|
||||
expect(stylesheetText(output, reference => `resolved:${reference}`))
|
||||
.toContain(`resolved:plugins/@deepseek-ai/dsh-client-asset-fixture/assets/whale(2x)-${WHALE_REV}.png`)
|
||||
await expect(loadImport(plugin, recordingContext(), './Bad.module.css', importer))
|
||||
.rejects.toThrow('a package asset url() takes no fragment or query')
|
||||
})
|
||||
|
||||
it('rejects asset imports in the Node Client face', async () => {
|
||||
|
||||
Reference in new issue
Block a user