diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md
index 43c4263136..9004b1906f 100644
--- a/THIRD_PARTY_NOTICES.md
+++ b/THIRD_PARTY_NOTICES.md
@@ -27,7 +27,7 @@ The Cordis framework and its foundation libraries are source-vendored into this
## Runtime npm dependencies
-External packages that a workspace package resolves at runtime. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default.
+External packages installed for runtime use or distributed inside the prebuilt browser artifacts. Browser inputs are resolved through the shipping tsdown and Vite configurations, independently of npm dependency sections. The tier covers every plugin a user can mount from `cordis.yml` — not only what the `dsh` CLI, Web UI, and Python SDK runtime load by default.
| Package | License |
| --- | --- |
@@ -55,7 +55,6 @@ External packages that a workspace package resolves at runtime. The tier covers
| [`@shikijs/langs`](https://github.com/shikijs/shiki) | MIT |
| [`@standard-schema/spec`](https://github.com/standard-schema/standard-schema) | MIT |
| [`@tanstack/react-virtual`](https://github.com/TanStack/virtual) | MIT |
-| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@vscode/ripgrep`](https://github.com/microsoft/vscode-ripgrep) | MIT |
| [`@xterm/headless`](https://github.com/xtermjs/xterm.js) | MIT |
| [`@yarnpkg/parsers`](https://github.com/yarnpkg/berry) | BSD-2-Clause |
@@ -88,7 +87,6 @@ External packages that a workspace package resolves at runtime. The tier covers
| [`micromark-util-classify-character`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-classify-character) | MIT |
| [`micromark-util-sanitize-uri`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) | MIT |
| [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT |
-| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
| [`mime-types`](https://github.com/jshttp/mime-types) | MIT |
| [`negotiator`](https://github.com/jshttp/negotiator) | MIT |
| [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT |
@@ -138,7 +136,7 @@ The installed SDK 0.3.263 declares the following optional platform packages. Eac
## Development-only npm dependencies
-External packages **directly declared** only by repository tooling, test infrastructure, the documentation site, the demo leaves, or the native launcher's build workspace. No shipped surface names them itself. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on the full closure — so this tier records who declares a package, not what a build ultimately bundles.
+External packages **directly declared** for development, tests, types, or tooling, without a runtime installation or browser-build relationship. A package here may still be pulled in transitively by a runtime dependency — `pnpm-lock.yaml` is the authority on that full closure.
| Package | License |
| --- | --- |
@@ -155,6 +153,7 @@ External packages **directly declared** only by repository tooling, test infrast
| [`@types/compression`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
+| [`@types/mdast`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/mime-types`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/negotiator`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
| [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT |
@@ -190,6 +189,7 @@ External packages **directly declared** only by repository tooling, test infrast
| [`lefthook`](https://github.com/evilmartians/lefthook) | MIT |
| [`lightningcss`](https://github.com/parcel-bundler/lightningcss) | MPL-2.0 |
| [`mermaid`](https://github.com/mermaid-js/mermaid) | MIT |
+| [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT |
| [`msgpackr`](http://github.com/kriszyp/msgpackr) | MIT |
| [`oxlint`](https://github.com/oxc-project/oxc) | MIT |
| [`oxlint-tsgolint`](https://github.com/oxc-project/tsgolint) | MIT |
diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts
index dff22a99ec..0e15b4f088 100644
--- a/apps/web/vite.config.ts
+++ b/apps/web/vite.config.ts
@@ -46,15 +46,21 @@ function rejectStandaloneServe(): Plugin {
*/
function emitPreviewPage(): Plugin {
let bootstrapFile: string | undefined
+ let write = true
return {
name: 'dsh-emit-preview-page',
+ configResolved(config) {
+ write = config.build.write
+ },
generateBundle(_options, bundle) {
+ if (!write) return
for (const item of Object.values(bundle)) {
if (item.type === 'chunk' && item.isEntry && item.name === 'bootstrap') bootstrapFile = item.fileName
}
if (bootstrapFile === undefined) throw new Error('vite: preview bootstrap entry missing from the bundle')
},
async closeBundle() {
+ if (!write) return
// A build that failed before generateBundle has no page to splice.
if (bootstrapFile === undefined) return
const page = await readFile(src('./dist/index.html'), 'utf8')
diff --git a/lefthook.yml b/lefthook.yml
index 1c2a693c88..237202e7e0 100644
--- a/lefthook.yml
+++ b/lefthook.yml
@@ -28,7 +28,7 @@ pre-commit:
# lefthook only inspects files present on disk — so that one case still
# falls through to the freshness assertion in the test lane.
- name: third-party notices (staged)
- glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/build-exe-for-python-sdk.ts}'
+ glob: '{package.json,*/package.json,*/*/package.json,*/*/*/package.json,*/*/*/*/package.json,pnpm-workspace.yaml,*/*/pnpm-workspace.yaml,pnpm-lock.yaml,vendor/README.md,python/*/pyproject.toml,scripts/gen-third-party-notices.ts,scripts/browser-bundled-externals.ts,scripts/build-exe-for-python-sdk.ts,tsconfig.base*.json,packages/*/*/src/**/*,packages/*/*/tsdown.config.ts,packages/client/tsdown.client.ts,apps/*/src/**/*,apps/*/vite.config.ts}'
run: node_modules/.bin/tsx scripts/gen-third-party-notices.ts && git add THIRD_PARTY_NOTICES.md
- name: whitespace (staged)
diff --git a/packages/client/store/package.json b/packages/client/store/package.json
index 172439a947..6bb0c0a212 100644
--- a/packages/client/store/package.json
+++ b/packages/client/store/package.json
@@ -22,15 +22,13 @@
"./package.json": "./package.json"
},
"license": "MIT",
- "dependencies": {
- "immer": "^10.1.1",
- "zustand": "~4.4.7"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "zustand": "~4.4.7",
+ "immer": "^10.1.1"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-attachment/package.json b/packages/client/ui-attachment/package.json
index 7679b48621..9adf41af13 100644
--- a/packages/client/ui-attachment/package.json
+++ b/packages/client/ui-attachment/package.json
@@ -42,9 +42,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@types/react": "~18.3.1",
@@ -58,7 +55,8 @@
"react": "^18.2.0",
"react-dom": "^18.2.0",
"@deepseek-ai/dsh-attachment": "workspace:^",
- "@deepseek-ai/dsh-client-ui-tool": "workspace:^"
+ "@deepseek-ai/dsh-client-ui-tool": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-commands/package.json b/packages/client/ui-commands/package.json
index bc7b069162..40ce5a3bff 100644
--- a/packages/client/ui-commands/package.json
+++ b/packages/client/ui-commands/package.json
@@ -41,9 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -64,7 +61,8 @@
"@deepseek-ai/dsh-client-store": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
- "@deepseek-ai/dsh-client-ui-session": "workspace:^"
+ "@deepseek-ai/dsh-client-ui-session": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-conversation/package.json b/packages/client/ui-conversation/package.json
index 8902063d3c..987aa9574f 100644
--- a/packages/client/ui-conversation/package.json
+++ b/packages/client/ui-conversation/package.json
@@ -46,13 +46,7 @@
},
"license": "MIT",
"dependencies": {
- "clsx": "^2.0.0",
- "@deepseek-ai/schemastery": "workspace:^",
- "@lexical/history": "^0.49.0",
- "@lexical/plain-text": "^0.49.0",
- "@lexical/text": "^0.49.0",
- "@lexical/utils": "^0.49.0",
- "lexical": "^0.49.0"
+ "@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
@@ -93,7 +87,13 @@
"react-dom": "^18.2.0",
"@types/react-dom": "~18.3.0",
"zod": "^4.4.3",
- "@deepseek-ai/dsh-settings": "workspace:^"
+ "@deepseek-ai/dsh-settings": "workspace:^",
+ "lexical": "^0.49.0",
+ "clsx": "^2.0.0",
+ "@lexical/utils": "^0.49.0",
+ "@lexical/text": "^0.49.0",
+ "@lexical/plain-text": "^0.49.0",
+ "@lexical/history": "^0.49.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-directory-picker-browse/package.json b/packages/client/ui-directory-picker-browse/package.json
index aaf735a27f..ec0a9d8ed0 100644
--- a/packages/client/ui-directory-picker-browse/package.json
+++ b/packages/client/ui-directory-picker-browse/package.json
@@ -41,9 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -59,7 +56,8 @@
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-dockkit/package.json b/packages/client/ui-dockkit/package.json
index 8e7a97e4dd..968d1ba505 100644
--- a/packages/client/ui-dockkit/package.json
+++ b/packages/client/ui-dockkit/package.json
@@ -22,18 +22,15 @@
"./package.json": "./package.json"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
"devDependencies": {
"@deepseek-ai/cordis": "workspace:^",
"@deepseek-ai/dsh-brand": "workspace:^",
"@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "react": "^18.2.0",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-input-trigger/package.json b/packages/client/ui-input-trigger/package.json
index a5d7b679d9..65e30abb16 100644
--- a/packages/client/ui-input-trigger/package.json
+++ b/packages/client/ui-input-trigger/package.json
@@ -41,9 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -61,7 +58,8 @@
"@deepseek-ai/dsh-api-session-controller": "workspace:^",
"@deepseek-ai/dsh-client-store": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
- "@deepseek-ai/dsh-client-ui-session": "workspace:^"
+ "@deepseek-ai/dsh-client-ui-session": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-model-selection/package.json b/packages/client/ui-model-selection/package.json
index 4d87ef6ca0..d3c89480e8 100644
--- a/packages/client/ui-model-selection/package.json
+++ b/packages/client/ui-model-selection/package.json
@@ -63,14 +63,12 @@
"@deepseek-ai/dsh-client-store": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
- "@deepseek-ai/dsh-client-ui-session": "workspace:^"
+ "@deepseek-ai/dsh-client-ui-session": "workspace:^",
+ "clsx": "^2.1.1"
},
"files": [
"lib/index.js",
"lib/client.js",
"lib/types/**/*.d.ts"
- ],
- "dependencies": {
- "clsx": "^2.1.1"
- }
+ ]
}
diff --git a/packages/client/ui-primitives/package.json b/packages/client/ui-primitives/package.json
index 41fbdb4ebe..2613c3aa98 100644
--- a/packages/client/ui-primitives/package.json
+++ b/packages/client/ui-primitives/package.json
@@ -22,32 +22,30 @@
"./package.json": "./package.json"
},
"license": "MIT",
- "dependencies": {
- "@shikijs/langs": "^4.3.1",
- "@types/mdast": "^4.0.4",
- "anser": "^2.3.5",
- "clsx": "^2.0.0",
- "katex": "^0.16.47",
- "mdast-util-from-markdown": "^2.0.3",
- "mdast-util-gfm": "^3.1.0",
- "mdast-util-math": "^3.0.0",
- "micromark-core-commonmark": "^2.0.3",
- "micromark-extension-gfm": "^3.0.0",
- "micromark-extension-math": "^3.1.0",
- "micromark-factory-space": "^2.0.1",
- "micromark-util-character": "^2.1.1",
- "micromark-util-classify-character": "^2.0.1",
- "micromark-util-sanitize-uri": "^2.0.1",
- "micromark-util-symbol": "^2.0.1",
- "micromark-util-types": "^2.0.2",
- "react": "^18.2.0",
- "react-dom": "^18.2.0",
- "shiki": "^4.3.1"
- },
"devDependencies": {
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
- "@deepseek-ai/cordis": "workspace:^"
+ "@deepseek-ai/cordis": "workspace:^",
+ "react": "^18.2.0",
+ "clsx": "^2.0.0",
+ "react-dom": "^18.2.0",
+ "anser": "^2.3.5",
+ "katex": "^0.16.47",
+ "micromark-core-commonmark": "^2.0.3",
+ "micromark-util-character": "^2.1.1",
+ "micromark-util-classify-character": "^2.0.1",
+ "micromark-util-symbol": "^2.0.1",
+ "micromark-util-types": "^2.0.2",
+ "shiki": "^4.3.1",
+ "@shikijs/langs": "^4.3.1",
+ "@types/mdast": "^4.0.4",
+ "micromark-factory-space": "^2.0.1",
+ "micromark-extension-math": "^3.1.0",
+ "mdast-util-from-markdown": "^2.0.3",
+ "mdast-util-gfm": "^3.1.0",
+ "mdast-util-math": "^3.0.0",
+ "micromark-extension-gfm": "^3.0.0",
+ "micromark-util-sanitize-uri": "^2.0.1"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-renderer/package.json b/packages/client/ui-renderer/package.json
index 32b616d5a2..2aefdb4b1e 100644
--- a/packages/client/ui-renderer/package.json
+++ b/packages/client/ui-renderer/package.json
@@ -40,9 +40,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "use-sync-external-store": "1.2.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -55,7 +52,8 @@
"@types/react-dom": "~18.3.0",
"@types/use-sync-external-store": "^1.5.0",
"react": "^18.2.0",
- "react-dom": "^18.2.0"
+ "react-dom": "^18.2.0",
+ "use-sync-external-store": "1.2.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-settings-general/package.json b/packages/client/ui-settings-general/package.json
index a31d2dad8d..2d33e995f4 100644
--- a/packages/client/ui-settings-general/package.json
+++ b/packages/client/ui-settings-general/package.json
@@ -43,8 +43,7 @@
},
"license": "MIT",
"dependencies": {
- "@deepseek-ai/schemastery": "workspace:^",
- "clsx": "^2.0.0"
+ "@deepseek-ai/schemastery": "workspace:^"
},
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
@@ -64,7 +63,8 @@
"react": "^18.2.0",
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
- "@deepseek-ai/dsh-settings": "workspace:^"
+ "@deepseek-ai/dsh-settings": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-settings-plugins/package.json b/packages/client/ui-settings-plugins/package.json
index 9f82713d90..c4b27fae88 100644
--- a/packages/client/ui-settings-plugins/package.json
+++ b/packages/client/ui-settings-plugins/package.json
@@ -54,14 +54,12 @@
"@deepseek-ai/dsh-client-ui-slots": "workspace:^",
"@types/react": "~18.3.1",
"react": "^18.2.0",
- "@deepseek-ai/dsh-client-ui-renderer": "workspace:^"
+ "@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
"lib/client.js",
"lib/types/**/*.d.ts"
- ],
- "dependencies": {
- "clsx": "^2.0.0"
- }
+ ]
}
diff --git a/packages/client/ui-sidebar-files/package.json b/packages/client/ui-sidebar-files/package.json
index 9e1a7f0663..ce97ee1ffc 100644
--- a/packages/client/ui-sidebar-files/package.json
+++ b/packages/client/ui-sidebar-files/package.json
@@ -41,11 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -66,7 +61,10 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1",
- "@types/react-dom": "~18.3.0"
+ "@types/react-dom": "~18.3.0",
+ "react": "^18.2.0",
+ "clsx": "^2.0.0",
+ "react-dom": "^18.2.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-sidebar-right/package.json b/packages/client/ui-sidebar-right/package.json
index 699bebc01f..c8fad0215c 100644
--- a/packages/client/ui-sidebar-right/package.json
+++ b/packages/client/ui-sidebar-right/package.json
@@ -42,12 +42,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0",
- "picomatch": "^4.0.4",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -69,7 +63,11 @@
"@testing-library/react": "^16.1.0",
"@types/picomatch": "^4.0.2",
"@types/react": "~18.3.1",
- "@types/react-dom": "~18.3.0"
+ "@types/react-dom": "~18.3.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "picomatch": "^4.0.4",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-sidebar-textpreview/package.json b/packages/client/ui-sidebar-textpreview/package.json
index 756b6a3dab..7801a14e1d 100644
--- a/packages/client/ui-sidebar-textpreview/package.json
+++ b/packages/client/ui-sidebar-textpreview/package.json
@@ -41,11 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0",
- "react": "^18.2.0",
- "react-dom": "^18.2.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -67,7 +62,10 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@testing-library/react": "^16.1.0",
"@types/react": "~18.3.1",
- "@types/react-dom": "~18.3.0"
+ "@types/react-dom": "~18.3.0",
+ "react": "^18.2.0",
+ "clsx": "^2.0.0",
+ "react-dom": "^18.2.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-sidebar/package.json b/packages/client/ui-sidebar/package.json
index a6f1b9d4a3..7c1d5531fd 100644
--- a/packages/client/ui-sidebar/package.json
+++ b/packages/client/ui-sidebar/package.json
@@ -43,9 +43,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -61,7 +58,8 @@
"@deepseek-ai/dsh-client-ui-workspace": "workspace:^",
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
- "react": "^18.2.0"
+ "react": "^18.2.0",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-theme/package.json b/packages/client/ui-theme/package.json
index b642fe86a4..5f9dd4940c 100644
--- a/packages/client/ui-theme/package.json
+++ b/packages/client/ui-theme/package.json
@@ -56,7 +56,8 @@
"@deepseek-ai/dsh-host-webserver": "workspace:^",
"@types/react": "~18.3.1",
"react": "^18.2.0",
- "@deepseek-ai/dsh-settings": "workspace:^"
+ "@deepseek-ai/dsh-settings": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
@@ -69,7 +70,6 @@
"watch": "tsdown --watch"
},
"dependencies": {
- "clsx": "^2.0.0",
"@deepseek-ai/schemastery": "workspace:^"
}
}
diff --git a/packages/client/ui-tool/package.json b/packages/client/ui-tool/package.json
index 453d7867bb..3a3722d64d 100644
--- a/packages/client/ui-tool/package.json
+++ b/packages/client/ui-tool/package.json
@@ -41,9 +41,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -73,7 +70,8 @@
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
- "@deepseek-ai/dsh-attachment": "workspace:^"
+ "@deepseek-ai/dsh-attachment": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-trajectory/package.json b/packages/client/ui-trajectory/package.json
index 53965b1cb7..7f9752a6a8 100644
--- a/packages/client/ui-trajectory/package.json
+++ b/packages/client/ui-trajectory/package.json
@@ -42,10 +42,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "@tanstack/react-virtual": "^3.14.9",
- "diff": "^9.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -70,7 +66,9 @@
"@deepseek-ai/dsh-client-ui-renderer": "workspace:^",
"@deepseek-ai/dsh-client-ui-session": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
- "@deepseek-ai/dsh-attachment": "workspace:^"
+ "@deepseek-ai/dsh-attachment": "workspace:^",
+ "@tanstack/react-virtual": "^3.14.9",
+ "diff": "^9.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-user-questions/package.json b/packages/client/ui-user-questions/package.json
index 58b6a1c836..b32352c522 100644
--- a/packages/client/ui-user-questions/package.json
+++ b/packages/client/ui-user-questions/package.json
@@ -43,9 +43,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -68,7 +65,8 @@
"react": "^18.2.0",
"@deepseek-ai/dsh-client-ui-conversation": "workspace:^",
"@deepseek-ai/dsh-client-connection": "workspace:^",
- "@deepseek-ai/dsh-session": "workspace:^"
+ "@deepseek-ai/dsh-session": "workspace:^",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/packages/client/ui-workspace/package.json b/packages/client/ui-workspace/package.json
index 72876b6d9e..fc4c8d2855 100644
--- a/packages/client/ui-workspace/package.json
+++ b/packages/client/ui-workspace/package.json
@@ -46,9 +46,6 @@
"watch": "tsdown --watch"
},
"license": "MIT",
- "dependencies": {
- "clsx": "^2.0.0"
- },
"peerDependencies": {
"@deepseek-ai/cordis": "workspace:^"
},
@@ -72,7 +69,8 @@
"@deepseek-ai/dsh-util-workspace-path": "workspace:^",
"@types/react": "~18.3.1",
"@deepseek-ai/cordis": "workspace:^",
- "react": "^18.2.0"
+ "react": "^18.2.0",
+ "clsx": "^2.0.0"
},
"files": [
"lib/index.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0c3122497b..78a9c7877a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2192,17 +2192,16 @@ importers:
version: link:../../typert/protocol
packages/client/store:
- dependencies:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
immer:
specifier: ^10.1.1
version: 10.2.0
zustand:
specifier: ~4.4.7
version: 4.4.7(@types/react@18.3.31)(immer@10.2.0)(react@18.3.1)
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
packages/client/ui-agent-preset:
devDependencies:
@@ -2307,10 +2306,6 @@ importers:
version: 18.3.1
packages/client/ui-attachment:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2345,6 +2340,9 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -2492,10 +2490,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-commands:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2545,6 +2539,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -2554,24 +2551,6 @@ importers:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
- '@lexical/history':
- specifier: ^0.49.0
- version: 0.49.0(typescript@6.0.3)
- '@lexical/plain-text':
- specifier: ^0.49.0
- version: 0.49.0(typescript@6.0.3)
- '@lexical/text':
- specifier: ^0.49.0
- version: 0.49.0(typescript@6.0.3)
- '@lexical/utils':
- specifier: ^0.49.0
- version: 0.49.0(typescript@6.0.3)
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
- lexical:
- specifier: ^0.49.0
- version: 0.49.0(typescript@6.0.3)
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2666,12 +2645,30 @@ importers:
'@lexical/headless':
specifier: ^0.49.0
version: 0.49.0(typescript@6.0.3)
+ '@lexical/history':
+ specifier: ^0.49.0
+ version: 0.49.0(typescript@6.0.3)
+ '@lexical/plain-text':
+ specifier: ^0.49.0
+ version: 0.49.0(typescript@6.0.3)
+ '@lexical/text':
+ specifier: ^0.49.0
+ version: 0.49.0(typescript@6.0.3)
+ '@lexical/utils':
+ specifier: ^0.49.0
+ version: 0.49.0(typescript@6.0.3)
'@types/react':
specifier: ~18.3.1
version: 18.3.31
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
+ lexical:
+ specifier: ^0.49.0
+ version: 0.49.0(typescript@6.0.3)
react:
specifier: ^18.2.0
version: 18.3.1
@@ -2731,10 +2728,6 @@ importers:
version: 18.3.1
packages/client/ui-directory-picker-browse:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2766,6 +2759,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -2798,16 +2794,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-dockkit:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
- react:
- specifier: ^18.2.0
- version: 18.3.1
- react-dom:
- specifier: ^18.2.0
- version: 18.3.1(react@18.3.1)
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2824,6 +2810,15 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
packages/client/ui-goal:
devDependencies:
@@ -2886,10 +2881,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-input-trigger:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -2930,6 +2921,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -3061,10 +3055,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-model-selection:
- dependencies:
- clsx:
- specifier: ^2.1.1
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3117,6 +3107,9 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ clsx:
+ specifier: ^2.1.1
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -3263,13 +3256,22 @@ importers:
version: 18.3.1
packages/client/ui-primitives:
- dependencies:
+ devDependencies:
+ '@deepseek-ai/cordis':
+ specifier: workspace:^
+ version: link:../../../vendor/cordis
'@shikijs/langs':
specifier: ^4.3.1
version: 4.3.1
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
+ '@types/react':
+ specifier: ~18.3.1
+ version: 18.3.31
+ '@types/react-dom':
+ specifier: ~18.3.0
+ version: 18.3.7(@types/react@18.3.31)
anser:
specifier: ^2.3.5
version: 2.3.5
@@ -3324,16 +3326,6 @@ importers:
shiki:
specifier: ^4.3.1
version: 4.3.1
- devDependencies:
- '@deepseek-ai/cordis':
- specifier: workspace:^
- version: link:../../../vendor/cordis
- '@types/react':
- specifier: ~18.3.1
- version: 18.3.31
- '@types/react-dom':
- specifier: ~18.3.0
- version: 18.3.7(@types/react@18.3.31)
packages/client/ui-reference:
devDependencies:
@@ -3378,10 +3370,6 @@ importers:
version: link:../../util/workspace-path
packages/client/ui-renderer:
- dependencies:
- use-sync-external-store:
- specifier: 1.2.0
- version: 1.2.0(react@18.3.1)
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3410,6 +3398,9 @@ importers:
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+ use-sync-external-store:
+ specifier: 1.2.0
+ version: 1.2.0(react@18.3.1)
packages/client/ui-schedule:
devDependencies:
@@ -3527,9 +3518,6 @@ importers:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3573,6 +3561,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -3662,10 +3653,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-settings-plugins:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3697,15 +3684,14 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
packages/client/ui-sidebar:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3740,21 +3726,14 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
- react:
- specifier: ^18.2.0
- version: 18.3.1
-
- packages/client/ui-sidebar-files:
- dependencies:
clsx:
specifier: ^2.0.0
version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
- react-dom:
- specifier: ^18.2.0
- version: 18.3.1(react@18.3.1)
+
+ packages/client/ui-sidebar-files:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3807,21 +3786,17 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
-
- packages/client/ui-sidebar-right:
- dependencies:
clsx:
specifier: ^2.0.0
version: 2.1.1
- picomatch:
- specifier: ^4.0.4
- version: 4.0.4
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+
+ packages/client/ui-sidebar-right:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3877,18 +3852,20 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
-
- packages/client/ui-sidebar-textpreview:
- dependencies:
clsx:
specifier: ^2.0.0
version: 2.1.1
+ picomatch:
+ specifier: ^4.0.4
+ version: 4.0.4
react:
specifier: ^18.2.0
version: 18.3.1
react-dom:
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
+
+ packages/client/ui-sidebar-textpreview:
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -3944,6 +3921,15 @@ importers:
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
+ react:
+ specifier: ^18.2.0
+ version: 18.3.1
+ react-dom:
+ specifier: ^18.2.0
+ version: 18.3.1(react@18.3.1)
packages/client/ui-skill:
devDependencies:
@@ -4070,9 +4056,6 @@ importers:
'@deepseek-ai/schemastery':
specifier: link:../../../vendor/schemastery
version: link:../../../vendor/schemastery
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -4113,15 +4096,14 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
packages/client/ui-tool:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -4195,6 +4177,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -4203,13 +4188,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-trajectory:
- dependencies:
- '@tanstack/react-virtual':
- specifier: ^3.14.9
- version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- diff:
- specifier: ^9.0.0
- version: 9.0.0
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -4262,12 +4240,18 @@ importers:
'@deepseek-ai/dsh-tools':
specifier: workspace:^
version: link:../../core/tools
+ '@tanstack/react-virtual':
+ specifier: ^3.14.9
+ version: 3.14.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@types/react':
specifier: ~18.3.1
version: 18.3.31
'@types/react-dom':
specifier: ~18.3.0
version: 18.3.7(@types/react@18.3.31)
+ diff:
+ specifier: ^9.0.0
+ version: 9.0.0
react:
specifier: ^18.2.0
version: 18.3.1
@@ -4276,10 +4260,6 @@ importers:
version: 18.3.1(react@18.3.1)
packages/client/ui-user-questions:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -4335,6 +4315,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
@@ -4391,10 +4374,6 @@ importers:
version: 18.3.1
packages/client/ui-workspace:
- dependencies:
- clsx:
- specifier: ^2.0.0
- version: 2.1.1
devDependencies:
'@deepseek-ai/cordis':
specifier: workspace:^
@@ -4453,6 +4432,9 @@ importers:
'@types/react':
specifier: ~18.3.1
version: 18.3.31
+ clsx:
+ specifier: ^2.0.0
+ version: 2.1.1
react:
specifier: ^18.2.0
version: 18.3.1
diff --git a/scripts/browser-bundled-externals.spec.ts b/scripts/browser-bundled-externals.spec.ts
new file mode 100644
index 0000000000..b4d95cf96d
--- /dev/null
+++ b/scripts/browser-bundled-externals.spec.ts
@@ -0,0 +1,127 @@
+import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
+import { tmpdir } from 'node:os'
+import { dirname, join, resolve } from 'node:path'
+import { afterEach, describe, expect, it } from 'vitest'
+import { browserBundledExternals, browserPackageOfFile, browserSourceAliases } from './browser-bundled-externals.ts'
+
+const roots: string[] = []
+const repositoryRoot = resolve(import.meta.dirname, '..')
+
+afterEach(() => {
+ for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
+})
+
+function fixture(): string {
+ const root = mkdtempSync(join(tmpdir(), 'dsh-browser-notices-'))
+ roots.push(root)
+ write(root, 'package.json', '{"type":"module"}')
+ write(root, 'tsconfig.base.json', JSON.stringify({
+ compilerOptions: {
+ module: 'esnext', target: 'es2022', jsx: 'react-jsx',
+ paths: { '@fixture/static': ['./packages/client/static/src/index.ts'] },
+ },
+ }))
+ write(root, 'tsconfig.base.client.json', '{"extends":"./tsconfig.base.json"}')
+ return root
+}
+
+function write(root: string, path: string, text: string): void {
+ const target = join(root, path)
+ mkdirSync(dirname(target), { recursive: true })
+ writeFileSync(target, text)
+}
+
+function library(root: string, name: string, source = 'export const value = 1'): void {
+ write(root, `node_modules/${name}/package.json`, JSON.stringify({ name, type: 'module', exports: './index.js' }))
+ write(root, `node_modules/${name}/index.js`, source)
+}
+
+function dynamicPlugin(root: string, source: string): void {
+ write(root, 'packages/client/dynamic/package.json', JSON.stringify({
+ name: '@fixture/dynamic', dsh: { client: { platform: 'web' } },
+ }))
+ write(root, 'packages/client/dynamic/tsdown.config.ts', [
+ 'export default {',
+ ' name: "@fixture/dynamic/client",',
+ ' entry: { client: "src/client/index.ts" },',
+ ' inputOptions: { resolve: { conditionNames: ["browser", "import", "default"] } },',
+ '}',
+ ].join('\n'))
+ write(root, 'packages/client/dynamic/src/client/index.ts', source)
+}
+
+describe('browser dependency discovery', () => {
+ it('records runtime imports through the client config without following upstream dependencies or erased types', async () => {
+ const root = fixture()
+ library(root, 'browser-lib', 'export { value } from "transitive-lib"')
+ library(root, 'transitive-lib')
+ dynamicPlugin(root, [
+ 'import type { MissingType } from "type-only-lib"',
+ 'import { value } from "browser-lib"',
+ 'export const output: MissingType = value',
+ ].join('\n'))
+
+ expect(await browserBundledExternals(root)).toEqual(new Set(['browser-lib']))
+ expect(existsSync(join(root, 'packages/client/dynamic/lib'))).toBe(false)
+ })
+
+ it('rejects an unresolved runtime library', async () => {
+ const root = fixture()
+ dynamicPlugin(root, 'export { value } from "missing-browser-lib"')
+ await expect(browserBundledExternals(root)).rejects.toThrow('cannot resolve missing-browser-lib')
+ })
+
+ it('rejects a declared client without its browser config', async () => {
+ const root = fixture()
+ dynamicPlugin(root, 'export const value = 1')
+ write(root, 'packages/client/dynamic/tsdown.config.ts', 'export default { name: "@fixture/dynamic", entry: "src/index.ts" }')
+ await expect(browserBundledExternals(root)).rejects.toThrow('has no browser build config')
+ })
+
+ it('follows shell workspace aliases, CSS assets and lazy imports without writing output', async () => {
+ const root = fixture()
+ library(root, 'shell-lib')
+ library(root, 'lazy-lib')
+ library(root, 'asset-lib')
+ write(root, 'node_modules/asset-lib/package.json', JSON.stringify({
+ name: 'asset-lib', exports: { './theme.css': './theme.css' },
+ }))
+ write(root, 'node_modules/asset-lib/theme.css', '.fixture { color: red }')
+ write(root, 'packages/client/static/package.json', '{"name":"@fixture/static"}')
+ write(root, 'packages/client/static/src/index.ts', [
+ 'import { value } from "shell-lib"',
+ 'import "asset-lib/theme.css"',
+ 'export const output = value',
+ 'export const lazy = () => import("lazy-lib")',
+ ].join('\n'))
+ const app = join(root, 'apps/web')
+ write(root, 'apps/web/package.json', '{"name":"@fixture/web","type":"module","exports":{"./dist/*":"./dist/*"}}')
+ symlinkSync(resolve(repositoryRoot, 'apps/web/node_modules'), join(app, 'node_modules'), 'junction')
+ write(root, 'apps/web/index.html', '')
+ write(root, 'apps/web/main.ts', 'import { output, lazy } from "@fixture/static"; console.log(output); void lazy()')
+ write(root, 'apps/web/vite.config.ts', `export default {
+ build: { rollupOptions: { input: { index: ${JSON.stringify(join(app, 'index.html'))}, preview: "missing-preview.ts" } } }
+ }`)
+ write(root, 'apps/web/dist/sentinel.txt', 'untouched')
+
+ expect(await browserBundledExternals(root)).toEqual(new Set(['shell-lib', 'lazy-lib', 'asset-lib']))
+ expect(readFileSync(join(app, 'dist/sentinel.txt'), 'utf8')).toBe('untouched')
+ expect(existsSync(join(app, 'dist/index.html'))).toBe(false)
+ expect(existsSync(join(root, 'packages/client/static/lib'))).toBe(false)
+ })
+
+ it('normalizes installed module ids and excludes virtual/workspace modules', () => {
+ expect(browserPackageOfFile('/repo/node_modules/.pnpm/pkg@1/node_modules/pkg/a.js')).toBe('pkg')
+ expect(browserPackageOfFile('C:\\repo\\node_modules\\@scope\\pkg\\a.css?url')).toBe('@scope/pkg')
+ expect(browserPackageOfFile('/repo/packages/client/a.ts')).toBeUndefined()
+ expect(browserPackageOfFile('\0vite/modulepreload-polyfill')).toBeUndefined()
+ })
+
+ it('maps exact names and subpaths from the source facade', () => {
+ const root = fixture()
+ const aliases = browserSourceAliases(root)
+ expect('@fixture/static'.replace(aliases[0]!.find, aliases[0]!.replacement))
+ .toBe(join(root, 'packages/client/static/src/index.ts'))
+ expect(aliases[0]!.find.test('@fixture/static-extra')).toBe(false)
+ })
+})
diff --git a/scripts/browser-bundled-externals.ts b/scripts/browser-bundled-externals.ts
new file mode 100644
index 0000000000..85053be30f
--- /dev/null
+++ b/scripts/browser-bundled-externals.ts
@@ -0,0 +1,178 @@
+/** Resolve direct third-party browser inputs through the shipping build configurations, without emitting files. */
+
+import { globSync, readFileSync } from 'node:fs'
+import { createRequire } from 'node:module'
+import { dirname, resolve } from 'node:path'
+import { pathToFileURL } from 'node:url'
+import { Rolldown, type UserConfigExport } from 'tsdown'
+import ts from 'typescript'
+
+interface Manifest {
+ name: string
+ private?: boolean
+ dsh?: { client?: unknown }
+ exports?: Record
+}
+
+interface ResolveContext {
+ resolve(source: string, importer: string, options: { skipSelf: boolean }): Promise<{ id: string } | null>
+}
+
+/**
+ * Name of the installed package owning a bundler-resolved file.
+ * @param file - Resolved module or asset id, including any loader query.
+ * @returns Package name, or undefined for workspace and virtual modules.
+ */
+export function browserPackageOfFile(file: string): string | undefined {
+ const normalized = file.replaceAll('\\', '/')
+ const marker = normalized.lastIndexOf('/node_modules/')
+ if (marker < 0) return undefined
+ const parts = normalized.slice(marker + '/node_modules/'.length).split('/')
+ return parts[0]?.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
+}
+
+function recorder(seen: Set, workspaceNames: ReadonlySet, followWorkspace = false) {
+ return {
+ name: 'dsh-browser-direct-dependencies',
+ enforce: 'pre' as const,
+ resolveId: {
+ order: 'pre' as const,
+ async handler(this: ResolveContext, source: string, importer: string | undefined) {
+ if (importer === undefined || source.startsWith('.') || source.startsWith('/')
+ || source.startsWith('\0') || source.startsWith('node:')) return null
+ const parts = source.split('/')
+ const name = source.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
+ if (name !== undefined && workspaceNames.has(name)) {
+ return followWorkspace ? null : { id: source, external: true }
+ }
+ const resolved = await this.resolve(source, importer, { skipSelf: true })
+ if (resolved === null) throw new Error(`browser notices: cannot resolve ${source} from ${importer}`)
+ const owner = browserPackageOfFile(resolved.id)
+ if (owner === undefined) return resolved
+ if (browserPackageOfFile(importer) === undefined) seen.add(owner)
+ // Notices disclose direct dependencies; upstream implementation imports stay in the lockfile.
+ return { id: source, external: true }
+ },
+ },
+ }
+}
+
+function readManifest(path: string): Manifest {
+ return JSON.parse(readFileSync(path, 'utf8')) as Manifest
+}
+
+/**
+ * Source aliases shared with the repository's source-plane TypeScript programs.
+ * @param root - Repository root containing tsconfig.base.json.
+ * @returns Exact and wildcard aliases for the Vite dependency walk.
+ */
+export function browserSourceAliases(root: string): { find: RegExp; replacement: string }[] {
+ const path = resolve(root, 'tsconfig.base.json')
+ const config = ts.readConfigFile(path, file => ts.sys.readFile(file))
+ if (config.error !== undefined) throw new Error(ts.flattenDiagnosticMessageText(config.error.messageText, '\n'))
+ const parsed = ts.parseJsonConfigFileContent(config.config, ts.sys, root)
+ return Object.entries(parsed.options.paths ?? {}).map(([name, targets]) => {
+ const target = targets[0]
+ if (target === undefined) throw new Error(`browser notices: ${name} has no source target in ${path}`)
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace('\\*', '(.*)')
+ return { find: new RegExp(`^${escaped}$`), replacement: resolve(root, target).replace('*', '$1') }
+ })
+}
+
+async function collectClientBundles(
+ root: string,
+ manifests: ReadonlyMap,
+ workspaceNames: ReadonlySet,
+ seen: Set,
+): Promise {
+ for (const [manifestPath, manifest] of manifests) {
+ if (manifest.private === true || manifest.dsh?.client === undefined) continue
+ const dir = dirname(manifestPath)
+ const loaded = await import(pathToFileURL(resolve(dir, 'tsdown.config.ts')).href) as { default: UserConfigExport }
+ const factory = await loaded.default
+ const configured = typeof factory === 'function' ? await factory({ env: {} }, { ci: false }) : factory
+ const configs = Array.isArray(configured) ? configured : [configured]
+ const client = configs.find(config => config.name === `${manifest.name}/client`)
+ if (client === undefined) throw new Error(`browser notices: ${manifest.name} has no browser build config`)
+ if (typeof client.inputOptions === 'function') throw new Error(`browser notices: ${manifest.name} needs resolved input options`)
+ const bundle = await Rolldown.rolldown({
+ ...client.inputOptions,
+ cwd: dir,
+ input: client.entry as Rolldown.InputOption,
+ platform: 'browser',
+ transform: client.define === undefined ? {} : { define: client.define },
+ plugins: [recorder(seen, workspaceNames), client.plugins ?? []] as NonNullable,
+ tsconfig: resolve(root, 'tsconfig.base.client.json'),
+ })
+ try {
+ await bundle.generate({ format: 'cjs', sourcemap: false })
+ } finally {
+ await bundle.close()
+ }
+ }
+}
+
+interface ShellConfig {
+ build: { rollupOptions?: { input?: string | string[] | Record } }
+}
+
+interface ViteApi {
+ resolveConfig(config: Record, command: 'build'): Promise
+ build(config: Record): Promise
+}
+
+async function collectShell(
+ root: string,
+ workspaceNames: ReadonlySet,
+ seen: Set,
+): Promise {
+ for (const path of globSync('apps/*/vite.config.ts', { cwd: root }).sort()) {
+ const dir = dirname(resolve(root, path))
+ const manifest = readManifest(resolve(dir, 'package.json'))
+ if (manifest.private === true || manifest.exports?.['./dist/*'] === undefined) continue
+ const vitePath = createRequire(resolve(dir, 'package.json')).resolve('vite')
+ const vite = await import(pathToFileURL(vitePath).href) as ViteApi
+ const config = await vite.resolveConfig({ root: dir, logLevel: 'error' }, 'build')
+ const input = config.build.rollupOptions?.input
+ const entries = typeof input === 'string' ? [input] : Object.values(input ?? {})
+ const pages = entries.filter(entry => entry.endsWith('.html'))
+ if (pages.length === 0) throw new Error(`browser notices: ${manifest.name} has no HTML build entry`)
+ await vite.build({
+ root: dir,
+ logLevel: 'error',
+ plugins: [recorder(seen, workspaceNames, true)],
+ resolve: { alias: browserSourceAliases(root) },
+ build: {
+ write: false,
+ minify: false,
+ sourcemap: false,
+ reportCompressedSize: false,
+ rollupOptions: {
+ input: pages.length === 1 ? pages[0] : pages,
+ // Chunk coloring expects full third-party bodies; the disclosure walk stops at their imports.
+ output: { manualChunks: () => undefined },
+ },
+ },
+ })
+ }
+}
+
+/**
+ * Direct third-party packages resolved by published browser builds.
+ * @param root - Repository root with installed build dependencies; lib/ is not required.
+ * @returns Names of distributed browser inputs, excluding workspace packages and erased types.
+ */
+export async function browserBundledExternals(root: string): Promise> {
+ const manifests = new Map()
+ for (const glob of ['packages/*/*/package.json', 'vendor/*/package.json']) {
+ for (const path of globSync(glob, { cwd: root }).sort()) {
+ const absolute = resolve(root, path)
+ manifests.set(absolute, readManifest(absolute))
+ }
+ }
+ const names = new Set([...manifests.values()].map(manifest => manifest.name))
+ const seen = new Set()
+ await collectClientBundles(root, manifests, names, seen)
+ await collectShell(root, names, seen)
+ return seen
+}
diff --git a/scripts/gen-third-party-notices.spec.ts b/scripts/gen-third-party-notices.spec.ts
index 7bf3f88b18..fabce6e8c9 100644
--- a/scripts/gen-third-party-notices.spec.ts
+++ b/scripts/gen-third-party-notices.spec.ts
@@ -4,6 +4,7 @@ import { tmpdir } from 'node:os'
import { describe, expect, it } from 'vitest'
import {
CLAUDE_AGENT_SDK_PACKAGE,
+ assertRuntimeLicenses,
claudeDistributionFromManifest,
collectPythonDependencies,
isOwnerAuthorizedRuntime,
@@ -24,8 +25,11 @@ describe('THIRD_PARTY_NOTICES.md', () => {
// already runs in the test lane, so the check costs no extra CI process.
// Pre-commit regenerates the file whenever a manifest is staged, so reaching
// this assertion means the notices were committed without that hook.
- it('matches what the generator produces from the current manifests', () => {
- const generated = render()
+ // This case resolves all browser build graphs as well as installed license metadata.
+ it('matches what the generator produces from the current manifests', {
+ timeout: 120_000,
+ }, async () => {
+ const generated = await render()
expect(generated).toContain('It depends on the third-party software listed below.')
expect(readFileSync(resolve(root, 'THIRD_PARTY_NOTICES.md'), 'utf8'), 'stale notices — run `pnpm run gen-third-party-notices`').toBe(generated)
})
@@ -42,6 +46,37 @@ function workspace(entries: Record): { manifests: Map {
+ it('keeps license rejection active when a browser library is declared for development', () => {
+ const { manifests, names } = workspace({
+ 'packages/client/ui/package.json': { devDependencies: { 'browser-lib': '^1', 'test-tool': '^1' } },
+ })
+ const tiers = tierExternalDeps(manifests, names, new Set(['browser-lib']))
+ const dependencies = [{ name: 'browser-lib', license: 'GPL-3.0-only' }, { name: 'test-tool', license: 'GPL-3.0-only' }]
+ .filter(dep => tiers.get(dep.name))
+ expect(dependencies.map(dep => dep.name)).toEqual(['browser-lib'])
+ expect(() => { assertRuntimeLicenses(dependencies) }).toThrow('browser-lib (GPL-3.0-only)')
+ expect(() => { assertRuntimeLicenses([{ name: 'browser-lib', license: 'MIT' }]) }).not.toThrow()
+ expect(() => { assertRuntimeLicenses([{ name: CLAUDE_AGENT_SDK_PACKAGE, license: 'SEE LICENSE IN README.md' }]) })
+ .not.toThrow()
+ })
+
+ it('keeps browser-bundled development dependencies in runtime disclosures', () => {
+ const { manifests, names } = workspace({
+ 'packages/client/ui/package.json': {
+ name: '@fixture/ui', devDependencies: { react: '^18', 'browser-lib': '^1', 'type-only': '^1' },
+ },
+ })
+ expect(tierExternalDeps(manifests, names, new Set(['react', 'browser-lib']))).toEqual(new Map([
+ ['tsx', true], ['react', true], ['browser-lib', true], ['type-only', false],
+ ]))
+ })
+
+ it('rejects a browser library missing from the disclosed declarations', () => {
+ const { manifests, names } = workspace({})
+ expect(() => tierExternalDeps(manifests, names, new Set(['missing-lib'])))
+ .toThrow('browser package missing-lib has no workspace dependency declaration')
+ })
+
it('tiers by declaring area, not by the declaring section name', () => {
const { manifests, names } = workspace({
// Root tooling and test infrastructure never ship, whichever section declares them.
diff --git a/scripts/gen-third-party-notices.ts b/scripts/gen-third-party-notices.ts
index b9bf11e182..9e83956e5e 100644
--- a/scripts/gen-third-party-notices.ts
+++ b/scripts/gen-third-party-notices.ts
@@ -13,6 +13,7 @@ import { dirname, resolve } from 'node:path'
import * as yaml from 'js-yaml'
import { parse as parseToml, type TomlTableWithoutBigInt, type TomlValueWithoutBigInt } from 'smol-toml'
import parseSpdx from 'spdx-expression-parse'
+import { browserBundledExternals } from './browser-bundled-externals.ts'
const root = resolve(import.meta.dirname, '..')
const OUT = 'THIRD_PARTY_NOTICES.md'
@@ -371,14 +372,11 @@ function normalizeRepo(raw: string | undefined): string | undefined {
}
/**
- * External npm dependencies, tiered by which workspace area declares them at
- * runtime: a package is runtime when any manifest outside `DEV_ONLY_AREAS`
- * names it in `dependencies`/`optionalDependencies`. A package declared only
- * by tooling, test infrastructure, the website, or the demo leaves — whatever
- * the declaring section is called — is development-only.
+ * Direct npm dependencies distributed through installed runtime libraries or
+ * browser builds. Tooling declarations alone do not imply distribution.
*/
-function collectNpmDeps(manifests: Map, names: Set): ExternalDep[] {
- return [...tierExternalDeps(manifests, names)]
+function collectNpmDeps(manifests: Map, names: Set, browser: ReadonlySet): ExternalDep[] {
+ return [...tierExternalDeps(manifests, names, browser)]
.filter(([name]) => !FIRST_PARTY.has(name))
.sort(([a], [b]) => a.localeCompare(b))
.map(([name, runtime]) => ({ name, ...installedMetadata(name, manifests), runtime }))
@@ -388,9 +386,12 @@ function collectNpmDeps(manifests: Map, names: Set): E
* Tier every external dependency the workspace declares.
* @param manifests - workspace manifests keyed by repository-relative path.
* @param names - every workspace package name, which never counts as external.
+ * @param browser - Direct third-party packages resolved by the browser builds.
* @returns each external package mapped to whether it is a runtime dependency.
*/
-export function tierExternalDeps(manifests: Map, names: Set): Map {
+export function tierExternalDeps(
+ manifests: Map, names: Set, browser: ReadonlySet = new Set(),
+): Map {
const tiers = new Map()
// `tsx` is runtime by fiat: the root source-run scripts execute through its ESM hook.
tiers.set('tsx', true)
@@ -399,11 +400,14 @@ export function tierExternalDeps(manifests: Map, names: Set !isPermissive(dep.license) && !isOwnerAuthorizedRuntime(dep.name))
+ if (rejected.length > 0) {
+ throw new Error(`gen-third-party-notices: runtime ${rejected.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
+ }
+}
+
/**
* Render the sentence that isolates non-permissive development tooling, or
* nothing at all when every development dependency is permissive.
@@ -671,14 +687,15 @@ ${rows.join('\n')}
/**
* Render the complete notices document.
- * @returns the exact bytes `THIRD_PARTY_NOTICES.md` must hold.
+ * @returns The exact bytes THIRD_PARTY_NOTICES.md must hold after resolving browser inputs.
*/
-export function render(): string {
+export async function render(): Promise {
+ const browser = await browserBundledExternals(root)
// The linked-manifest cache is keyed by name only, so it must not outlive
// the manifests map it was resolved from; render() owns that single load.
workspaceLinkedManifestCache.clear()
const { manifests, names } = loadWorkspaceManifests()
- const npm = collectNpmDeps(manifests, names)
+ const npm = collectNpmDeps(manifests, names, browser)
const runtimeDeps = npm.filter(dep => dep.runtime)
const devDeps = npm.filter(dep => !dep.runtime)
const vendored = collectVendored()
@@ -690,15 +707,7 @@ export function render(): string {
? collectClaudeDistribution(manifests)
: undefined
const nonPermissiveDev = devDeps.filter(dep => !isPermissive(dep.license))
- // A copyleft license reaching a shipped surface is a distribution decision,
- // not a rendering detail; the notices cannot quietly absorb it.
- const nonPermissiveRuntime = runtimeDeps.filter(dep =>
- !isPermissive(dep.license)
- && !isOwnerAuthorizedRuntime(dep.name),
- )
- if (nonPermissiveRuntime.length > 0) {
- throw new Error(`gen-third-party-notices: runtime ${nonPermissiveRuntime.map(dep => `${dep.name} (${dep.license})`).join(', ')} is not a permissive license; review the distribution terms and record the decision before regenerating.`)
- }
+ assertRuntimeLicenses(runtimeDeps)
const patchedLines = patched.map(({ spec, patch }) => `- \`${spec}\` — [\`${patch}\`](${patch})`)
return `