mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-10-26 16:55:08 +00:00
fix(client): keep plugin strings out of the asset resolver rewrite
This commit is contained in:
1 parent
4fb0054448
commit
d7191fafb6
5 files changed
+36
-14
No files matched your search
+2
-2
@@ -15,8 +15,8 @@
|
||||
en: 704267953c123629
|
||||
zh: 860997024ad7825e
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/build:
|
||||
en: da95b904bd60815f
|
||||
zh: 3a598bacc159abc4
|
||||
en: 485013b2b6778fa2
|
||||
zh: 7e5cb238bb2b8585
|
||||
/agent-note-client-plugin-package-assets-served-by-url/decision/host-route:
|
||||
en: 151b7300f5b27a9f
|
||||
zh: fae9adff942c2ce8
|
||||
|
||||
@@ -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, 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.
|
||||
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. lightningcss does not call its `Url` visitor inside `image-set()`, so an `Image` visitor rewrites those options with the same rule. 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
|
||||
|
||||
|
||||
+1
-1
@@ -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/*`;扩展名不受支持;路径段为 `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,运行它的页面会请求那些文件名。
|
||||
动态浏览器 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 不会在 `image-set()` 内部调用 `Url` visitor,因此由 `Image` visitor 按同样的规则改写这些选项。改写时先写入 lightningcss 从不转义的标识符占位,因此含括号或引号的文件名仍会变成对应的解析调用。双 face 包的 Node Client face 拒绝资源 import。浏览器 face 是 `lib/assets/` 的唯一写入方:一次性构建会删除本次没有产出的文件,watch 重建则保留它们,因为 Host 仍在提供上一代 bundle,运行它的页面会请求那些文件名。
|
||||
|
||||
### Host 路由
|
||||
|
||||
|
||||
@@ -516,9 +516,10 @@ const URL_SCHEME = /^[a-z][a-z\d+.-]*:/i
|
||||
* Free identifier an asset reference compiles to before rendering; the CommonJS
|
||||
* render turns each call into `require.asset(...)`, the module loader's
|
||||
* synchronous asset resolver, which returns the reference unchanged unless its
|
||||
* carrier maps it.
|
||||
* carrier maps it. The random part keeps plugin strings that happen to contain
|
||||
* the fixed prefix out of that rewrite.
|
||||
*/
|
||||
const PACKAGE_ASSET_CALL = '__dshPackageAsset'
|
||||
const PACKAGE_ASSET_CALL = `__dshPackageAsset_${randomBytes(6).toString('hex')}`
|
||||
|
||||
/** Compile one asset reference into the resolver call. */
|
||||
function packageAssetCall(reference: string): string {
|
||||
@@ -666,7 +667,7 @@ function packageAssets(manifest: PackageManifest, packageDir: string): PackageAs
|
||||
const rewrite = (url: Url): Url | undefined => {
|
||||
// Empty values, fragments, root-relative paths, schemes such as `data:`, and placeholders name no package file.
|
||||
if (url.url === '' || url.url.startsWith('#') || url.url.startsWith('/') || URL_SCHEME.test(url.url)) return undefined
|
||||
if (url.url.startsWith('__dshPackageAsset_')) return undefined
|
||||
if (inserted.has(url.url)) return undefined
|
||||
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`)
|
||||
}
|
||||
|
||||
@@ -43,6 +43,16 @@ const WHALE = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1
|
||||
const WHALE_REV = packageAssetRevision(WHALE)
|
||||
const WHALE_URL = `plugins/@deepseek-ai/dsh-client-asset-fixture/assets/whale-${WHALE_REV}.png`
|
||||
|
||||
/** The per-build resolver call identifier a compiled module emits before rendering. */
|
||||
const RESOLVER_CALL = /__dshPackageAsset_[0-9a-f]{12}(?=\()/u
|
||||
|
||||
/** Read the resolver call identifier from one compiled module. */
|
||||
function resolverCall(output: string | null): string {
|
||||
const call = RESOLVER_CALL.exec(output ?? '')?.[0]
|
||||
if (call === undefined) throw new Error('compiled module carries no resolver call')
|
||||
return call
|
||||
}
|
||||
|
||||
/**
|
||||
* Evaluate the stylesheet text an emitted style module or inline-text module computes.
|
||||
* @param output - emitted module source.
|
||||
@@ -51,10 +61,11 @@ const WHALE_URL = `plugins/@deepseek-ai/dsh-client-asset-fixture/assets/whale-${
|
||||
*/
|
||||
function stylesheetText(output: string | null, resolve: (reference: string) => string): string {
|
||||
if (output === null) throw new Error('stylesheet module was not emitted')
|
||||
const call = RESOLVER_CALL.exec(output)?.[0]
|
||||
const body = output
|
||||
.replace(/^export default (.*);$/mu, 'return typeof css === "undefined" ? $1 : css;')
|
||||
.replace(/^export \{\};$/mu, 'return css;')
|
||||
return runInNewContext(`(() => {\n${body}\n})()`, { __dshPackageAsset: resolve, document: undefined }) as string
|
||||
return runInNewContext(`(() => {\n${body}\n})()`, { ...call === undefined ? {} : { [call]: resolve }, document: undefined }) as string
|
||||
}
|
||||
|
||||
const temporaryRoots: string[] = []
|
||||
@@ -201,7 +212,7 @@ describe('client bundle package asset imports', () => {
|
||||
const first = await load.call(context, id)
|
||||
const second = await load.call(context, id)
|
||||
|
||||
const expected = `export default __dshPackageAsset("plugins/@deepseek-ai/dsh-client-asset-fixture/assets/icons/whale%402x-${WHALE_REV}.png");`
|
||||
const expected = `export default ${resolverCall(first)}("plugins/@deepseek-ai/dsh-client-asset-fixture/assets/icons/whale%402x-${WHALE_REV}.png");`
|
||||
expect(id).toBe(asset)
|
||||
expect([first, second]).toEqual([expected, expected])
|
||||
expect(emittedFiles(context)).toEqual([{
|
||||
@@ -219,12 +230,16 @@ describe('client bundle package asset imports', () => {
|
||||
|
||||
it('renders each resolver call as the module loader require.asset in the CommonJS bundle only', async () => {
|
||||
const root = await assetPackage({ 'src/client/assets/whale.png': WHALE })
|
||||
const { renderChunk } = browserPlugins(root)('dsh-package-asset')
|
||||
const plugin = browserPlugins(root)('dsh-package-asset')
|
||||
const { renderChunk } = plugin
|
||||
if (renderChunk === undefined) throw new Error('asset plugin has no renderChunk hook')
|
||||
const code = `const a = __dshPackageAsset("${WHALE_URL}");\nconst css = "x" + __dshPackageAsset("${WHALE_URL}") + "y";\n`
|
||||
const call = resolverCall(await hooks(plugin).load.call(recordingContext(), join(root, 'src/client/assets/whale.png')))
|
||||
// Plugin data that only shares the fixed prefix stays as written.
|
||||
const literal = 'const text = "__dshPackageAsset(";\n'
|
||||
const code = `const a = ${call}("${WHALE_URL}");\nconst css = "x" + ${call}("${WHALE_URL}") + "y";\n${literal}`
|
||||
|
||||
expect(renderChunk(code, {}, { format: 'cjs' })?.toString())
|
||||
.toBe(`const a = require.asset("${WHALE_URL}");\nconst css = "x" + require.asset("${WHALE_URL}") + "y";\n`)
|
||||
.toBe(`const a = require.asset("${WHALE_URL}");\nconst css = "x" + require.asset("${WHALE_URL}") + "y";\n${literal}`)
|
||||
expect(renderChunk(code, {}, { format: 'es' })).toBeNull()
|
||||
expect(renderChunk('const a = 1;\n', {}, { format: 'cjs' })).toBeNull()
|
||||
})
|
||||
@@ -325,7 +340,11 @@ describe('client bundle package asset imports', () => {
|
||||
it('keeps literal placeholder-like text and resolves only the references the rewrite inserted', async () => {
|
||||
const root = await assetPackage({
|
||||
'src/client/assets/whale.png': WHALE,
|
||||
'src/client/theme.css': '.a::after { content: "__dshPackageAsset0__"; } .b { background: url(./assets/whale.png); }',
|
||||
'src/client/assets/__dshPackageAsset_logo.png': WHALE,
|
||||
'src/client/theme.css': [
|
||||
'.a::after { content: "__dshPackageAsset0__"; } .b { background: url(./assets/whale.png); }',
|
||||
'.c { background: url(./assets/__dshPackageAsset_logo.png); }',
|
||||
].join('\n'),
|
||||
})
|
||||
|
||||
const output = await loadImport(browserPlugins(root)('dsh-css-global-inline'), recordingContext(), './theme.css', join(root, 'src/client/index.ts'))
|
||||
@@ -333,6 +352,8 @@ describe('client bundle package asset imports', () => {
|
||||
const css = stylesheetText(output, reference => `resolved:${reference}`)
|
||||
expect(css).toContain('content:"__dshPackageAsset0__"')
|
||||
expect(css).toContain(`url(resolved:${WHALE_URL})`)
|
||||
// A real file whose name shares the placeholder prefix is still a package asset.
|
||||
expect(css).toContain(`url(resolved:plugins/@deepseek-ai/dsh-client-asset-fixture/assets/__dshPackageAsset_logo-${WHALE_REV}.png)`)
|
||||
})
|
||||
|
||||
it('rejects an uppercase extension, which the asset import types do not declare', async () => {
|
||||
@@ -391,7 +412,7 @@ describe('client bundle stylesheet url()', () => {
|
||||
|
||||
const output = await loadImport(browserPlugins(root)('dsh-css-modules-inline'), context, './View.module.css', join(root, 'src/client/index.ts'))
|
||||
|
||||
expect(output).toContain(`url(" + __dshPackageAsset(${JSON.stringify(WHALE_URL)}) + ")`)
|
||||
expect(output).toContain(`url(" + ${resolverCall(output)}(${JSON.stringify(WHALE_URL)}) + ")`)
|
||||
const css = stylesheetText(output, reference => `resolved:${reference}`)
|
||||
expect(css).toContain(`url(resolved:${WHALE_URL})`)
|
||||
for (const preserved of ['(mask-image:url(\'\'))', 'background:url()', 'url(data:image/svg+xml,%3Csvg%3E)', 'url(#mask)', 'url(/static/a.png)', 'url(https://example.com/a.png)']) {
|
||||
|
||||
Reference in new issue
Block a user