2026-07-19 21:17:57 +08:00
import { fileURLToPath } from 'node:url'
import { defineConfig } from 'vite'
2026-07-28 18:04:21 +08:00
import type { Plugin } from 'vite'
2026-07-19 21:17:57 +08:00
import react from '@vitejs/plugin-react'
const src = ( rel : string ) : string = > fileURLToPath ( new URL ( rel , import . meta . url ) )
2026-07-28 18:04:21 +08:00
const STANDALONE_ERROR = 'apps/web is not a standalone application: bare Vite cannot inject window.__DSH_BOOT__. '
+ 'Build with `pnpm run build && pnpm run build:web`, then run `dsh web` (repository checkout: `pnpm run dsh -- web`). '
+ 'For client-plugin HMR, run `pnpm run dsh -- web --dev` together with `pnpm run dev:web`.'
/** Fail before a Vite dev or preview server can expose the boot-manifest-free shell. */
function rejectStandaloneServe ( ) : Plugin {
return {
name : 'dsh-reject-standalone-web-serve' ,
2026-07-28 22:12:00 +08:00
config ( _config , env ) {
if ( env . command === 'serve' ) throw new Error ( STANDALONE_ERROR )
} ,
2026-07-28 18:04:21 +08:00
}
}
2026-07-19 21:17:57 +08:00
2026-08-06 15:48:10 +08:00
/**
* Vendor-chunk membership, by exact npm package name — the heavy render
* families (math, highlight, markdown) that change only on dependency bumps.
* Only packages workspace code imports DIRECTLY need listing: their private
* transitive dependencies (unified/hast/oniguruma machinery, ~50 packages)
* are imported solely by these and rollup's chunk coloring pulls them into
* vendor automatically. A dependency shared with index-side code falls back
* to index — a few kB of dilution, never a correctness problem. Anything not
* listed (react family, the vendored cordis workspace, tiny helpers like
* anser/clsx, all workspace code) stays in the default `index` chunk, so
* editing shell code re-hashes only index and returning clients keep the
* cached vendor chunk.
*/
const VENDOR_PACKAGES : ReadonlySet < string > = new Set ( [
// math
'katex' ,
'rehype-katex' ,
// syntax highlight (@shikijs/langs is handled separately below —
// lazy grammars must not land here)
'shiki' ,
// markdown pipeline
'react-markdown' ,
'remark-gfm' ,
'remark-math' ,
'mdast-util-from-markdown' ,
'mdast-util-gfm' ,
'micromark-extension-gfm' ,
'micromark-extension-math' ,
'micromark-factory-space' ,
'micromark-util-character' ,
'micromark-util-symbol' ,
'micromark-util-types' ,
] )
/**
* Boot grammars statically imported by ui-primitives' highlight.ts
* (`@shikijs/langs/typescript` → `dist/typescript.mjs`, etc.). They live in
* the same package as the lazy read-card grammars, but unlike those they are
* part of the initial load and belong in the vendor chunk; the lazy ones must
* stay unassigned so each keeps its own on-demand chunk.
*/
const BOOT_GRAMMAR_FILES : readonly string [ ] = [
'dist/typescript.mjs' ,
'dist/shellscript.mjs' ,
'dist/json.mjs' ,
]
/** Font asset extensions routed to assets/fonts/ (KaTeX's woff2/woff/ttf faces today). */
const FONT_EXTENSIONS : readonly string [ ] = [ '.woff2' , '.woff' , '.ttf' ]
/** npm package name of a resolved module id (the segment after the LAST `node_modules/` — pnpm nests the real package under an inner node_modules). */
function npmPackageOf ( id : string ) : string | undefined {
const parts = id . split ( '/node_modules/' )
if ( parts . length === 1 ) return undefined
const [ first , second ] = parts [ parts . length - 1 ] . split ( '/' )
if ( first . startsWith ( '.' ) ) return undefined // .pnpm store segment, not a package
return first . startsWith ( '@' ) ? ` ${ first } / ${ second } ` : first
}
2026-07-19 21:17:57 +08:00
export default defineConfig ( {
2026-07-28 18:04:21 +08:00
plugins : [ rejectStandaloneServe ( ) , react ( ) ] ,
2026-08-04 14:15:32 +08:00
build : {
sourcemap : true ,
2026-08-06 15:48:10 +08:00
rollupOptions : {
output : {
// Output layout: the two main chunks stay at assets/ root; lazy
// @shikijs/langs grammar chunks group under assets/langs/; fonts
// (today all KaTeX faces referenced by vendor.css) group under
// assets/fonts/. Sourcemaps need no arrangement: rollup writes each
// .map next to its js and references it by bare relative filename.
chunkFileNames ( chunk ) : string {
// Grammar chunks are recognized by their member modules, not the
// facade: shared embedded-grammar chunks (e.g. html+javascript,
// split out because php/ruby/mdx embed them) have no facade at all.
// index and vendor are excluded by name — vendor legitimately
// carries the three boot grammars.
if ( chunk . name === 'index' || chunk . name === 'vendor' ) return 'assets/[name]-[hash].js'
const isLangChunk = chunk . moduleIds . some ( id = > id . includes ( '/node_modules/@shikijs/langs/' ) )
return isLangChunk ? 'assets/langs/[name]-[hash].js' : 'assets/[name]-[hash].js'
} ,
assetFileNames ( asset ) : string {
const fileName = asset . names [ 0 ] ? ? ''
const isFont = FONT_EXTENSIONS . some ( ext = > fileName . endsWith ( ext ) )
return isFont ? 'assets/fonts/[name]-[hash][extname]' : 'assets/[name]-[hash][extname]'
} ,
manualChunks ( id : string ) : string | undefined {
const pkg = npmPackageOf ( id )
if ( pkg === undefined ) return undefined // workspace + vendored cordis: index
if ( pkg === '@shikijs/langs' ) {
return BOOT_GRAMMAR_FILES . some ( file = > id . endsWith ( ` / ${ file } ` ) ) ? 'vendor' : undefined
}
return VENDOR_PACKAGES . has ( pkg ) ? 'vendor' : undefined
} ,
} ,
} ,
2026-08-04 14:15:32 +08:00
} ,
2026-07-19 21:17:57 +08:00
resolve : {
// Workspace packages resolve to SOURCE: package.json exports point at lib
// for Node/type consumers, but the browser bundle must compile src directly
// so CSS rides vite's pipeline instead of the CSS-externalized lib bundle.
2026-07-23 21:55:39 +08:00
// Only the shell's normal-package surface is aliased — plugin packages are
// NEVER bundled here (web2 shell self-sufficiency); they arrive as runtime
// bundles through the client module system. Order matters — subpath
// aliases must win over bare-name prefixes.
2026-07-19 21:17:57 +08:00
alias : [
2026-07-23 21:55:39 +08:00
// Browserization of the vendored cordis Loader: its only node-only
// import; the two process probes are mapped by `define` below.
{ find : /^node:module$/ , replacement : src ( './src/node-module-stub.ts' ) } ,
2026-07-19 21:17:57 +08:00
{ find : /^@deepseek-ai\/dsh-client-web$/ , replacement : src ( '../../packages/client/web/src/boot.tsx' ) } ,
{ find : /^@deepseek-ai\/dsh-client-web-react$/ , replacement : src ( '../../packages/client/web-react/src/index.ts' ) } ,
{ find : /^@deepseek-ai\/dsh-client-ui-slots$/ , replacement : src ( '../../packages/client/ui-slots/src/index.ts' ) } ,
{ find : /^@deepseek-ai\/dsh-client-ui-primitives$/ , replacement : src ( '../../packages/client/ui-primitives/src/index.ts' ) } ,
2026-07-31 11:12:18 +08:00
{ find : /^@deepseek-ai\/dsh-client-schema-form$/ , replacement : src ( '../../packages/client/schema-form/src/index.ts' ) } ,
2026-07-25 01:19:30 +08:00
{ find : /^@deepseek-ai\/dsh-client-modules\/client$/ , replacement : src ( '../../packages/client/modules/src/client/index.ts' ) } ,
2026-07-19 21:17:57 +08:00
] ,
} ,
2026-07-23 21:55:39 +08:00
define : {
// vendored loader internal.ts: fromInternal() probes the Node major —
// "0.0.0" takes neither branch, returning undefined (exactly the empty
// internal slot the shell boot fills with the client module loader).
'process.versions.node' : '"0.0.0"' ,
'process.execArgv' : '[]' ,
// vendored loader index.ts: envData falls to its default branch.
'process.env.CORDIS_SHARED' : 'undefined' ,
} ,
2026-07-19 21:17:57 +08:00
} )