@citisen/dsh-font 0.1.0 → 0.2.0
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/README.md +274 -14
- package/README.zh.md +120 -9
- package/lib/client.js +1911 -145
- package/lib/index.js +66 -4
- package/package.json +9 -3
- package/scripts/build-client.mjs +24 -1
- package/scripts/release.mjs +91 -0
- package/scripts/verify-client.mjs +1177 -12
- package/scripts/verify-host.mjs +59 -5
- package/scripts/verify-profile.mjs +64 -16
- package/src/client.js +1893 -145
package/scripts/verify-host.mjs
CHANGED
|
@@ -12,14 +12,15 @@
|
|
|
12
12
|
import assert from 'node:assert/strict'
|
|
13
13
|
import { existsSync } from 'node:fs'
|
|
14
14
|
import { createRequire } from 'node:module'
|
|
15
|
-
import { dirname, join } from 'node:path'
|
|
15
|
+
import { dirname, join, resolve } from 'node:path'
|
|
16
16
|
import { pathToFileURL } from 'node:url'
|
|
17
17
|
import { fileURLToPath } from 'node:url'
|
|
18
18
|
|
|
19
19
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Import `lib/index.js
|
|
22
|
+
* Import `lib/index.js`, either from a path given on the command line or from
|
|
23
|
+
* an installed profile.
|
|
23
24
|
*
|
|
24
25
|
* The host half imports `@deepseek-ai/schemastery`, which dsh supplies to a
|
|
25
26
|
* plugin from the installation's module fallback rather than from the plugin's
|
|
@@ -27,8 +28,16 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
27
28
|
* this check also proves the plugin's runtime dependencies actually resolve at
|
|
28
29
|
* the place dsh will load it from. Falling back to the direct path keeps the
|
|
29
30
|
* check usable in a bare checkout.
|
|
31
|
+
*
|
|
32
|
+
* Note that importing through a directory junction resolves to its real path,
|
|
33
|
+
* so pass the installed `lib/index.js` explicitly to exercise a published copy
|
|
34
|
+
* rather than a linked one.
|
|
30
35
|
*/
|
|
31
36
|
async function importHost() {
|
|
37
|
+
const explicit = process.argv[2] ?? process.env.DSH_FONT_HOST
|
|
38
|
+
if (explicit !== undefined) {
|
|
39
|
+
return { host: await import(pathToFileURL(resolve(explicit)).href), via: resolve(explicit) }
|
|
40
|
+
}
|
|
32
41
|
const dshHome = process.env.DSH_HOME
|
|
33
42
|
const profile = process.env.DSH_PROFILE ?? 'web'
|
|
34
43
|
if (dshHome !== undefined) {
|
|
@@ -37,7 +46,7 @@ async function importHost() {
|
|
|
37
46
|
try {
|
|
38
47
|
// Resolve the bare specifier: `exports` maps "." to lib/index.js, and a
|
|
39
48
|
// direct `dsh-font/lib/index.js` path is deliberately not exported.
|
|
40
|
-
const resolved = createRequire(anchor).resolve('dsh-font')
|
|
49
|
+
const resolved = createRequire(anchor).resolve('@citisen/dsh-font')
|
|
41
50
|
return { host: await import(pathToFileURL(resolved).href), via: resolved }
|
|
42
51
|
} catch {
|
|
43
52
|
/* not installed in that profile; fall through to the direct import */
|
|
@@ -56,6 +65,9 @@ const {
|
|
|
56
65
|
FontSettingsSchema,
|
|
57
66
|
DEFAULT_UI_FONT_FAMILY,
|
|
58
67
|
DEFAULT_CODE_FONT_FAMILY,
|
|
68
|
+
DEFAULT_CODE_FONT_WEIGHT,
|
|
69
|
+
DEFAULT_UI_FONT_WEIGHT,
|
|
70
|
+
FONT_WEIGHTS,
|
|
59
71
|
FONT_STYLE_ID,
|
|
60
72
|
fontStyleSheet,
|
|
61
73
|
fontBootstrapScript,
|
|
@@ -70,28 +82,61 @@ assert.equal(FONT_SETTINGS_NAMESPACE, 'ui-font')
|
|
|
70
82
|
const defaults = FontSettingsSchema({})
|
|
71
83
|
assert.equal(defaults.uiFontFamily, DEFAULT_UI_FONT_FAMILY)
|
|
72
84
|
assert.equal(defaults.codeFontFamily, DEFAULT_CODE_FONT_FAMILY)
|
|
85
|
+
assert.equal(defaults.codeFontWeight, DEFAULT_CODE_FONT_WEIGHT)
|
|
86
|
+
assert.equal(defaults.uiFontWeight, DEFAULT_UI_FONT_WEIGHT)
|
|
87
|
+
assert.equal(DEFAULT_UI_FONT_WEIGHT, 400, 'the shipped interface weight is the design system base')
|
|
73
88
|
assert.equal(defaults.uiFontScale, 1)
|
|
74
89
|
assert.equal(defaults.contentFontSize, 14)
|
|
75
90
|
assert.equal(defaults.codeFontSize, 12)
|
|
76
91
|
|
|
92
|
+
// The weight is a closed vocabulary, not a free number: a weight no family is
|
|
93
|
+
// guaranteed to have would be synthesized by the browser (faux-bold), so it
|
|
94
|
+
// must be rejected at the wire boundary rather than painted.
|
|
95
|
+
assert.deepEqual(FONT_WEIGHTS, [100, 200, 300, 400, 500, 600, 700, 800, 900])
|
|
96
|
+
for (const weight of FONT_WEIGHTS) {
|
|
97
|
+
assert.equal(FontSettingsSchema({ codeFontWeight: weight }).codeFontWeight, weight)
|
|
98
|
+
assert.equal(FontSettingsSchema({ uiFontWeight: weight }).uiFontWeight, weight)
|
|
99
|
+
}
|
|
100
|
+
assert.throws(() => FontSettingsSchema({ codeFontWeight: 550 }))
|
|
101
|
+
assert.throws(() => FontSettingsSchema({ codeFontWeight: 0 }))
|
|
102
|
+
assert.throws(() => FontSettingsSchema({ codeFontWeight: '500' }))
|
|
103
|
+
assert.throws(() => FontSettingsSchema({ uiFontWeight: 550 }))
|
|
104
|
+
assert.throws(() => FontSettingsSchema({ uiFontWeight: 'medium' }))
|
|
105
|
+
|
|
77
106
|
// Out-of-range values must be rejected at the wire boundary.
|
|
78
107
|
assert.throws(() => FontSettingsSchema({ contentFontSize: 99 }))
|
|
79
108
|
assert.throws(() => FontSettingsSchema({ uiFontScale: 0.1 }))
|
|
80
109
|
|
|
81
110
|
// The pre-paint sheet carries the scale and both size axes, one rule per
|
|
82
|
-
// hard-coded UI text step
|
|
111
|
+
// hard-coded UI text step — and the code weight, which the design system has no
|
|
112
|
+
// token of its own for.
|
|
83
113
|
const sheet = fontStyleSheet(defaults)
|
|
84
114
|
assert.match(sheet, /--dsh-font-ui-scale:1;/)
|
|
85
115
|
assert.match(sheet, /--dsh-font-content-size:14px/)
|
|
86
116
|
assert.match(sheet, /--dsh-font-code-size:12px/)
|
|
117
|
+
assert.match(sheet, /--dsh-font-code-weight:400;/)
|
|
87
118
|
assert.match(sheet, /font-size:calc\(14px \* var\(--dsh-font-ui-scale,1\)\) !important/)
|
|
88
119
|
for (const step of [11, 12, 13, 14, 16, 20, 24]) {
|
|
89
120
|
assert.ok(sheet.includes(`calc(${String(step)}px * `), `missing scale rule for ${String(step)}px`)
|
|
90
121
|
}
|
|
91
122
|
|
|
123
|
+
// The interface weight is opted into: the shipped 400 emits no rule at all, so
|
|
124
|
+
// a default install's first frame is exactly what the design system paints.
|
|
125
|
+
assert.ok(
|
|
126
|
+
!/font-weight:/.test(sheet),
|
|
127
|
+
'the shipped interface weight must not emit a weight rule',
|
|
128
|
+
)
|
|
129
|
+
const heavy = fontStyleSheet({ ...defaults, uiFontWeight: 500 })
|
|
130
|
+
assert.match(heavy, /html body\{font-weight:500\}/)
|
|
131
|
+
|
|
92
132
|
const script = fontBootstrapScript(defaults)
|
|
93
133
|
assert.ok(script.includes(JSON.stringify(FONT_STYLE_ID)), 'bootstrap must key on the stylesheet id')
|
|
94
134
|
assert.ok(!script.includes('</script'), 'bootstrap must not close its own script element')
|
|
135
|
+
assert.ok(!script.includes('font-weight'), 'the default bootstrap must carry no weight rule')
|
|
136
|
+
assert.ok(
|
|
137
|
+
fontBootstrapScript({ ...defaults, uiFontWeight: 500 }).includes('font-weight:500'),
|
|
138
|
+
'a set interface weight must reach the pre-paint bootstrap',
|
|
139
|
+
)
|
|
95
140
|
|
|
96
141
|
// Execute the bootstrap against a DOM stub and read back what it wrote.
|
|
97
142
|
const appended = []
|
|
@@ -124,12 +169,21 @@ assert.match(created[0].textContent, /--dsh-font-ui-scale:1;/)
|
|
|
124
169
|
assert.equal(appended.length, 1)
|
|
125
170
|
assert.equal(rootStyle.get('--dsw-font-family'), DEFAULT_UI_FONT_FAMILY)
|
|
126
171
|
assert.equal(rootStyle.get('--ds-font-family-code'), DEFAULT_CODE_FONT_FAMILY)
|
|
172
|
+
assert.equal(rootStyle.get('--dsh-font-code-weight'), '400')
|
|
127
173
|
delete globalThis.document
|
|
128
174
|
|
|
129
175
|
// A non-default section must flow through both the sheet and the script.
|
|
130
|
-
const custom = {
|
|
176
|
+
const custom = {
|
|
177
|
+
...defaults,
|
|
178
|
+
uiFontScale: 1.25,
|
|
179
|
+
contentFontSize: 16,
|
|
180
|
+
codeFontSize: 13,
|
|
181
|
+
codeFontWeight: 500,
|
|
182
|
+
}
|
|
131
183
|
assert.match(fontStyleSheet(custom), /--dsh-font-content-size:16px/)
|
|
184
|
+
assert.match(fontStyleSheet(custom), /--dsh-font-code-weight:500;/)
|
|
132
185
|
assert.match(fontStyleSheet(custom), /font-size:calc\(16px \* var\(--dsh-font-ui-scale,1\)\)/)
|
|
186
|
+
assert.ok(fontBootstrapScript(custom).includes('--dsh-font-code-weight'))
|
|
133
187
|
|
|
134
188
|
const injection = fontInjection(defaults)
|
|
135
189
|
assert.equal(injection.kind, 'script')
|
|
@@ -1,41 +1,89 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* End-to-end profile composition check.
|
|
3
3
|
*
|
|
4
|
-
* Loads
|
|
4
|
+
* Loads a real dsh profile through dsh's own profile loader and asserts the
|
|
5
5
|
* composed entry list contains this bundle's row. This is the check that
|
|
6
6
|
* catches the failure modes a browser never explains: a bundle the loader
|
|
7
7
|
* cannot resolve, a patch file with the wrong shape, or a row that never made
|
|
8
8
|
* it into `dsh.profile.bundles`.
|
|
9
9
|
*
|
|
10
|
+
* It needs a dsh installation and an initialized profile, so it SKIPS (exit 0)
|
|
11
|
+
* when neither is present — a clean CI runner has no dsh, and a release must
|
|
12
|
+
* not fail merely because this check cannot run there. Set `DSH_REQUIRE=1` to
|
|
13
|
+
* turn a skip into a failure.
|
|
14
|
+
*
|
|
10
15
|
* Usage:
|
|
11
16
|
* node scripts/verify-profile.mjs [profile-name]
|
|
17
|
+
*
|
|
18
|
+
* Environment:
|
|
19
|
+
* DSH_HOME Harness home holding profiles/ (default ~/.dsh)
|
|
20
|
+
* DSH_PROFILE Profile name (default web; a command-line argument wins)
|
|
21
|
+
* DSH_CHECKOUT dsh installation's node_modules/@deepseek-ai
|
|
22
|
+
* DSH_REQUIRE Set to 1 to fail instead of skipping when dsh is unavailable
|
|
12
23
|
*/
|
|
13
24
|
|
|
14
|
-
import { readFileSync, statSync } from 'node:fs'
|
|
25
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
15
26
|
import { createRequire } from 'node:module'
|
|
16
27
|
import { dirname, join } from 'node:path'
|
|
17
28
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
29
|
|
|
19
|
-
/**
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
/** This plugin's package name, as declared by its own manifest. */
|
|
31
|
+
const OWN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
32
|
+
const PACKAGE_NAME = JSON.parse(readFileSync(join(OWN_ROOT, 'package.json'), 'utf8')).name
|
|
33
|
+
|
|
34
|
+
const HOME = process.env.USERPROFILE ?? process.env.HOME ?? ''
|
|
35
|
+
const DSH_HOME = process.env.DSH_HOME ?? (HOME === '' ? undefined : join(HOME, '.dsh'))
|
|
36
|
+
|
|
37
|
+
/** Give up (or fail, under DSH_REQUIRE) because dsh is not usable here. */
|
|
38
|
+
function skip(reason) {
|
|
39
|
+
console.log(`verify-profile: SKIP — ${reason}`)
|
|
40
|
+
if (process.env.DSH_REQUIRE === '1') {
|
|
41
|
+
console.error('verify-profile: DSH_REQUIRE=1, treating the skip as a failure')
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
process.exit(0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Locate the dsh installation whose loader and composition rules this check
|
|
49
|
+
* uses. `npx` caches live under a version-hashed directory, so discover the
|
|
50
|
+
* path rather than pin one: a pinned path rots on the next install and would
|
|
51
|
+
* silently turn this check into a no-op.
|
|
52
|
+
* @returns the `@deepseek-ai` directory, or undefined when none is present.
|
|
53
|
+
*/
|
|
54
|
+
function findCheckout() {
|
|
55
|
+
if (process.env.DSH_CHECKOUT !== undefined) return process.env.DSH_CHECKOUT
|
|
56
|
+
const candidates = []
|
|
57
|
+
const npxRoot = join(HOME, 'AppData', 'Local', 'npm-cache', '_npx')
|
|
58
|
+
if (existsSync(npxRoot)) {
|
|
59
|
+
for (const entry of readdirSync(npxRoot)) {
|
|
60
|
+
candidates.push(join(npxRoot, entry, 'node_modules', '@deepseek-ai'))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
candidates.push(join(HOME, 'node_modules', '@deepseek-ai'))
|
|
64
|
+
return candidates.find((candidate) =>
|
|
65
|
+
existsSync(join(candidate, 'dsh-app-boot', 'lib', 'index.js')),
|
|
25
66
|
)
|
|
26
|
-
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const DSH_ROOT = findCheckout()
|
|
70
|
+
if (DSH_ROOT === undefined) {
|
|
71
|
+
skip('no dsh installation found (set DSH_CHECKOUT to point at one)')
|
|
72
|
+
}
|
|
27
73
|
|
|
28
|
-
const
|
|
74
|
+
const INSTALL_ANCHOR = join(DSH_ROOT, 'dsh', 'package.json')
|
|
75
|
+
const profileName = process.argv[2] ?? process.env.DSH_PROFILE ?? 'web'
|
|
29
76
|
|
|
30
77
|
const { loadProfile, composeEntries } = await import(
|
|
31
|
-
pathToFileURL(
|
|
78
|
+
pathToFileURL(join(DSH_ROOT, 'dsh-app-boot', 'lib', 'index.js')).href
|
|
32
79
|
)
|
|
33
80
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
81
|
+
let profile
|
|
82
|
+
try {
|
|
83
|
+
profile = loadProfile('dsh', profileName, INSTALL_ANCHOR, DSH_HOME)
|
|
84
|
+
} catch (error) {
|
|
85
|
+
skip(`profile "${profileName}" is not available: ${String(error)}`)
|
|
86
|
+
}
|
|
39
87
|
|
|
40
88
|
const layers = profile.layers.map((layer) => layer.packageName)
|
|
41
89
|
console.log(`verify-profile: profile "${profileName}" at ${profile.dir}`)
|