| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624 |
- /**
- * Shared tsdown preset for UI plugin client bundles. Emits a closure-factory
- * artifact: the bundle calls window.__ModuleLoader__.load({id, factory})
- * and resolves externals through the injected require (loader module table —
- * cordis DI entities, no globals, no import map). CSS is compiled by
- * lightningcss inside the bundle: `x.module.css` yields its hashed class map
- * and injects a tagged style at factory execution, while `x.css?inline`
- * exports compiled text for a plugin-owned lifecycle effect. The virtual
- * loaders register each real stylesheet as a watch dependency.
- */
- import { readFile } from 'node:fs/promises'
- import { existsSync, globSync, readFileSync } from 'node:fs'
- import { isBuiltin } from 'node:module'
- import { basename, dirname, isAbsolute, relative, resolve as resolvePath, sep } from 'node:path'
- import { fileURLToPath } from 'node:url'
- import type { UserConfig } from 'tsdown'
- import { transform } from 'lightningcss'
- import { optionalStringArray } from './modules/src/client/manifest.ts'
- import { PLATFORM_MODULES, PRELOADED_CLIENT_EXTERNALS } from './web/src/platform.ts'
- import { clientBuildEnvironmentDefines } from '../../scripts/client-build-environment.ts'
- /**
- * Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
- * (which requires @tsdown/css). The suffix matters: tsdown's guard matches ids
- * ending in `.css`, so the virtual id must not.
- */
- const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
- const GLOBAL_CSS_VIRTUAL_PREFIX = '\0dsh-global-css:'
- const INLINE_CSS_VIRTUAL_PREFIX = '\0dsh-inline-css:'
- const CSS_VIRTUAL_SUFFIX = '.mjs'
- const INLINE_CSS_QUERY = '?inline'
- /** Emit one plugin-owned style injector and an optional CSS Modules export. */
- function styleInjectionModule(
- id: string,
- fileId: string,
- css: string,
- classMap?: Readonly<Record<string, string>>,
- ): string {
- const source = [
- `const css = ${JSON.stringify(css)};`,
- `const tagId = ${JSON.stringify(`${id}/${basename(fileId)}`)};`,
- 'if (typeof document !== \'undefined\' && document.querySelector(\'style[data-plugin-css=\' + JSON.stringify(tagId) + \']\') === null) {',
- ' const tag = document.createElement(\'style\');',
- ` tag.dataset.plugin = ${JSON.stringify(id)};`,
- ' tag.dataset.pluginCss = tagId;',
- ' tag.textContent = css;',
- ' document.head.appendChild(tag);',
- '}',
- ]
- source.push(classMap === undefined ? 'export {};' : `export default ${JSON.stringify(classMap)};`)
- return source.join('\n')
- }
- /**
- * Contract layers and pure folds a client bundle may inline: browser-safe
- * values with no runtime identity to share (no Symbol/instanceof/singleton state).
- * Everything else under @deepseek-ai/* is either a module-table entry
- * (external) or a leak the purity gate rejects.
- */
- export const INLINE_SAFE = /^(?:@deepseek-ai\/dsh-(?:file-reference|session|llm|tools|brand|deque|output-retention|typert-protocol|util-crypto|util-values|util-workspace-path)(?:\/|$)|@deepseek-ai\/dsh-token-meter\/client$|@deepseek-ai\/dsh-host-open-in-app\/shared$|@deepseek-ai\/dsh-agent-presets\/display$|@deepseek-ai\/dsh-spill-policy\/notice$)/
- /**
- * Vendored framework libraries: rescoped into @deepseek-ai, so the gate below
- * would read them as plugin packages. They carry no cross-plugin runtime
- * identity to share — the framework itself is a requested module-table row
- * (external), while these are ordinary libraries a browser bundle inlines.
- */
- const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/
- /** Generated descriptor/codec contribution with no shared runtime identity. */
- const GENERATED_REMOTE = /^@deepseek-ai\/dsh-[a-z0-9]+(?:-[a-z0-9]+)*\/remote$/
- /**
- * Workspace mode replaces an empty config array with the root defaults. A
- * falsey entry instead removes this package before entry resolution.
- */
- const SKIP_WORKSPACE_BUILD: UserConfig = { entry: '' }
- const REPOSITORY_ROOT = fileURLToPath(new URL('../..', import.meta.url))
- /** Rebase a physical lib-relative source onto a browser URL that mirrors the repository directories. */
- function browserSourcePath(source: string, sourcemapPath: string): string {
- if (!source.startsWith('.')) return source
- const physicalSource = resolvePath(dirname(sourcemapPath), source)
- const repositoryPath = relative(REPOSITORY_ROOT, physicalSource).split(sep).join('/')
- return repositoryPath.startsWith('packages/') ? `../../../${repositoryPath}` : source
- }
- /**
- * Build the tsdown config for one UI plugin package: the node-half lib build
- * plus the browser client bundle. Client packages emit both halves during the
- * Client pass by default; packages needed for Host reflection may opt into the
- * earlier Host pass. A package-level tsdown.config.ts REPLACES the root
- * workspace layout, so the lib half must be restated here — dropping it leaves
- * the package without lib/index.js and the host Loader cannot import its node
- * half. The Client build consumes `lib/types` and chains those tsc maps, with
- * original source content, into the standalone plugin map.
- * @param id - plugin id (package name), stamped into the __ModuleLoader__.load
- * handoff and onto the injected style tags.
- * @param libEntry - node-half entries, spelled at the call site so the
- * package-invariants gate can see `lib/types/invariant.js` in each package's
- * own tsdown.config.ts (a preset-side glob hides it from the mechanical check).
- * @param options - phase placement, lib overrides, and companion Node configs.
- * @returns ENV-selected tsdown config for the current build face.
- */
- export function clientBundle(
- id: string,
- libEntry: readonly string[],
- options: ClientBundleOptions = {},
- ): BuildFaceConfig {
- const lib = clientLibraryConfig(id, libEntry, options.lib)
- return ({ env }) => {
- const face = buildFace(env?.DSH_BUILD_FACE)
- const clientEntry = face === undefined ? 'src/client/index.ts' : 'lib/types/client/index.js'
- const client = clientConfig(id, clientEntry)
- const node = [lib, ...(options.companions ?? [])]
- if (face === 'host') return options.hostPhase === true ? node : [SKIP_WORKSPACE_BUILD]
- if (face === 'client') {
- return options.hostPhase === true ? [client] : [...node, client]
- }
- return [...node, client]
- }
- }
- /**
- * Build the tsdown config for a client library the compile shell links
- * statically (the static assembly channel: `apps/web` resolves the package
- * name, bundles the artifact, and owns the chunk layout and the CSS pipeline).
- *
- * Calling this preset is what puts a package in the static assembly channel,
- * so the call sites are the roster: gates read it through
- * {@link isStaticLinkedConfig} rather than a second hand-kept list. A package on
- * this roster must not be a module-table row as well — the browser would take
- * the statically linked copy and a provider's bytes would sit unused in its
- * bundle.
- *
- * Four artifact contracts:
- * 1. every bare specifier stays an import. The shell attributes chunk bytes by
- * `node_modules/<pkg>`, so a dependency inlined into a workspace file is
- * attributed to no npm package and its bytes fall into the index chunk,
- * which collapses the vendor/index cache split.
- * 2. `esm` on `platform: 'browser'` — the shell is the only consumer.
- * 3. sourcemaps, chained through the tsc maps under `lib/types` to the sources.
- * 4. stylesheets ship with the package: a relative `.css` import survives as a
- * relative external and the sheet is emitted under `lib/` at its
- * `src`-relative path, so vite stays the only owner of class hashing.
- * @param id - package name, used in tsdown diagnostics.
- * @param libEntry - emitted JavaScript entries consumed from `lib/types`, one
- * bundle each: a multi-entry build would emit a hash-named shared chunk that
- * the exact `files` list cannot publish.
- * @returns ENV-selected tsdown config for the Client build face.
- */
- export function staticLinked(id: string, libEntry: readonly string[]): BuildFaceConfig {
- // Each entry names its own output file, so two entries with the same basename
- // would overwrite one artifact instead of emitting two.
- const names = new Set(libEntry.map(entry => basename(entry, '.js')))
- if (names.size !== libEntry.length) {
- throw new Error(`tsdown: ${id} entries collide on an output name: ${libEntry.join(', ')}`)
- }
- return clientOnly(libEntry.map(entry => staticLinkedConfig(id, entry)))
- }
- /**
- * Whether a package's tsdown configs put it in the static assembly channel.
- * The roster has no separate list: gates load each package's own
- * `tsdown.config.ts`, call it for the Client face, and ask this.
- * @param configs - configs a package's build-face function returned.
- * @returns true when at least one config was built by {@link staticLinked}.
- */
- export function isStaticLinkedConfig(configs: readonly UserConfig[]): boolean {
- return configs.some(config => (config.plugins as readonly { name?: string }[] | undefined ?? [])
- .some(plugin => plugin.name === STATIC_LINKED_PLUGIN))
- }
- /**
- * Build a Client-only Node library during the Client pass.
- * @param id - Package name used in tsdown diagnostics.
- * @param libEntry - Emitted JavaScript entries consumed from `lib/types`.
- * @returns ENV-selected tsdown config for the Client build face.
- */
- export function clientLibrary(id: string, libEntry: readonly string[]): BuildFaceConfig {
- const lib = clientLibraryConfig(id, libEntry)
- return clientOnly([lib])
- }
- /**
- * Select arbitrary package-local configs only during the Client pass.
- * @param configs - Node-side configs emitted after Client tsc.
- * @returns ENV-selected tsdown config for the Client build face.
- */
- export function clientOnly(configs: readonly UserConfig[]): BuildFaceConfig {
- return ({ env }) => buildFace(env?.DSH_BUILD_FACE) === 'host'
- ? [SKIP_WORKSPACE_BUILD]
- : [...configs]
- }
- interface ClientBundleOptions {
- /** Emit the Node-side artifacts during the Host pass instead of the Client pass. */
- readonly hostPhase?: boolean
- /** Additional Node-side configs emitted alongside the package library. */
- readonly companions?: readonly UserConfig[]
- /** Overrides for the package's primary Node-side library config. */
- readonly lib?: UserConfig
- }
- type BuildFace = 'host' | 'client' | undefined
- type BuildFaceConfig = (inlineConfig: Pick<UserConfig, 'env'>) => UserConfig[]
- function buildFace(value: unknown): BuildFace {
- if (value === undefined || value === 'host' || value === 'client') return value
- throw new Error(`tsdown: --env.DSH_BUILD_FACE must be host or client, received ${String(value)}`)
- }
- function clientLibraryConfig(
- id: string,
- libEntry: readonly string[],
- overrides: UserConfig = {},
- ): UserConfig {
- const isProductionDependency = (specifier: string): boolean =>
- matchesSpecifier(productionExternals(id), specifier)
- return {
- name: id,
- entry: [...libEntry],
- outDir: 'lib',
- format: ['esm'],
- platform: 'node',
- target: 'es2024',
- fixedExtension: false,
- dts: false,
- clean: false,
- deps: {
- // The Node half runs from a real install: a production dependency is on
- // disk there and stays an import, everything else inlines. Stating both
- // halves takes the artifact off tsdown's getProductionDeps fallback, where
- // moving a dependency between npm sections silently re-bundles it.
- // Builtins keep tsdown's own handling (neither side claims them).
- neverBundle: isProductionDependency,
- alwaysBundle: (specifier: string) => !isBuiltin(specifier) && !isProductionDependency(specifier),
- },
- ...overrides,
- }
- }
- /** The slice of the rolldown plugin context the stylesheet plugin uses. */
- interface AssetEmitter {
- emitFile(file: {
- type: 'asset'
- fileName: string
- source: Uint8Array
- originalFileName: string
- }): string
- }
- function staticLinkedConfig(id: string, entry: string, outputName = basename(entry, '.js')): UserConfig {
- const emitted = new Set<string>()
- return {
- name: id,
- entry: { [outputName]: entry },
- outDir: 'lib',
- format: ['esm'],
- platform: 'browser',
- target: 'es2024',
- fixedExtension: false,
- dts: false,
- clean: false,
- // The shell compiles this artifact, so its map is the only path from a
- // browser stack frame back to the TSX (tsc emits the lib/types half).
- sourcemap: true,
- outputOptions: { sourcemapExcludeSources: false },
- plugins: [{
- // Contract 1. `pre` because tsdown's own deps plugin would otherwise
- // resolve and inline every specifier missing from the npm production
- // sections, which is the coupling this preset exists to remove. The name
- // is also the roster marker {@link isStaticLinkedConfig} reads.
- name: STATIC_LINKED_PLUGIN,
- resolveId: {
- order: 'pre' as const,
- handler(source: string, importer: string | undefined) {
- // An entry arrives without an importer and must stay internal.
- if (importer === undefined) return null
- return isBareSpecifier(source) ? { id: source, external: true } : null
- },
- },
- }, tscSourceMapPlugin(), {
- // Contract 4. The import survives verbatim and the sheet lands beside the
- // JavaScript, so the shell's CSS Modules pipeline sees a real stylesheet.
- name: 'dsh-css-asset',
- async resolveId(this: AssetEmitter, source: string, importer: string | undefined) {
- if (!source.endsWith('.css') || importer === undefined) return null
- const { file, fileName } = stylesheetAsset(source, importer)
- if (!emitted.has(fileName)) {
- emitted.add(fileName)
- // originalFileName also puts the physical sheet in the watch graph.
- this.emitFile({ type: 'asset', fileName, source: await readFile(file), originalFileName: file })
- }
- // Every emitted chunk sits at the lib/ root, so the src-relative name
- // is what resolves from there. Rolldown keeps relative externals as
- // written instead of re-normalizing them.
- return { id: `./${fileName}`, external: true }
- },
- }],
- }
- }
- /** Whether a specifier names a package rather than a file next to its importer. */
- function isBareSpecifier(specifier: string): boolean {
- return !specifier.startsWith('.') && !specifier.startsWith('\0') && !isAbsolute(specifier)
- }
- /**
- * Locate a stylesheet import against the package sources and name its emitted position.
- * @param source - relative import specifier as written in the source.
- * @param importer - absolute path of the importing module, emitted or source.
- * @returns the stylesheet on disk plus its `src`-relative name under `lib/`.
- */
- function stylesheetAsset(source: string, importer: string): { readonly file: string, readonly fileName: string } {
- const file = sourceAssetPath(source, importer)
- const boundary = file.lastIndexOf(SOURCE_MARKER)
- if (boundary < 0) throw new Error(`tsdown: stylesheet ${file} is outside the package sources`)
- return { file, fileName: file.slice(boundary + SOURCE_MARKER.length).split(sep).join('/') }
- }
- /** The manifest fields the build faces read to state their own module edges. */
- interface WorkspaceManifest {
- readonly name?: string
- /** Sections a real install materializes on disk next to the built package. */
- readonly dependencies?: Record<string, string>
- readonly peerDependencies?: Record<string, string>
- readonly optionalDependencies?: Record<string, string>
- readonly dsh?: { readonly client?: { readonly external?: unknown } }
- }
- const manifestCache = new Map<string, WorkspaceManifest>()
- const productionExternalCache = new Map<string, readonly RegExp[]>()
- const clientExternalCache = new Map<string, ReadonlySet<string>>()
- /**
- * Read one workspace package's manifest. Located by package name rather than by
- * cwd, because tsdown evaluates every package config with the repository root as
- * `process.cwd()` during a workspace build. Callers read it on the first
- * resolveId of a build, not while a config is built, so selecting a build face
- * never touches a manifest.
- * @param id - package name, as spelled at the preset call site.
- * @returns the parsed manifest.
- * @throws {Error} when no workspace package declares that name.
- */
- function workspaceManifest(id: string): WorkspaceManifest {
- const cached = manifestCache.get(id)
- if (cached !== undefined) return cached
- for (const manifestPath of globSync('packages/*/*/package.json', { cwd: REPOSITORY_ROOT })) {
- const manifest = JSON.parse(
- readFileSync(resolvePath(REPOSITORY_ROOT, manifestPath), 'utf8'),
- ) as WorkspaceManifest
- if (manifest.name !== id) continue
- manifestCache.set(id, manifest)
- return manifest
- }
- throw new Error(`tsdown: no packages/*/*/package.json declares the name ${id}`)
- }
- /**
- * External patterns for one package's Node half: its own production sections,
- * subpaths included.
- * @param id - package name, as spelled at the preset call site.
- * @returns one `^name(/|$)` pattern per production dependency, name-sorted.
- */
- function productionExternals(id: string): readonly RegExp[] {
- const cached = productionExternalCache.get(id)
- if (cached !== undefined) return cached
- const manifest = workspaceManifest(id)
- const names = new Set([
- ...Object.keys(manifest.dependencies ?? {}),
- ...Object.keys(manifest.peerDependencies ?? {}),
- ...Object.keys(manifest.optionalDependencies ?? {}),
- ])
- const patterns = [...names].sort().map(name => new RegExp(`^${escapeSpecifier(name)}(/|$)`))
- productionExternalCache.set(id, patterns)
- return patterns
- }
- /**
- * Module-table specifiers one `dsh.client` declaration requests. Matching is
- * exact, never normalized: a package declares the specifier its own code
- * imports, and the loader keys static entries the same way.
- * @param subject - package name, used in diagnostics.
- * @param declaration - the package's `dsh.client` object.
- * @returns the requested specifiers, empty when the package declares none.
- * @throws {Error} when `external` is not a string array.
- */
- export function requestedExternals(
- subject: string,
- declaration: { readonly external?: unknown },
- ): ReadonlySet<string> {
- return new Set(optionalStringArray(subject, 'dsh.client.external', declaration.external) ?? [])
- }
- /**
- * Module-table specifiers one package requests. The shell baseline is implicit
- * for every dynamic bundle; `dsh.client.external` only adds package-specific
- * dynamic rows or subpaths.
- * @param id - package name, as spelled at the preset call site.
- * @returns the baseline plus the package's explicit requests.
- */
- function clientExternals(id: string): ReadonlySet<string> {
- const cached = clientExternalCache.get(id)
- if (cached !== undefined) return cached
- const externals = new Set([
- ...PLATFORM_MODULES,
- ...PRELOADED_CLIENT_EXTERNALS,
- ...requestedExternals(id, workspaceManifest(id).dsh?.client ?? {}),
- ])
- clientExternalCache.set(id, externals)
- return externals
- }
- /** Escape a package name for literal use inside a RegExp source. */
- function escapeSpecifier(name: string): string {
- return name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
- }
- /** Whether an import specifier is the package a pattern names, or one of its subpaths. */
- function matchesSpecifier(patterns: readonly RegExp[], specifier: string): boolean {
- return patterns.some(pattern => pattern.test(specifier))
- }
- function clientConfig(id: string, entry: string): UserConfig {
- const isRequested = (specifier: string): boolean => clientExternals(id).has(specifier)
- return {
- name: `${id}/client`,
- entry: { client: entry },
- // Browser bundle lands next to the node half (single lib/ artifact dir;
- // the entryFileNames pin keeps it exactly lib/client.js). clean must stay
- // off — a default clean would wipe the node-half output emitted above.
- outDir: 'lib',
- format: 'cjs',
- platform: 'browser',
- // Types ship from lib/types (tsc); dts here would wrap the banner/footer into .d.cts and break parsing.
- dts: false,
- // Plugin code is fetched outside Vite's module graph, so its own bundle
- // must carry the TS/TSX mapping consumed by browser profiling tools.
- sourcemap: true,
- clean: false,
- deps: {
- neverBundle: isRequested,
- // Anything NOT requested from the loader module table must inline
- // (wire/type layers, zod, clsx — every non-shared dep). A require() the
- // table cannot answer is a guaranteed runtime throw, so the rule is the
- // package's own request list: requested specifiers stay imports,
- // everything else is bundled.
- alwaysBundle: (specifier: string) => !isRequested(specifier),
- },
- // Dual-mode libraries (lexical's exports carry development/production/
- // node conditions; the node file picks its flavor with a top-level await
- // a CJS bundle cannot carry) resolve their static flavor matching the
- // NODE_ENV the defines below bake in.
- inputOptions: {
- resolve: {
- conditionNames: [
- (process.env.NODE_ENV ?? 'production') === 'development' ? 'development' : 'production',
- 'browser', 'import', 'module', 'default',
- ],
- },
- },
- // Browser bundles inline node-idiom deps (zustand/immer read
- // process.env.NODE_ENV; zustand's esm build also probes
- // import.meta.env.MODE, which a CJS output cannot carry — rolldown flags
- // EMPTY_IMPORT_META). vite defined both on the seed path; tsdown inlining
- // needs the substitutions here or the factory throws ReferenceError at
- // boot / the build gate reds. Both keys honor the build's NODE_ENV so a
- // dev build keeps the dev-branch semantics; artifacts default to production.
- // The bare `import.meta.env` key is required alongside the precise MODE
- // key: zustand probes `import.meta.env ? import.meta.env.MODE : ...`, and
- // the truthiness probe would otherwise survive as an empty import.meta.
- define: {
- ...clientBuildEnvironmentDefines(process.env),
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
- 'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV ?? 'production'),
- 'import.meta.env': JSON.stringify({ MODE: process.env.NODE_ENV ?? 'production' }),
- },
- plugins: [{
- // Bundle purity gate (build-time mirror of the module-edge rules): the
- // baseline and package-specific requests stay external, inline-safe wire layers
- // inline, and every other @deepseek-ai value import is a build error — a
- // cross-plugin value import either inlines a duplicate runtime instance
- // or requires a specifier the module table cannot answer for this package.
- // Cross-plugin collaboration goes through cordis services instead.
- name: 'dsh-client-bundle-purity',
- resolveId(source: string) {
- if (!source.startsWith('@deepseek-ai/')) return null
- if (isRequested(source)) return null // requested module-table row: external wins
- if (VENDORED_LIBRARY.test(source)) return null // vendored library: inline, no shared identity
- if (INLINE_SAFE.test(source) || GENERATED_REMOTE.test(source)) return null // wire contribution: inline is the point
- throw new Error(
- `client bundle purity: "${source}" is not in the default client externals or ${id}'s dsh.client.external, an inline-safe wire layer, or a generated /remote contribution — `
- + 'cross-plugin value imports are forbidden; declare a non-default module request or collaborate through cordis services '
- + '(type-only imports are erased and never reach this gate)',
- )
- },
- }, tscSourceMapPlugin(), {
- name: 'dsh-css-modules-inline',
- resolveId(source: string, importer: string | undefined) {
- if (!source.endsWith('.module.css')) return null
- const abs = importer !== undefined ? sourceAssetPath(source, importer) : source
- return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
- },
- async load(virtualId: string) {
- if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null
- const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
- // The virtual id otherwise hides the physical stylesheet from Rolldown's watch graph.
- this.addWatchFile(fileId)
- const source = await readFile(fileId)
- const { code, exports: cssExports } = transform({
- filename: fileId,
- code: source,
- cssModules: { pattern: '[hash]_[local]' },
- minify: true,
- })
- const classMap: Record<string, string> = {}
- const exportEntries = Object.entries(cssExports ?? {})
- .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
- for (const [local, exp] of exportEntries) classMap[local] = exp.name
- return styleInjectionModule(id, fileId, code.toString(), classMap)
- },
- }, {
- name: 'dsh-css-text-inline',
- resolveId(source: string, importer: string | undefined) {
- if (!source.endsWith(`.css${INLINE_CSS_QUERY}`)) return null
- const stylesheet = source.slice(0, -INLINE_CSS_QUERY.length)
- const abs = importer !== undefined ? sourceAssetPath(stylesheet, importer) : stylesheet
- return INLINE_CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
- },
- async load(virtualId: string) {
- if (!virtualId.startsWith(INLINE_CSS_VIRTUAL_PREFIX)) return null
- const fileId = virtualId.slice(INLINE_CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
- this.addWatchFile(fileId)
- const source = await readFile(fileId)
- const { code } = transform({ filename: fileId, code: source, minify: true })
- return `export default ${JSON.stringify(code.toString())};`
- },
- }, {
- name: 'dsh-css-global-inline',
- resolveId(source: string, importer: string | undefined) {
- if (!source.endsWith('.css') || source.endsWith('.module.css')) return null
- const abs = importer !== undefined ? sourceAssetPath(source, importer) : source
- return GLOBAL_CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
- },
- async load(virtualId: string) {
- if (!virtualId.startsWith(GLOBAL_CSS_VIRTUAL_PREFIX)) return null
- const fileId = virtualId.slice(GLOBAL_CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
- this.addWatchFile(fileId)
- const source = await readFile(fileId)
- const { code } = transform({ filename: fileId, code: source, minify: true })
- return styleInjectionModule(id, fileId, code.toString())
- },
- }],
- outputOptions: {
- entryFileNames: 'client.js',
- sourcemapExcludeSources: false,
- // The map is served from /plugins/<scoped-package>/client.js.map. The
- // browser resolves its local sources back into URLs that mirror the
- // /packages/<group>/<package>/src directories; sourcesContent keeps them usable
- // without exposing that tree as an HTTP route.
- sourcemapPathTransform: browserSourcePath,
- banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
- footer: 'return module.exports; } });',
- intro: 'var module = { exports: {} }; var exports = module.exports;',
- },
- }
- }
- /** Chain tsc's emitted maps into any Client bundle that consumes `lib/types`. */
- function tscSourceMapPlugin() {
- return {
- name: 'dsh-tsc-sourcemap',
- async load(id: string) {
- if (!id.includes(TYPES_MARKER) || !id.endsWith('.js') || !existsSync(`${id}.map`)) return null
- const code = await readFile(id, 'utf8')
- const mapPath = `${id}.map`
- const map = JSON.parse(await readFile(mapPath, 'utf8')) as {
- sourceRoot?: unknown
- sources?: unknown
- sourcesContent?: unknown
- [key: string]: unknown
- }
- if (!Array.isArray(map.sources) || map.sources.some(source => typeof source !== 'string')) {
- throw new Error(`client sourcemap: ${mapPath} has invalid sources`)
- }
- const sources = map.sources as string[]
- if (
- !Array.isArray(map.sourcesContent)
- || map.sourcesContent.length !== sources.length
- || map.sourcesContent.some(source => typeof source !== 'string')
- ) {
- const sourceRoot = typeof map.sourceRoot === 'string' ? map.sourceRoot : ''
- map.sourcesContent = await Promise.all(sources.map(async source =>
- await readFile(resolvePath(dirname(mapPath), sourceRoot, source), 'utf8')))
- }
- return { code: code.replace(SOURCEMAP_COMMENT, ''), map }
- },
- }
- }
- /** Path segment separating a package's tsc output from the sources it was emitted from. */
- const TYPES_MARKER = `${sep}lib${sep}types${sep}`
- /** Plugin name carrying contract 1, and the marker that identifies a statically linked config. */
- const STATIC_LINKED_PLUGIN = 'dsh-static-linked-external'
- /** Path segment a package's sources hang under, and the root emitted assets mirror. */
- const SOURCE_MARKER = `${sep}src${sep}`
- /** Trailing sourcemap reference tsc appends to every emitted module. */
- const SOURCEMAP_COMMENT = /\n\/\/# sourceMappingURL=.*\s*$/
- /** Resolve an emitted JS asset import against its source-tree counterpart. */
- function sourceAssetPath(source: string, importer: string): string {
- const emitted = resolvePath(dirname(importer), source)
- if (existsSync(emitted)) return emitted
- const boundary = emitted.indexOf(TYPES_MARKER)
- if (boundary < 0) return emitted
- return resolvePath(emitted.slice(0, boundary), 'src', emitted.slice(boundary + TYPES_MARKER.length))
- }
|