dsh-llm-fallbacks 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -0
- package/README.zh-CN.md +119 -0
- package/bundle/cordis.patch.yml +19 -0
- package/dist/chains.d.ts +167 -0
- package/dist/client/ConversationFallbackSwitch.d.ts +87 -0
- package/dist/client/FallbacksCard.d.ts +86 -0
- package/dist/client/GeneralFallbacksRow.d.ts +19 -0
- package/dist/client/fallbacks-store.d.ts +379 -0
- package/dist/client/index.d.ts +56 -0
- package/dist/client/index.js +3916 -0
- package/dist/client/locales.d.ts +281 -0
- package/dist/client/switch-guard.d.ts +17 -0
- package/dist/commands.d.ts +176 -0
- package/dist/config.d.ts +136 -0
- package/dist/cooldown.d.ts +48 -0
- package/dist/events.d.ts +36 -0
- package/dist/gateway.d.ts +176 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +1853 -0
- package/dist/roles.d.ts +70 -0
- package/dist/selectors.d.ts +35 -0
- package/dist/state.d.ts +112 -0
- package/package.json +91 -0
- package/scripts/build-client.ts +374 -0
- package/scripts/verify-dist.mjs +68 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client bundle build: emits the closure-factory CJS artifact the dsh web
|
|
3
|
+
* loader consumes — `window.__ModuleLoader__.load({ id: 'dsh-llm-fallbacks',
|
|
4
|
+
* factory: (require) => { … return module.exports; } })`. Externals resolve
|
|
5
|
+
* through the loader module table (platform seed entries + the documented
|
|
6
|
+
* `@deepseek-ai/dsh-client-runtime/client` exemption); everything else
|
|
7
|
+
* inlines.
|
|
8
|
+
*
|
|
9
|
+
* Build tool: `tsdown` (rolldown) — the same tool the dsh host itself uses
|
|
10
|
+
* for its client bundles (packages/client/tsdown.client.ts pattern), run via
|
|
11
|
+
* `tsx`. Replaces the previous `bun build` recipe; no bun anywhere in the
|
|
12
|
+
* pnpm stack. `dist/client/index.js` is a build artifact (gitignored like the
|
|
13
|
+
* rest of `dist/`); the matching `dist/client/index.d.ts` is emitted by `tsc`
|
|
14
|
+
* (`emitDeclarationOnly`) from `src/client/index.ts`.
|
|
15
|
+
*
|
|
16
|
+
* One build plugin enforces the bundle contract (mirroring the dsh tsdown
|
|
17
|
+
* client recipe):
|
|
18
|
+
* - CSS Modules (`*.module.css`) are compiled to a hashed class map plus an
|
|
19
|
+
* inline `<style data-plugin>` injection that runs when the factory
|
|
20
|
+
* materializes (the loader removes plugin-owned tags on unload). The
|
|
21
|
+
* transform itself (FNV-1a `_<8hex>_<local>` hash, comment-stripped css)
|
|
22
|
+
* mirrors the host bundle's `_1zfRHq_section` shape — the `_` prefix keeps
|
|
23
|
+
* every hashed class a legal CSS identifier (a bare `8hex_local` starts
|
|
24
|
+
* with a digit and the browser silently drops those rules) — and the
|
|
25
|
+
* bundle-contract assertions at the bottom pin that invariant. Only the
|
|
26
|
+
* plugin API moved from bun's `onLoad` to rolldown's `resolveId`/`load`
|
|
27
|
+
* over a virtual id (the virtual id must not end in `.css` so tsdown's own
|
|
28
|
+
* css pipeline never sees it).
|
|
29
|
+
*
|
|
30
|
+
* The `@deepseek-ai/*` purity gate (mirror of the host's
|
|
31
|
+
* `dsh-client-bundle-purity` plugin) is enforced at resolution time: any
|
|
32
|
+
* value import of an `@deepseek-ai/*` module outside CLIENT_EXTERNALS
|
|
33
|
+
* throws, so the 20260812 type-only peers (`dsh-client-ui-settings-plugins`,
|
|
34
|
+
* `dsh-client-locale`, `dsh-api-remotes`, …) can never leak into the
|
|
35
|
+
* browser-loadable artifact. The emitted text may reference only
|
|
36
|
+
* CLIENT_EXTERNALS entries (asserted below).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { build } from 'tsdown'
|
|
40
|
+
import { basename, dirname, join, resolve } from 'node:path'
|
|
41
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
42
|
+
|
|
43
|
+
const ID = 'dsh-llm-fallbacks'
|
|
44
|
+
const ENTRY = 'src/client/index.ts'
|
|
45
|
+
const OUT_DIR = 'dist/client'
|
|
46
|
+
const OUT_FILE = 'index.js'
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Frozen loader module table (dsh mechanism-guide): the plan-documented
|
|
50
|
+
* composition = PLATFORM_MODULES (10 entries, `web/src/platform.ts:8-15`)
|
|
51
|
+
* + the RUNTIME_STORE_EXEMPTION (`@deepseek-ai/dsh-client-runtime/client`,
|
|
52
|
+
* `tsdown.client.ts:62-65`). `@deepseek-ai/cordis` is the in-box cordis
|
|
53
|
+
* framework (dsh-advisor alignment): the client graph resolves it through
|
|
54
|
+
* the generated shim, so it stays on the table as a real loader-table
|
|
55
|
+
* specifier for the frozen-composition claim.
|
|
56
|
+
*/
|
|
57
|
+
export const CLIENT_EXTERNALS: readonly string[] = [
|
|
58
|
+
'react',
|
|
59
|
+
'react/jsx-runtime',
|
|
60
|
+
'react-dom',
|
|
61
|
+
'react-dom/client',
|
|
62
|
+
'@deepseek-ai/cordis',
|
|
63
|
+
'@deepseek-ai/dsh-client-ui-slots',
|
|
64
|
+
'@deepseek-ai/dsh-client-web-react',
|
|
65
|
+
'@deepseek-ai/dsh-client-ui-primitives',
|
|
66
|
+
'@deepseek-ai/dsh-client-ui-attachment',
|
|
67
|
+
'@deepseek-ai/dsh-client-schema-form',
|
|
68
|
+
'@deepseek-ai/dsh-client-runtime/client',
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Deterministic CSS-module class hash: FNV-1a 32-bit over the local name →
|
|
73
|
+
* `_<8hex>_<local>`. The `_` prefix is load-bearing: an 8-hex FNV-1a hash
|
|
74
|
+
* starts with a digit ~62.5% of the time, and `.8b697c55_fieldRow` is not a
|
|
75
|
+
* legal CSS identifier — the browser silently drops the whole rule. The
|
|
76
|
+
* underscore form mirrors the host bundle's `_1zfRHq_section` shape (the dsh
|
|
77
|
+
* recipe's `[hash]_[local]` pattern). The hash only needs to be stable within
|
|
78
|
+
* the bundle — the class map and the rewritten css text come from the same
|
|
79
|
+
* transform.
|
|
80
|
+
*/
|
|
81
|
+
function hashClass(local: string): string {
|
|
82
|
+
let h = 0x811c9dc5
|
|
83
|
+
for (let i = 0; i < local.length; i++) {
|
|
84
|
+
h ^= local.charCodeAt(i)
|
|
85
|
+
h = Math.imul(h, 0x01000193) >>> 0
|
|
86
|
+
}
|
|
87
|
+
return `_${h.toString(16).padStart(8, '0')}_${local}`
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Inline `<style data-plugin>` injection source for a css text blob: the tag
|
|
92
|
+
* is created once per factory materialization (the loader removes plugin-owned
|
|
93
|
+
* tags on unload).
|
|
94
|
+
*/
|
|
95
|
+
function styleInjectionContents(cssText: string, tagId: string): string {
|
|
96
|
+
return [
|
|
97
|
+
`const css = ${JSON.stringify(cssText)};`,
|
|
98
|
+
`const tagId = ${JSON.stringify(tagId)};`,
|
|
99
|
+
`if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {`,
|
|
100
|
+
` const tag = document.createElement('style');`,
|
|
101
|
+
` tag.dataset.plugin = ${JSON.stringify(ID)};`,
|
|
102
|
+
` tag.dataset.pluginCss = tagId;`,
|
|
103
|
+
` tag.textContent = css;`,
|
|
104
|
+
` document.head.appendChild(tag);`,
|
|
105
|
+
`}`,
|
|
106
|
+
].join('\n')
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Rewrite css-module class tokens (`.local` -> `._<8hex>_<local>`) in
|
|
111
|
+
* **selector position only** — the text between the previous `}`/start and
|
|
112
|
+
* each `{`, at every nesting depth. Declaration values (data-uris, decimals,
|
|
113
|
+
* `content:` strings) are copied verbatim, so a `url("…www.w3.org…")` never
|
|
114
|
+
* gets its dots rewritten into spurious class tokens (the data-uri
|
|
115
|
+
* corruption class). Same brace-walking shape as `collectCssSelectors` —
|
|
116
|
+
* and the same documented limitation: a literal `{` inside a string/url
|
|
117
|
+
* value would mis-walk; the current css keeps all braces out of strings.
|
|
118
|
+
*/
|
|
119
|
+
function hashSelectorTokens(cssText: string, classMap: Record<string, string>): string {
|
|
120
|
+
let out = ''
|
|
121
|
+
let i = 0
|
|
122
|
+
let selectorStart = 0
|
|
123
|
+
while (i < cssText.length) {
|
|
124
|
+
if (cssText[i] === '{') {
|
|
125
|
+
out += cssText.slice(selectorStart, i).replace(/\.[A-Za-z_][A-Za-z0-9_-]*/g, (match) => {
|
|
126
|
+
const local = match.slice(1)
|
|
127
|
+
const hashed = hashClass(local)
|
|
128
|
+
classMap[local] = hashed
|
|
129
|
+
return `.${hashed}`
|
|
130
|
+
})
|
|
131
|
+
let depth = 1
|
|
132
|
+
const blockStart = i + 1
|
|
133
|
+
i++
|
|
134
|
+
while (i < cssText.length && depth > 0) {
|
|
135
|
+
if (cssText[i] === '{') depth++
|
|
136
|
+
else if (cssText[i] === '}') depth--
|
|
137
|
+
i++
|
|
138
|
+
}
|
|
139
|
+
const blockEnd = depth === 0 ? i - 1 : i
|
|
140
|
+
out += `{${hashSelectorTokens(cssText.slice(blockStart, blockEnd), classMap)}}`
|
|
141
|
+
selectorStart = i
|
|
142
|
+
} else {
|
|
143
|
+
i++
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
out += cssText.slice(selectorStart)
|
|
147
|
+
return out
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compile one `*.module.css` file into a JS module: the css text (comments
|
|
152
|
+
* stripped, class tokens hashed) plus a `<style data-plugin>` injection that
|
|
153
|
+
* runs at factory materialization, and the hashed class map as the default
|
|
154
|
+
* export (mirror of the dsh CSS plugin). Class tokens hash to
|
|
155
|
+
* `_<8hex>_<local>` — legal CSS identifiers, mirroring the host bundle's
|
|
156
|
+
* `_1zfRHq_section` shape (see `hashClass`). The class map and the rewritten
|
|
157
|
+
* css text come from this single transform, so they can never drift. The
|
|
158
|
+
* rewrite is selector-aware (see `hashSelectorTokens`): declaration values —
|
|
159
|
+
* including `url()` data-uris — pass through byte-identical, so the emitted
|
|
160
|
+
* chevron SVG keeps `www.w3.org/2000/svg` intact.
|
|
161
|
+
*
|
|
162
|
+
* simplify: the css text is emitted as-is (comment-stripped, not minified);
|
|
163
|
+
* the dsh recipe runs lightningcss. If bundle size ever matters, switch the
|
|
164
|
+
* transform to lightningcss or add a minifier here.
|
|
165
|
+
*/
|
|
166
|
+
function cssModuleContents(fileId: string): string {
|
|
167
|
+
const source = readFileSync(fileId, 'utf8').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
168
|
+
const classMap: Record<string, string> = {}
|
|
169
|
+
const css = hashSelectorTokens(source, classMap)
|
|
170
|
+
const tagId = `${ID}/${basename(fileId)}`
|
|
171
|
+
return [
|
|
172
|
+
styleInjectionContents(css, tagId),
|
|
173
|
+
`export default ${JSON.stringify(classMap)};`,
|
|
174
|
+
].join('\n')
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Virtual-id wrapper keeping module CSS away from tsdown's own css pipeline
|
|
178
|
+
// (mirror of the dsh tsdown.client.ts pattern): the suffix matters — tsdown's
|
|
179
|
+
// guard matches ids ending in `.css`, so the virtual id must not.
|
|
180
|
+
const CSS_VIRTUAL_PREFIX = '\0dsh-css:'
|
|
181
|
+
const CSS_VIRTUAL_SUFFIX = '.mjs'
|
|
182
|
+
|
|
183
|
+
await build({
|
|
184
|
+
// `config: false` — run this config standalone, never merge the root
|
|
185
|
+
// tsdown.config.ts (which owns the host bundle + `clean: true`).
|
|
186
|
+
config: false,
|
|
187
|
+
name: `${ID}/client`,
|
|
188
|
+
entry: { index: ENTRY },
|
|
189
|
+
outDir: OUT_DIR,
|
|
190
|
+
format: 'cjs',
|
|
191
|
+
platform: 'browser',
|
|
192
|
+
target: 'es2022',
|
|
193
|
+
dts: false,
|
|
194
|
+
clean: false,
|
|
195
|
+
deps: {
|
|
196
|
+
// The loader module table is the rule: table entries stay external
|
|
197
|
+
// (tsdown also auto-externalizes peerDependencies), everything else must
|
|
198
|
+
// inline — the browser loader cannot answer a require() it does not
|
|
199
|
+
// know. `onlyBundle: false` marks the inlining intentional.
|
|
200
|
+
neverBundle: [...CLIENT_EXTERNALS],
|
|
201
|
+
alwaysBundle: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
|
|
202
|
+
onlyBundle: false,
|
|
203
|
+
},
|
|
204
|
+
// react/zustand-style deps read process.env.NODE_ENV; honor the build env
|
|
205
|
+
// like the dsh recipe (artifacts default to production). No `import.meta`
|
|
206
|
+
// define needed: nothing inlined reads it (the store engine is external),
|
|
207
|
+
// and a literal `import.meta` would be a SyntaxError under the classic
|
|
208
|
+
// <script> loader anyway.
|
|
209
|
+
define: {
|
|
210
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV ?? 'production'),
|
|
211
|
+
},
|
|
212
|
+
outputOptions: {
|
|
213
|
+
entryFileNames: OUT_FILE,
|
|
214
|
+
// Closure-factory handoff (mirror of the dsh tsdown recipe): `intro`
|
|
215
|
+
// lands inside the factory body (after `banner`) and declares
|
|
216
|
+
// `module`/`exports` because the bundler's cjs emission assigns
|
|
217
|
+
// module.exports itself; the factory returns that surface to the loader.
|
|
218
|
+
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(ID)}, factory: (require) => {`,
|
|
219
|
+
intro: 'var module = { exports: {} }; var exports = module.exports;',
|
|
220
|
+
footer: 'return module.exports; } });',
|
|
221
|
+
},
|
|
222
|
+
plugins: [
|
|
223
|
+
{
|
|
224
|
+
// Bundle purity gate (mirror of the host's `dsh-client-bundle-purity`
|
|
225
|
+
// plugin, tsdown.client.ts:227-237): a value import of any
|
|
226
|
+
// `@deepseek-ai/*` module outside CLIENT_EXTERNALS throws at
|
|
227
|
+
// resolution time. Type-only imports are erased before the module
|
|
228
|
+
// graph is built, so reaching this hook means a real value import —
|
|
229
|
+
// which must not be inlined into the browser-loadable artifact. The
|
|
230
|
+
// emitted-surface scan below is the second line of defense; this hook
|
|
231
|
+
// closes the inlining path, where a stray value import would
|
|
232
|
+
// otherwise be bundled silently (the old require-only scan only saw
|
|
233
|
+
// externals, never inlined modules).
|
|
234
|
+
name: 'dsh-client-bundle-purity',
|
|
235
|
+
resolveId(source: string) {
|
|
236
|
+
if (source.startsWith('@deepseek-ai/') && !CLIENT_EXTERNALS.includes(source)) {
|
|
237
|
+
throw new Error(
|
|
238
|
+
`client bundle purity: value import of ${source} is not in CLIENT_EXTERNALS — type-only peers must stay type-only (use \`import type\`)`,
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return null
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: 'dsh-css-modules-inline',
|
|
246
|
+
resolveId(source: string, importer: string | undefined) {
|
|
247
|
+
if (!source.endsWith('.module.css')) return null
|
|
248
|
+
const abs = importer !== undefined ? resolve(dirname(importer), source) : source
|
|
249
|
+
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX
|
|
250
|
+
},
|
|
251
|
+
load(id: string) {
|
|
252
|
+
if (!id.startsWith(CSS_VIRTUAL_PREFIX)) return null
|
|
253
|
+
const fileId = id.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length)
|
|
254
|
+
return cssModuleContents(fileId)
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const outFile = join(OUT_DIR, OUT_FILE)
|
|
261
|
+
if (!existsSync(outFile)) {
|
|
262
|
+
throw new Error(`client bundle build: expected ${outFile} — check the tsdown config (entry/entryFileNames/outDir)`)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Collect every selector text from a css blob: the text between the previous
|
|
267
|
+
* `}`/start and each `{`, at every nesting depth (so `@media` blocks are
|
|
268
|
+
* walked too). The bundle contract uses this to check class-selector tokens
|
|
269
|
+
* in selector position only — declaration values like `opacity: 0.6`,
|
|
270
|
+
* `transition: .12s`, or the data-uri decimals never sit in a selector, so
|
|
271
|
+
* they cannot be mistaken for a `.`-prefixed class.
|
|
272
|
+
*/
|
|
273
|
+
function collectCssSelectors(cssText: string): string[] {
|
|
274
|
+
const selectors: string[] = []
|
|
275
|
+
const walk = (text: string) => {
|
|
276
|
+
let i = 0
|
|
277
|
+
let selectorStart = 0
|
|
278
|
+
while (i < text.length) {
|
|
279
|
+
if (text[i] === '{') {
|
|
280
|
+
selectors.push(text.slice(selectorStart, i))
|
|
281
|
+
let depth = 1
|
|
282
|
+
const blockStart = i + 1
|
|
283
|
+
i++
|
|
284
|
+
while (i < text.length && depth > 0) {
|
|
285
|
+
if (text[i] === '{') depth++
|
|
286
|
+
else if (text[i] === '}') depth--
|
|
287
|
+
i++
|
|
288
|
+
}
|
|
289
|
+
walk(text.slice(blockStart, depth === 0 ? i - 1 : i))
|
|
290
|
+
selectorStart = i
|
|
291
|
+
} else {
|
|
292
|
+
i++
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
walk(cssText)
|
|
297
|
+
return selectors
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Inline bundle-contract assertions: the emitted text must carry the inlined
|
|
301
|
+
// css-module class map (the transform ran), must NOT contain `import.meta` /
|
|
302
|
+
// ESM statements (the classic-script loader would fail to parse them), every
|
|
303
|
+
// `@deepseek-ai/*` reference in the emitted text must be a documented
|
|
304
|
+
// CLIENT_EXTERNALS entry (the 20260812 type-only peers —
|
|
305
|
+
// `dsh-client-ui-settings-plugins` / `dsh-client-locale` / `dsh-api-remotes` and
|
|
306
|
+
// the other type-only imports — must never appear at runtime, whether as
|
|
307
|
+
// `require(...)` calls or as any other surviving text reference), and every
|
|
308
|
+
// css-module class must be a legal CSS identifier — `hashClass` emits
|
|
309
|
+
// `_<8hex>_<local>` (mirroring the host bundle's `_1zfRHq_section` shape)
|
|
310
|
+
// because a digit-leading hash would be a valid JS key but an illegal
|
|
311
|
+
// selector the browser silently drops.
|
|
312
|
+
const bundleText = readFileSync(outFile, 'utf8')
|
|
313
|
+
if (!bundleText.includes('data-plugin-css')) {
|
|
314
|
+
throw new Error('client bundle contract: no inlined css-module style injection found — check the dsh-css-modules-inline plugin')
|
|
315
|
+
}
|
|
316
|
+
if (bundleText.includes('import.meta') || /(^|\n)\s*(import|export)\s/.test(bundleText)) {
|
|
317
|
+
throw new Error('client bundle contract: emitted bundle contains import.meta / ESM statements — the classic-script loader would fail to parse it')
|
|
318
|
+
}
|
|
319
|
+
// Emitted-surface contract: subsumes the require-only check (externals are
|
|
320
|
+
// the only legitimate `require(...)` targets) and also catches any other
|
|
321
|
+
// surviving text reference to a non-external package. The resolution-time
|
|
322
|
+
// purity plugin above is the structural gate; this scan pins the contract on
|
|
323
|
+
// the artifact itself.
|
|
324
|
+
const deepseekTokens = [...bundleText.matchAll(/@deepseek-ai\/[\w./-]+/g)].map(match => match[0])
|
|
325
|
+
const unexpected = deepseekTokens.filter(specifier => !CLIENT_EXTERNALS.includes(specifier))
|
|
326
|
+
if (unexpected.length > 0) {
|
|
327
|
+
throw new Error(`client bundle contract: non-external @deepseek-ai/* reference(s) survived in the emitted bundle: ${unexpected.join(', ')}`)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// CSS-identifier contract: no class selector in the injected css text starts
|
|
331
|
+
// with a digit (`.\d`), and every hashed class name in the inlined class map
|
|
332
|
+
// matches `^[A-Za-z_][A-Za-z0-9_-]*$`.
|
|
333
|
+
const injectedCssMatches = [...bundleText.matchAll(/\bconst css = ("(?:[^"\\]|\\.)*");/g)]
|
|
334
|
+
if (injectedCssMatches.length === 0) {
|
|
335
|
+
throw new Error('client bundle contract: no inlined css-module style text found — check the dsh-css-modules-inline plugin')
|
|
336
|
+
}
|
|
337
|
+
for (const match of injectedCssMatches) {
|
|
338
|
+
const cssText = JSON.parse(match[1]) as string
|
|
339
|
+
for (const selector of collectCssSelectors(cssText)) {
|
|
340
|
+
for (const token of selector.matchAll(/\.[A-Za-z_0-9][A-Za-z0-9_-]*/g)) {
|
|
341
|
+
if (!/^\.[A-Za-z_]/.test(token[0])) {
|
|
342
|
+
throw new Error(`client bundle contract: class selector ${token[0]} starts with a non-identifier char — hashClass must emit _<8hex>_<local> (mirror the host _1zfRHq_section shape)`)
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Declaration-position contract: a hashed class token (`_<8hex>_`) inside
|
|
347
|
+
// a `url(...)` means the css-module transform leaked into a declaration
|
|
348
|
+
// value — the data-uri corruption class (`www.w3.org` ->
|
|
349
|
+
// `www._0e4734df_w3._9c1df059_org`). The transform rewrites selector
|
|
350
|
+
// position only, so this must never fire; it guards against a future
|
|
351
|
+
// regression to a naive global replace. The matcher accepts quoted urls
|
|
352
|
+
// whose content contains the other quote type (the chevron data uri is
|
|
353
|
+
// double-quoted and full of `xmlns='...'`), or bare unquoted urls.
|
|
354
|
+
for (const urlMatch of cssText.matchAll(/url\(\s*(?:"([^"]*)"|'([^']*)'|([^"'()\s]+))\s*\)/g)) {
|
|
355
|
+
const urlContent = urlMatch[1] ?? urlMatch[2] ?? urlMatch[3]
|
|
356
|
+
if (/_[0-9a-fA-F]{8}_/.test(urlContent)) {
|
|
357
|
+
throw new Error(`client bundle contract: hashed class token leaked into url() declaration value: ${urlContent} — the css-module transform must rewrite selector position only`)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
const classMapMatches = [...bundleText.matchAll(/\b\w+_module_css_default\s*=\s*(\{[\s\S]*?\n\s*\});/g)]
|
|
362
|
+
if (classMapMatches.length === 0) {
|
|
363
|
+
throw new Error('client bundle contract: no inlined css-module class map found — check the dsh-css-modules-inline plugin')
|
|
364
|
+
}
|
|
365
|
+
for (const mapMatch of classMapMatches) {
|
|
366
|
+
const classMap = JSON.parse(mapMatch[1]) as Record<string, string>
|
|
367
|
+
for (const [local, hashed] of Object.entries(classMap)) {
|
|
368
|
+
if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(hashed)) {
|
|
369
|
+
throw new Error(`client bundle contract: hashed class "${local}" -> "${hashed}" is not a valid CSS identifier — hashClass must emit _<8hex>_<local> (mirror the host _1zfRHq_section shape)`)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(`build-client: ${ENTRY} -> ${outFile} (closure-factory CJS, tsdown)`)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse-only guard for dist artifacts — never executes module code, never
|
|
3
|
+
* resolves imports. Catches the boot failure mode where the bundler leaves
|
|
4
|
+
* standard-decorator syntax (e.g. `@Remote(...)`) verbatim in emitted ESM,
|
|
5
|
+
* which Node cannot parse.
|
|
6
|
+
*
|
|
7
|
+
* - host bundle: `dist/index.js` is ESM -> `node --check --input-type=module`
|
|
8
|
+
* (syntax check only, no execution, no dependency resolution). The original
|
|
9
|
+
* design called for `new vm.SourceTextModule(...)`, but Node 24 removed the
|
|
10
|
+
* experimental `vm.Module` API; `--check --input-type=module` is Node's
|
|
11
|
+
* built-in parse-only ESM path and matches the old guard's semantics.
|
|
12
|
+
* - client bundle: `dist/client/index.js` is a CJS closure -> `vm.Script`
|
|
13
|
+
* (classic-script compile at construction).
|
|
14
|
+
*
|
|
15
|
+
* On SyntaxError, prints file + message and exits 1; otherwise prints sizes
|
|
16
|
+
* and exits 0.
|
|
17
|
+
*/
|
|
18
|
+
import { spawnSync } from 'node:child_process'
|
|
19
|
+
import { readFileSync } from 'node:fs'
|
|
20
|
+
import { Script } from 'node:vm'
|
|
21
|
+
|
|
22
|
+
const targets = [
|
|
23
|
+
{ name: 'host', file: 'dist/index.js', kind: 'esm' },
|
|
24
|
+
{ name: 'client', file: 'dist/client/index.js', kind: 'cjs' },
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
let failed = false
|
|
28
|
+
for (const { name, file, kind } of targets) {
|
|
29
|
+
const code = readFileSync(file, 'utf8')
|
|
30
|
+
const bytes = Buffer.byteLength(code)
|
|
31
|
+
const lines = code.split('\n').length
|
|
32
|
+
let error = null
|
|
33
|
+
if (kind === 'esm') {
|
|
34
|
+
const result = spawnSync(process.execPath, ['--check', '--input-type=module'], {
|
|
35
|
+
input: code,
|
|
36
|
+
encoding: 'utf8',
|
|
37
|
+
})
|
|
38
|
+
if (result.status !== 0) {
|
|
39
|
+
error = new SyntaxError(
|
|
40
|
+
(result.stderr.split('\n').find(line => line.startsWith('SyntaxError')) ??
|
|
41
|
+
result.stderr.trim() ??
|
|
42
|
+
result.error?.message ??
|
|
43
|
+
'syntax check failed'),
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
} else {
|
|
47
|
+
try {
|
|
48
|
+
new Script(code)
|
|
49
|
+
} catch (cause) {
|
|
50
|
+
error = cause
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (error === null) {
|
|
54
|
+
console.log(`verify-dist: ${name} OK (${file}, ${bytes} bytes, ${lines} lines)`)
|
|
55
|
+
} else {
|
|
56
|
+
failed = true
|
|
57
|
+
if (error instanceof SyntaxError) {
|
|
58
|
+
console.error(`verify-dist: ${name} SyntaxError in ${file}: ${error.message}`)
|
|
59
|
+
} else {
|
|
60
|
+
throw error
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (failed) {
|
|
66
|
+
console.error('verify-dist: dist parse check failed')
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|