mirror of
https://github.com/dsh-market/dsh-market.git
synced 2026-09-28 05:03:07 +08:00
@liuwenji007 proposed a bot that rebuilds client/ on PR branches, because every front-end PR conflicts there after another one lands. The problem is real — they were hitting it on three stacked PRs at once — but the diagnosis narrows further than the proposal assumed. Measured on the two committed artifacts: client/client.js 11,201 lines, unminified, real identifiers client/client.js.map 0 newlines — one 789KB line Line-based merges handle the first and cannot handle the second. That matches what the queue actually did: #511 rebased with zero conflicts, and #518's only conflict is the map. The bundle is not the problem; the map is, and it always will be, because a single-line file has no merge granularity at all. So: no sourcemap for the client bundle. The reasoning is about this repository, not the browser — the map was bought for debugging, and the bundle it maps is already readable, so a stack trace against it names the real functions either way. Not worth a permanent tax on everyone who touches the client. It also rode along in the published package, where nothing consumed it: 1.0 MB → 809 kB packed, 3.9 MB → 3.1 MB unpacked. What stays: the bundle is still committed (the market must install where build scripts are blocked), CI still enforces that it matches the source, and the pipeline still preserves line counts — that property serves determinism as well as the map that is now gone. The bot idea is declined separately on the issue; the short version is that pushing to fork branches needs `pull_request_target`, which runs privileged in the base repo's context against untrusted code.
128 lines
6.2 KiB
TypeScript
128 lines
6.2 KiB
TypeScript
/**
|
|
* Browser client bundle for the dshmarket plugin, mirroring the DeepSeek
|
|
* Harness client preset (packages/client/tsdown.client.ts) for an external
|
|
* package: a closure-factory artifact that calls
|
|
* window.__ModuleLoader__.load({ id, factory }) and resolves externals
|
|
* through the injected require (loader module table). CSS Modules compile via
|
|
* lightningcss inside the bundle: importing `x.module.css` yields the hashed
|
|
* class map, and the css text auto-injects a <style data-plugin> tag at
|
|
* factory execution (the loader removes plugin-owned tags on unload).
|
|
*
|
|
* scripts/preflight.mjs asserts the emitted client/client.js starts with the
|
|
* exact `window.__ModuleLoader__.load({ id: "dshmarket"` prefix.
|
|
*/
|
|
import { readFile } from 'node:fs/promises'
|
|
import { basename, dirname, relative, resolve as resolvePath } from 'node:path'
|
|
import { defineConfig } from 'tsdown'
|
|
import { transform } from 'lightningcss'
|
|
|
|
const id = 'dshmarket'
|
|
|
|
/**
|
|
* Externals resolved from the loader module table at runtime. Only the
|
|
* platform seed entries this bundle actually requires; everything else
|
|
* (nothing today) inlines.
|
|
*/
|
|
const CLIENT_EXTERNALS = ['react', 'react/jsx-runtime', 'react-dom', '@deepseek-ai/dsh-client-ui-primitives']
|
|
|
|
/**
|
|
* 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 CSS_VIRTUAL_SUFFIX = '.mjs'
|
|
|
|
export default defineConfig({
|
|
entry: { client: 'src/client/index.ts' },
|
|
// The published artifact location: package.json exports "./client" points
|
|
// at client/client.js, so the bundle lands there directly.
|
|
outDir: 'client',
|
|
format: 'cjs',
|
|
platform: 'browser',
|
|
target: 'es2022',
|
|
// Host types ship from lib/types (tsc); dts here would wrap the
|
|
// banner/footer into .d.cts and break parsing.
|
|
dts: false,
|
|
// No sourcemap, and the reason is the repository rather than the browser.
|
|
//
|
|
// `client.js` is committed (the market must install where build scripts are
|
|
// blocked) and CI enforces that it matches the source. That is fine for the
|
|
// bundle itself: 11k unminified lines with real identifiers, which git
|
|
// merges line by line like any other file — measured across a run of
|
|
// front-end PRs, it did not conflict once.
|
|
//
|
|
// The map is one 789KB line. Every change to it is a whole-file conflict,
|
|
// for every contributor, every time another front-end PR lands first
|
|
// (#533 by @liuwenji007, who was hitting it on three stacked PRs). It also
|
|
// rode along in the published package, where nothing consumed it.
|
|
//
|
|
// The debugging it bought was small, because the bundle it maps is already
|
|
// readable — a stack trace against it names the real functions. Not worth a
|
|
// permanent tax on everyone who touches the client.
|
|
sourcemap: false,
|
|
clean: false,
|
|
external: [...CLIENT_EXTERNALS],
|
|
// tsdown auto-externalizes package dependencies; anything NOT in the loader
|
|
// module table must inline instead — a require() the table cannot answer is
|
|
// a guaranteed runtime throw.
|
|
noExternal: (source: string) => (CLIENT_EXTERNALS.includes(source) ? undefined : true),
|
|
define: {
|
|
'process.env.NODE_ENV': JSON.stringify('production'),
|
|
'import.meta.env.MODE': JSON.stringify('production'),
|
|
'import.meta.env': JSON.stringify({ MODE: 'production' }),
|
|
},
|
|
plugins: [{
|
|
name: 'dsh-css-modules-inline',
|
|
resolveId(source: string, importer: string | undefined) {
|
|
if (!source.endsWith('.module.css')) return null
|
|
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source
|
|
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
|
},
|
|
async load(this: { addWatchFile(file: string): void }, 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)
|
|
// The filename feeds lightningcss's `[hash]`. Handing it the absolute
|
|
// path made the class prefix a fingerprint of the checkout location:
|
|
// the same commit built `.SOz1_a_root` on one machine and
|
|
// `.eGUBIq_root` on the CI runner (#472's second half, measured by the
|
|
// reproducibility guard this repo now runs). Repo-relative with posix
|
|
// separators is the same input everywhere, including Windows.
|
|
const { code, exports: cssExports } = transform({
|
|
filename: relative(process.cwd(), fileId).split('\\').join('/'),
|
|
code: source,
|
|
cssModules: { pattern: '[hash]_[local]' },
|
|
minify: true,
|
|
// Without targets, lightningcss collapses the hand-written
|
|
// `backdrop-filter` + `-webkit-backdrop-filter` pair to the -webkit-
|
|
// form only, losing Firefox. Targets (major << 16) keep both.
|
|
targets: { chrome: 90 << 16, firefox: 100 << 16, safari: 13 << 16, edge: 90 << 16 },
|
|
})
|
|
const classMap: Record<string, string> = {}
|
|
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name
|
|
// One <style data-plugin> per module file; idempotent under re-evaluation.
|
|
return [
|
|
`const css = ${JSON.stringify(code.toString())};`,
|
|
`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);',
|
|
'}',
|
|
`export default ${JSON.stringify(classMap)};`,
|
|
].join('\n')
|
|
},
|
|
}],
|
|
outputOptions: {
|
|
entryFileNames: 'client.js',
|
|
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(id)}, factory: (require) => {`,
|
|
footer: 'return module.exports; } });',
|
|
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
|
},
|
|
})
|