@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.
@@ -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` through the installed profile when one exists.
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 = { ...defaults, uiFontScale: 1.25, contentFontSize: 16, codeFontSize: 13 }
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 the real `web` profile through dsh's own profile loader and asserts the
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
- /** The dsh installation whose own loader and composition rules this check uses. */
20
- const DSH_ROOT =
21
- process.env.DSH_CHECKOUT ??
22
- join(
23
- process.env.LOCALAPPDATA ?? 'C:/Users/Administrator/AppData/Local',
24
- 'npm-cache/_npx/1e7f6d9597241db0/node_modules/@deepseek-ai',
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
- const INSTALL_ANCHOR = `${DSH_ROOT}/dsh/package.json`
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 profileName = process.argv[2] ?? 'web'
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(`${DSH_ROOT}/dsh-app-boot/lib/index.js`).href
78
+ pathToFileURL(join(DSH_ROOT, 'dsh-app-boot', 'lib', 'index.js')).href
32
79
  )
33
80
 
34
- const profile = loadProfile('dsh', profileName, INSTALL_ANCHOR)
35
-
36
- /** This plugin's package name, as declared by its own manifest. */
37
- const OWN_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
38
- const PACKAGE_NAME = JSON.parse(readFileSync(join(OWN_ROOT, 'package.json'), 'utf8')).name
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}`)