basalt-ui 1.22.0 → 1.23.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/AGENTS.md +29 -12
- package/CHANGELOG.md +28 -0
- package/MIGRATING.md +207 -12
- package/README.md +74 -21
- package/agent/rules/basalt-app.md +33 -3
- package/agent/rules/basalt-charts.md +78 -7
- package/agent/rules/basalt-content.md +3 -3
- package/agent/rules/basalt-mantine.md +4 -4
- package/agent/rules/basalt-tokens.md +35 -6
- package/agent/skills/basalt-app/SKILL.md +25 -12
- package/agent/skills/basalt-charts/SKILL.md +27 -10
- package/agent/templates/CLAUDE-block.md.tpl +3 -2
- package/agent/templates/DESIGN.md.tpl +3 -1
- package/configs/oxlint-plugin.js +49 -8
- package/configs/oxlint-plugin.test.ts +88 -31
- package/dist/charts/hooks/useBandPlot.d.ts +138 -0
- package/dist/charts/hooks/useBandPlot.d.ts.map +1 -0
- package/dist/charts/hooks/useBandPlot.js +127 -0
- package/dist/charts/index.d.ts +4 -0
- package/dist/charts/index.d.ts.map +1 -1
- package/dist/charts/index.js +14 -0
- package/dist/charts/kinds/BandStrip.d.ts +127 -0
- package/dist/charts/kinds/BandStrip.d.ts.map +1 -0
- package/dist/charts/kinds/BandStrip.js +271 -0
- package/dist/charts/kinds/BandStrip.test.d.ts +2 -0
- package/dist/charts/kinds/BandStrip.test.d.ts.map +1 -0
- package/dist/charts/kinds/Bars.d.ts +5 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Bars.js +2 -0
- package/dist/charts/kinds/MirroredBars.d.ts +95 -0
- package/dist/charts/kinds/MirroredBars.d.ts.map +1 -0
- package/dist/charts/kinds/MirroredBars.js +328 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts +2 -0
- package/dist/charts/kinds/MirroredBars.test.d.ts.map +1 -0
- package/dist/charts/kinds/MultiLine.d.ts +5 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.js +2 -0
- package/dist/charts/kinds/StackedArea.d.ts +5 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.js +2 -0
- package/dist/charts/kinds/ZonedLine.d.ts +5 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.js +2 -0
- package/dist/charts/primitives/CartesianChart.d.ts +18 -2
- package/dist/charts/primitives/CartesianChart.d.ts.map +1 -1
- package/dist/charts/primitives/CartesianChart.js +5 -2
- package/dist/charts/primitives/HatchPattern.d.ts +31 -0
- package/dist/charts/primitives/HatchPattern.d.ts.map +1 -0
- package/dist/charts/primitives/HatchPattern.js +31 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +228 -31
- package/dist/cli/shipped-versions.test.d.ts +2 -0
- package/dist/cli/shipped-versions.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +60 -13
- package/dist/vite.d.ts +42 -3
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +67 -30
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/hooks/useBandPlot.ts +302 -0
- package/src/charts/index.ts +14 -0
- package/src/charts/kinds/BandStrip.test.tsx +282 -0
- package/src/charts/kinds/BandStrip.tsx +457 -0
- package/src/charts/kinds/Bars.tsx +7 -1
- package/src/charts/kinds/DualPanel.test.tsx +10 -4
- package/src/charts/kinds/MirroredBars.test.tsx +232 -0
- package/src/charts/kinds/MirroredBars.tsx +491 -0
- package/src/charts/kinds/MultiLine.tsx +7 -1
- package/src/charts/kinds/StackedArea.tsx +7 -1
- package/src/charts/kinds/ZonedLine.tsx +7 -1
- package/src/charts/primitives/CartesianChart.test.tsx +86 -4
- package/src/charts/primitives/CartesianChart.tsx +28 -3
- package/src/charts/primitives/HatchPattern.tsx +55 -0
- package/src/cli/help.test.ts +74 -1
- package/src/cli/index.ts +436 -52
- package/src/cli/shipped-versions.test.ts +83 -0
- package/src/cli/tokens-css.test.ts +62 -0
- package/src/cli/toolchain-wiring.test.ts +171 -4
- package/src/cli/waiver-audit.test.ts +7 -3
- package/src/guard/check-source.test.ts +211 -42
- package/src/guard/index.ts +149 -23
- package/src/vite.test.ts +79 -0
- package/src/vite.ts +141 -39
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* No shipped artifact may name a basalt version npm never served.
|
|
3
|
+
*
|
|
4
|
+
* Three rounds running, the newest `MIGRATING.md` heading named a phantom — `1.20.1` (shipped as
|
|
5
|
+
* 1.21.0), then `1.21.1` (shipped as 1.22.0) — because the docs pass runs BEFORE semantic-release
|
|
6
|
+
* computes the number, so whatever is written is a guess. That would be a docs problem alone if it
|
|
7
|
+
* stayed in docs. It does not: `sync` copies `agent/rules/**` and `agent/skills/**` verbatim into
|
|
8
|
+
* every consumer, so "Since 1.21.1" is currently shipped, as fact, into seven repos.
|
|
9
|
+
*
|
|
10
|
+
* This is the gate that ends it, and it is mechanical rather than editorial: every basalt-shaped
|
|
11
|
+
* version literal in a file a consumer receives must appear as a release heading in CHANGELOG.md —
|
|
12
|
+
* the one file `semantic-release` writes AFTER the number exists. A guessed version fails here
|
|
13
|
+
* before it can ship.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately NOT a ban on version literals: `agent/rules/**` is release-history prose and
|
|
16
|
+
* "widened at 1.21.0, promoted at 1.22.0" is exactly the sentence a consumer needs. What it may
|
|
17
|
+
* not do is name a release that does not exist.
|
|
18
|
+
*
|
|
19
|
+
* The complementary half — no version stamped into a file nothing ever rewrites — lives in
|
|
20
|
+
* `toolchain-wiring.test.ts` ("DESIGN.md carries no version").
|
|
21
|
+
*
|
|
22
|
+
* Run: bun test packages/basalt-ui/src/cli/shipped-versions.test.ts
|
|
23
|
+
*/
|
|
24
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
25
|
+
import { join, relative, resolve } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { describe, expect, it } from 'bun:test'
|
|
28
|
+
|
|
29
|
+
const PKG_ROOT = fileURLToPath(new URL('../../', import.meta.url))
|
|
30
|
+
|
|
31
|
+
/** Every version CHANGELOG.md records, plus the one package.json currently declares. */
|
|
32
|
+
const RELEASED = new Set<string>([
|
|
33
|
+
...(
|
|
34
|
+
readFileSync(resolve(PKG_ROOT, 'CHANGELOG.md'), 'utf8').match(/^#+\s*\[?(\d+\.\d+\.\d+)/gm) ??
|
|
35
|
+
[]
|
|
36
|
+
).map((h) => h.replace(/^#+\s*\[?/, '')),
|
|
37
|
+
(JSON.parse(readFileSync(resolve(PKG_ROOT, 'package.json'), 'utf8')) as { version: string })
|
|
38
|
+
.version,
|
|
39
|
+
])
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* basalt's own version line is 0.x / 1.x, so a literal with any other major belongs to a peer
|
|
43
|
+
* (visx 4.0.0, motion 12.42.0, the fontsource 5.2.x trio) and is none of this gate's business. A
|
|
44
|
+
* `^`/`~`/`@`-prefixed literal is a dependency RANGE for the same reason — `vite-plugin-pwa
|
|
45
|
+
* (^1.3.0)` shares a major with basalt and is still not a claim about basalt.
|
|
46
|
+
*/
|
|
47
|
+
const BASALT_VERSION_LITERAL = /(^|[^-~^@\w.])([01]\.\d+\.\d+)(?![\w.])/g
|
|
48
|
+
|
|
49
|
+
function versionsIn(content: string): string[] {
|
|
50
|
+
return [...content.matchAll(BASALT_VERSION_LITERAL)].map((m) => m[2] as string)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Every file under `dir`, recursively. */
|
|
54
|
+
function filesUnder(dir: string): string[] {
|
|
55
|
+
return readdirSync(dir).flatMap((name) => {
|
|
56
|
+
const abs = join(dir, name)
|
|
57
|
+
return statSync(abs).isDirectory() ? filesUnder(abs) : [abs]
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Everything `init`/`sync` place in a consumer repo, plus the presets they `extends` into.
|
|
62
|
+
const SHIPPED = [
|
|
63
|
+
...filesUnder(resolve(PKG_ROOT, 'agent')),
|
|
64
|
+
resolve(PKG_ROOT, 'configs/check.yml'),
|
|
65
|
+
resolve(PKG_ROOT, 'configs/lefthook.yml'),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
describe('shipped agent assets name only versions that exist', () => {
|
|
69
|
+
it.each(SHIPPED.map((abs) => [relative(PKG_ROOT, abs), abs] as const))(
|
|
70
|
+
'%s',
|
|
71
|
+
(_rel, abs: string) => {
|
|
72
|
+
const unreleased = [...new Set(versionsIn(readFileSync(abs, 'utf8')))].filter(
|
|
73
|
+
(v) => !RELEASED.has(v),
|
|
74
|
+
)
|
|
75
|
+
expect(unreleased).toEqual([])
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
it('reads a real changelog, so an empty RELEASED set can never make this vacuous', () => {
|
|
80
|
+
expect(RELEASED.size).toBeGreaterThan(20)
|
|
81
|
+
expect(RELEASED.has('1.21.1')).toBe(false)
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -14,6 +14,7 @@ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSy
|
|
|
14
14
|
import { tmpdir } from 'node:os'
|
|
15
15
|
import { join, resolve } from 'node:path'
|
|
16
16
|
|
|
17
|
+
import { GENERATED_HEADER_LINE } from '../guard'
|
|
17
18
|
import { buildPaletteCss } from '../tokens'
|
|
18
19
|
import { normalizeColorFunctions, run, tokensCss } from './index'
|
|
19
20
|
|
|
@@ -118,3 +119,64 @@ describe('tokens:css', () => {
|
|
|
118
119
|
expect(readdirSync(dir)).toEqual(['package.json'])
|
|
119
120
|
})
|
|
120
121
|
})
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* `--check` gates the token VALUES, never the provenance line.
|
|
125
|
+
*
|
|
126
|
+
* Line 2 of the generated header carries the emitting version, and `--check` used to compare the
|
|
127
|
+
* whole file byte for byte — so every basalt-ui release forced a mandatory no-op commit in every
|
|
128
|
+
* consumer that commits a generated sheet: regenerate, one line moves, nothing else, ship it. That
|
|
129
|
+
* is the rot pattern removed from DESIGN.md this cycle, on a file whose gate is byte-equality.
|
|
130
|
+
*/
|
|
131
|
+
describe('tokens:css --check and the provenance line', () => {
|
|
132
|
+
/** Capture stdout/stderr so the check's own sentence can be asserted. */
|
|
133
|
+
function check(out: string): { code: number; log: string } {
|
|
134
|
+
const originalLog = console.log
|
|
135
|
+
const originalError = console.error
|
|
136
|
+
let log = ''
|
|
137
|
+
const sink = (...args: unknown[]) => {
|
|
138
|
+
log += `${args.join(' ')}\n`
|
|
139
|
+
}
|
|
140
|
+
console.log = sink
|
|
141
|
+
console.error = sink
|
|
142
|
+
try {
|
|
143
|
+
return { code: tokensCss(['--check', '--out', out], dir), log }
|
|
144
|
+
} finally {
|
|
145
|
+
console.log = originalLog
|
|
146
|
+
console.error = originalError
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
it('passes when only the emitting VERSION on line 2 differs', () => {
|
|
151
|
+
const emitted = emit([])
|
|
152
|
+
const lines = emitted.split('\n')
|
|
153
|
+
lines[1] = (lines[1] as string).replace(/basalt-ui [\d.]+/, 'basalt-ui 0.0.1')
|
|
154
|
+
writeFileSync(resolve(dir, 'out.css'), lines.join('\n'))
|
|
155
|
+
|
|
156
|
+
const { code, log } = check('out.css')
|
|
157
|
+
expect(code).toBe(0)
|
|
158
|
+
expect(log).toContain('is up to date')
|
|
159
|
+
expect(log).toContain('provenance line still names an older basalt-ui')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('says nothing about provenance when the file is byte-identical', () => {
|
|
163
|
+
emit([])
|
|
164
|
+
const { code, log } = check('out.css')
|
|
165
|
+
expect(code).toBe(0)
|
|
166
|
+
expect(log).not.toContain('provenance line')
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('STILL fails when a token value actually moved', () => {
|
|
170
|
+
const emitted = emit([])
|
|
171
|
+
writeFileSync(resolve(dir, 'out.css'), emitted.replace('--vx-neutral', '--vx-neutrall'))
|
|
172
|
+
const { code, log } = check('out.css')
|
|
173
|
+
expect(code).toBe(1)
|
|
174
|
+
expect(log).toContain('differs from what')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('the header itself is unchanged — the `@generated` exemption needs both lines verbatim', () => {
|
|
178
|
+
const lines = emit([]).split('\n')
|
|
179
|
+
expect(lines[0]).toBe(GENERATED_HEADER_LINE)
|
|
180
|
+
expect(lines[1]).toMatch(/^\/\* basalt-ui [\d.]+ — `basalt-ui tokens:css --out out\.css` \*\/$/)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
@@ -344,7 +344,7 @@ describe('check-theme — finds the config a root-invoked hook cannot see', () =
|
|
|
344
344
|
it('reports ambiguity rather than guessing when two packages carry a config', () => {
|
|
345
345
|
ambiguousWorkspace()
|
|
346
346
|
const { code, log } = capture(() => checkTheme(dir))
|
|
347
|
-
expect(log).toContain('2
|
|
347
|
+
expect(log).toContain('2 packages below it carry one')
|
|
348
348
|
expect(log).toContain('BASALT_CWD')
|
|
349
349
|
expect(code).toBe(1)
|
|
350
350
|
})
|
|
@@ -363,7 +363,7 @@ describe('doctor — an ambiguous project is terminal, exactly as it is for chec
|
|
|
363
363
|
it('short-circuits instead of running every check against the wrong root', () => {
|
|
364
364
|
ambiguousWorkspace()
|
|
365
365
|
const { code, log } = capture(() => doctor(dir))
|
|
366
|
-
expect(log).toContain('2
|
|
366
|
+
expect(log).toContain('2 packages below it carry one')
|
|
367
367
|
expect(log).toContain('BASALT_CWD')
|
|
368
368
|
expect(code).toBe(1)
|
|
369
369
|
// The bug this pins shut: falling back to the invocation cwd ran the remaining checks against
|
|
@@ -381,7 +381,7 @@ describe('doctor — an ambiguous project is terminal, exactly as it is for chec
|
|
|
381
381
|
it('still runs normally once BASALT_CWD picks a package', () => {
|
|
382
382
|
ambiguousWorkspace()
|
|
383
383
|
const { code, log } = capture(() => doctor(join(dir, 'apps/a')))
|
|
384
|
-
expect(log).not.toContain('
|
|
384
|
+
expect(log).not.toContain('packages below it carry one')
|
|
385
385
|
expect(code).toBe(1) // no install / no manifest there — but the checks RAN
|
|
386
386
|
expect(log).toContain('guard-scan')
|
|
387
387
|
})
|
|
@@ -945,7 +945,7 @@ describe('sync — a scaffold is init’s decision, never a drift refresh’s',
|
|
|
945
945
|
ambiguousWorkspace()
|
|
946
946
|
const { code, log } = capture(() => sync({}, dir))
|
|
947
947
|
expect(code).toBe(1)
|
|
948
|
-
expect(log).toContain('2
|
|
948
|
+
expect(log).toContain('2 packages below it carry one')
|
|
949
949
|
expect(log).toContain('BASALT_CWD')
|
|
950
950
|
})
|
|
951
951
|
|
|
@@ -1043,3 +1043,170 @@ describe('doctor — exit status per outcome, not per printed string', () => {
|
|
|
1043
1043
|
expect(capture(() => doctor(dir, ['--tokens-only', '--framework'])).code).toBe(1)
|
|
1044
1044
|
})
|
|
1045
1045
|
})
|
|
1046
|
+
|
|
1047
|
+
// ── the project resolver, for a repo that declares no workspaces ───────────────────────────────
|
|
1048
|
+
//
|
|
1049
|
+
// linewatch keeps its whole basalt consumer in `web/` and declares no `workspaces` field anywhere.
|
|
1050
|
+
// Measured at its repo root, all three project-scoped commands got that wrong in a different way:
|
|
1051
|
+
// `check-theme` printed `✓ no off-palette colors` having scanned ZERO files, `doctor` inferred
|
|
1052
|
+
// `tokens-only` for a full Mantine consumer, and `sync` refused while naming `basalt-ui init` as
|
|
1053
|
+
// the remedy — the one command that WOULD have written a competing install at the root. The
|
|
1054
|
+
// workspace-declaration path already handled image-share's `apps/admin`; nothing handled this.
|
|
1055
|
+
|
|
1056
|
+
/** linewatch's shape: no `workspaces` field, the whole consumer one directory down. */
|
|
1057
|
+
function undeclaredChild(child = 'web'): void {
|
|
1058
|
+
write('package.json', JSON.stringify({ name: 'root' }))
|
|
1059
|
+
write(`${child}/package.json`, JSON.stringify({ name: child, basalt: { roots: ['src'] } }))
|
|
1060
|
+
write(`${child}/src/app.tsx`, "export const c = '#ff0000'\n")
|
|
1061
|
+
write(join(child, MANIFEST_PATH), JSON.stringify({ version: 1, files: {} }))
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
describe('resolveProjectDir — a repo that declares no workspaces still has a layout', () => {
|
|
1065
|
+
it('sync relocates into the undeclared child instead of refusing with init as the advice', () => {
|
|
1066
|
+
undeclaredChild()
|
|
1067
|
+
const { log } = capture(() => sync({ check: true }, dir))
|
|
1068
|
+
expect(log).toContain('running in ./web, where it lives')
|
|
1069
|
+
expect(log).not.toContain('basalt-ui init')
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
it('sync writes NOTHING at the root it was invoked from', () => {
|
|
1073
|
+
undeclaredChild()
|
|
1074
|
+
capture(() => sync({}, dir))
|
|
1075
|
+
expect(readdirSync(dir).toSorted()).toEqual(['package.json', 'web'])
|
|
1076
|
+
expect(readPkg()['basalt']).toBeUndefined()
|
|
1077
|
+
})
|
|
1078
|
+
|
|
1079
|
+
it('check-theme scans the child rather than reporting green over zero files', () => {
|
|
1080
|
+
undeclaredChild()
|
|
1081
|
+
const { code, log } = capture(() => checkTheme(dir, []))
|
|
1082
|
+
expect(log).toContain('running in ./web, where it lives')
|
|
1083
|
+
expect(log).not.toContain('no off-palette colors')
|
|
1084
|
+
expect(code).toBe(1)
|
|
1085
|
+
})
|
|
1086
|
+
|
|
1087
|
+
it('doctor reports on the child, so it cannot infer tokens-only from the empty root', () => {
|
|
1088
|
+
undeclaredChild()
|
|
1089
|
+
const { log } = capture(() => doctor(dir, []))
|
|
1090
|
+
expect(log).toContain(join(dir, 'web'))
|
|
1091
|
+
expect(log).not.toContain('profile: tokens-only')
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
it('two undeclared children are ambiguous, named, and never guessed between', () => {
|
|
1095
|
+
undeclaredChild('web')
|
|
1096
|
+
undeclaredChild('admin')
|
|
1097
|
+
const { code, log } = capture(() => sync({}, dir))
|
|
1098
|
+
expect(code).toBe(1)
|
|
1099
|
+
expect(log).toContain('2 packages below it carry one')
|
|
1100
|
+
expect(log).toContain('./admin')
|
|
1101
|
+
expect(log).toContain('./web')
|
|
1102
|
+
expect(readdirSync(dir)).not.toContain('DESIGN.md')
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
it('a declared workspaces field still wins — the layout scan is a fallback, not a union', () => {
|
|
1106
|
+
// `stray/` carries a basalt key but is not a workspace package. Treating both as candidates
|
|
1107
|
+
// would turn a working single-candidate repo into an ambiguity error on upgrade.
|
|
1108
|
+
write('package.json', JSON.stringify({ name: 'root', workspaces: ['apps/*'] }))
|
|
1109
|
+
write('apps/web/package.json', JSON.stringify({ name: 'web', basalt: { roots: ['src'] } }))
|
|
1110
|
+
write('apps/web/src/app.tsx', 'export const App = () => null\n')
|
|
1111
|
+
write(join('apps/web', MANIFEST_PATH), JSON.stringify({ version: 1, files: {} }))
|
|
1112
|
+
write('stray/package.json', JSON.stringify({ name: 'stray', basalt: { roots: ['src'] } }))
|
|
1113
|
+
const { log } = capture(() => sync({ check: true }, dir))
|
|
1114
|
+
expect(log).toContain('running in ./apps/web, where it lives')
|
|
1115
|
+
})
|
|
1116
|
+
})
|
|
1117
|
+
|
|
1118
|
+
// ── sync honours the profile doctor already reads ──────────────────────────────────────────────
|
|
1119
|
+
//
|
|
1120
|
+
// rollhook is tokens-only in both apps and declares it. `sync` told it to run `basalt-ui init` —
|
|
1121
|
+
// the exact advice `doctor`, in the same directory at the same version, exists to prevent — and
|
|
1122
|
+
// exited 1, which also put `sync --check` out of reach of the one CI drift gate every other
|
|
1123
|
+
// consumer runs.
|
|
1124
|
+
|
|
1125
|
+
describe('sync — tokens-only is a pass, not a refusal', () => {
|
|
1126
|
+
function tokensOnly(): void {
|
|
1127
|
+
write(
|
|
1128
|
+
'package.json',
|
|
1129
|
+
JSON.stringify({ name: 'marketing', basalt: { roots: ['src'], profile: 'tokens-only' } }),
|
|
1130
|
+
)
|
|
1131
|
+
write('src/app.ts', 'export const c = 1\n')
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
it('reports n/a and exits 0, mirroring doctor’s manifest row', () => {
|
|
1135
|
+
tokensOnly()
|
|
1136
|
+
const { code, log } = capture(() => sync({}, dir))
|
|
1137
|
+
expect(code).toBe(0)
|
|
1138
|
+
expect(log).toContain('n/a — a tokens-only consumer has no scaffold to reconcile')
|
|
1139
|
+
expect(log).not.toContain('basalt-ui init')
|
|
1140
|
+
})
|
|
1141
|
+
|
|
1142
|
+
it('is wirable into CI: --check exits 0 instead of failing on a scaffold it must not have', () => {
|
|
1143
|
+
tokensOnly()
|
|
1144
|
+
expect(capture(() => sync({ check: true, flags: [] }, dir)).code).toBe(0)
|
|
1145
|
+
})
|
|
1146
|
+
|
|
1147
|
+
it('writes nothing — no manifest, no DESIGN.md, no basalt.roots backfill', () => {
|
|
1148
|
+
tokensOnly()
|
|
1149
|
+
capture(() => sync({}, dir))
|
|
1150
|
+
expect(readdirSync(dir).toSorted()).toEqual(['package.json', 'src'])
|
|
1151
|
+
})
|
|
1152
|
+
|
|
1153
|
+
it('--framework forces the full profile back on', () => {
|
|
1154
|
+
tokensOnly()
|
|
1155
|
+
const { code, log } = capture(() => sync({ flags: ['--framework'] }, dir))
|
|
1156
|
+
expect(code).toBe(1)
|
|
1157
|
+
expect(log).toContain('refusing to scaffold')
|
|
1158
|
+
})
|
|
1159
|
+
|
|
1160
|
+
it('contradictory profile flags exit 1, exactly as they do for check-theme and doctor', () => {
|
|
1161
|
+
tokensOnly()
|
|
1162
|
+
expect(capture(() => sync({ flags: ['--tokens-only', '--framework'] }, dir)).code).toBe(1)
|
|
1163
|
+
})
|
|
1164
|
+
})
|
|
1165
|
+
|
|
1166
|
+
// ── no shipped artifact carries a version number that goes stale ───────────────────────────────
|
|
1167
|
+
//
|
|
1168
|
+
// `DESIGN.md` is a seed: written once at init, then consumer-owned and never reconciled. Its
|
|
1169
|
+
// opener stamped `{{BASALT_VERSION}}`, so across the seven consumers the same line read 1.0.0,
|
|
1170
|
+
// 1.9.0, 1.21.0 and 1.22.0 under an identical 1.22.0 install — reported three rounds running as
|
|
1171
|
+
// four separate doc bugs. It was one bug: a version number in a file nothing ever rewrites.
|
|
1172
|
+
|
|
1173
|
+
describe('DESIGN.md carries no version, and sync heals the ones already written', () => {
|
|
1174
|
+
function seeded(): void {
|
|
1175
|
+
write('package.json', JSON.stringify({ name: 'fixture', basalt: { roots: ['src'] } }))
|
|
1176
|
+
write('src/app.tsx', 'export const App = () => null\n')
|
|
1177
|
+
capture(() => init(dir))
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
it('a fresh scaffold stamps no version at all', () => {
|
|
1181
|
+
seeded()
|
|
1182
|
+
expect(read('DESIGN.md')).not.toMatch(/Managed by basalt-ui \(\d+\.\d+\.\d+\)/)
|
|
1183
|
+
expect(read('DESIGN.md')).toContain('run `basalt-ui doctor` for the version')
|
|
1184
|
+
})
|
|
1185
|
+
|
|
1186
|
+
it('sync rewrites a stale opener in place and leaves the rest of the file alone', () => {
|
|
1187
|
+
seeded()
|
|
1188
|
+
const consumerOwned = '\n## Series\n\nOurs: hrv, rhr.\n'
|
|
1189
|
+
write('DESIGN.md', `# app — Design\n\n> Managed by basalt-ui (1.9.0). Thin.\n${consumerOwned}`)
|
|
1190
|
+
capture(() => sync({}, dir))
|
|
1191
|
+
expect(read('DESIGN.md')).toContain(
|
|
1192
|
+
'> Managed by basalt-ui — run `basalt-ui doctor` for the version. Thin.',
|
|
1193
|
+
)
|
|
1194
|
+
expect(read('DESIGN.md')).toContain(consumerOwned)
|
|
1195
|
+
})
|
|
1196
|
+
|
|
1197
|
+
it('--check writes nothing, including the heal', () => {
|
|
1198
|
+
seeded()
|
|
1199
|
+
const stale = '# app — Design\n\n> Managed by basalt-ui (1.9.0). Thin.\n'
|
|
1200
|
+
write('DESIGN.md', stale)
|
|
1201
|
+
capture(() => sync({ check: true }, dir))
|
|
1202
|
+
expect(read('DESIGN.md')).toBe(stale)
|
|
1203
|
+
})
|
|
1204
|
+
|
|
1205
|
+
it('leaves an opener the consumer already rewrote by hand untouched', () => {
|
|
1206
|
+
seeded()
|
|
1207
|
+
const owned = '# app — Design\n\n> Our own words entirely.\n'
|
|
1208
|
+
write('DESIGN.md', owned)
|
|
1209
|
+
capture(() => sync({}, dir))
|
|
1210
|
+
expect(read('DESIGN.md')).toBe(owned)
|
|
1211
|
+
})
|
|
1212
|
+
})
|
|
@@ -104,11 +104,15 @@ describe('check-theme --audit-allows — theme-allow annotations', () => {
|
|
|
104
104
|
})
|
|
105
105
|
|
|
106
106
|
it('names the scope it audited, so "0 dead" is never read as "0 dead anywhere"', () => {
|
|
107
|
-
// The audit reads exactly what check-theme reads
|
|
108
|
-
//
|
|
107
|
+
// The audit reads exactly what check-theme reads, and that is WIDER than `basalt.roots` — two
|
|
108
|
+
// consumers reported a live `public/site.webmanifest` waiver under a scope line claiming
|
|
109
|
+
// files outside roots are not audited. The line has to name every class the scan reaches, or
|
|
110
|
+
// it is a second false statement in the sentence that exists to prevent one.
|
|
109
111
|
fixture("export const c = '#ff0000' // theme-allow raw-hex — brand literal\n")
|
|
110
112
|
const { log } = audit()
|
|
111
|
-
expect(log).toContain('Scope: the 1 file(s) check-theme scans under basalt.roots
|
|
113
|
+
expect(log).toContain('Scope: the 1 file(s) check-theme scans — everything under basalt.roots')
|
|
114
|
+
expect(log).toContain('sibling index.html and public/ tree')
|
|
115
|
+
expect(log).toContain('basalt.include')
|
|
112
116
|
})
|
|
113
117
|
|
|
114
118
|
it('marks an unaccountable waiver while still reporting what it covers', () => {
|