basalt-ui 1.20.0 → 1.21.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/AGENTS.md +9 -5
- package/CHANGELOG.md +19 -0
- package/MIGRATING.md +147 -49
- package/README.md +42 -8
- package/agent/rules/basalt-app.md +41 -0
- package/agent/rules/basalt-charts.md +15 -12
- package/agent/rules/basalt-content.md +4 -0
- package/agent/rules/basalt-router.md +57 -5
- package/agent/rules/basalt-state.md +21 -3
- package/agent/rules/basalt-tokens.md +73 -16
- package/agent/skills/basalt-app/SKILL.md +4 -1
- package/agent/skills/basalt-charts/SKILL.md +5 -3
- package/agent/templates/CLAUDE-block.md.tpl +11 -5
- package/configs/lefthook.yml +48 -15
- package/configs/oxlint-plugin.js +181 -68
- package/configs/oxlint-plugin.test.ts +215 -9
- package/dist/charts/kinds/Bars.d.ts +1 -1
- package/dist/charts/kinds/Bars.d.ts.map +1 -1
- package/dist/charts/kinds/Donut.d.ts +1 -1
- package/dist/charts/kinds/Donut.d.ts.map +1 -1
- package/dist/charts/kinds/DualPanel.d.ts +1 -1
- package/dist/charts/kinds/DualPanel.d.ts.map +1 -1
- package/dist/charts/kinds/Heatmap.d.ts +1 -1
- package/dist/charts/kinds/Heatmap.d.ts.map +1 -1
- package/dist/charts/kinds/MultiLine.d.ts +1 -1
- package/dist/charts/kinds/MultiLine.d.ts.map +1 -1
- package/dist/charts/kinds/StackedArea.d.ts +1 -1
- package/dist/charts/kinds/StackedArea.d.ts.map +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts +1 -1
- package/dist/charts/kinds/ZonedLine.d.ts.map +1 -1
- package/dist/cli/index.d.ts +48 -14
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +328 -66
- package/dist/cli/waiver-audit.test.d.ts +2 -0
- package/dist/cli/waiver-audit.test.d.ts.map +1 -0
- package/dist/guard/index.d.ts +47 -2
- package/dist/guard/index.d.ts.map +1 -1
- package/dist/guard/index.js +176 -31
- package/dist/router-tanstack/multi-search-param-store.d.ts +13 -0
- package/dist/router-tanstack/multi-search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/multi-search-param-store.js +4 -1
- package/dist/router-tanstack/search-param-store.d.ts +29 -5
- package/dist/router-tanstack/search-param-store.d.ts.map +1 -1
- package/dist/router-tanstack/search-param-store.js +31 -3
- package/dist/router-tanstack/search-param-store.test.d.ts +2 -0
- package/dist/router-tanstack/search-param-store.test.d.ts.map +1 -0
- package/dist/shell/index.d.ts +12 -5
- package/dist/shell/index.d.ts.map +1 -1
- package/dist/shell/index.js +24 -6
- package/dist/tokens/index.d.ts +3 -1
- package/dist/tokens/index.d.ts.map +1 -1
- package/dist/tokens/index.js +5 -1
- package/dist/tokens/palette.d.ts +2 -2
- package/dist/tokens/palette.js +2 -2
- package/dist/tokens/text-scale.test.d.ts +2 -0
- package/dist/tokens/text-scale.test.d.ts.map +1 -0
- package/dist/tokens.css +4 -2
- package/dist/vite.d.ts +24 -2
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +54 -13
- package/llms.txt +2 -2
- package/package.json +1 -1
- package/src/charts/kinds/Bars.tsx +1 -1
- package/src/charts/kinds/Donut.tsx +1 -1
- package/src/charts/kinds/DualPanel.tsx +2 -2
- package/src/charts/kinds/Heatmap.tsx +1 -1
- package/src/charts/kinds/MultiLine.tsx +1 -1
- package/src/charts/kinds/StackedArea.tsx +1 -1
- package/src/charts/kinds/ZonedLine.tsx +1 -1
- package/src/cli/index.ts +607 -128
- package/src/cli/init-sync.test.ts +2 -0
- package/src/cli/template-ledger.test.ts +1 -1
- package/src/cli/toolchain-wiring.test.ts +162 -0
- package/src/cli/waiver-audit.test.ts +131 -0
- package/src/guard/check-source.test.ts +441 -1
- package/src/guard/index.ts +430 -60
- package/src/router-tanstack/multi-search-param-store.ts +16 -1
- package/src/router-tanstack/search-param-store.test.ts +135 -0
- package/src/router-tanstack/search-param-store.ts +90 -8
- package/src/shell/app-mobile-nav.tsx +1 -1
- package/src/shell/index.test.tsx +52 -0
- package/src/shell/index.tsx +64 -13
- package/src/tokens/index.ts +24 -1
- package/src/tokens/palette.ts +2 -2
- package/src/tokens/text-scale.test.ts +51 -0
- package/src/vite.test.ts +125 -6
- package/src/vite.ts +148 -20
package/configs/oxlint-plugin.js
CHANGED
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
* `./oxlint-plugin.js` — a consumer `extends`-ing that preset inherits `jsPlugins` too, and the
|
|
10
10
|
* path resolves relative to the preset file, not the consumer's own config.
|
|
11
11
|
*
|
|
12
|
-
* The
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The design-guard rules below support the same `theme-allow` escape as `src/guard`, and the same
|
|
13
|
+
* two-scope grammar: `theme-allow [<id>…] [— <why>]` at the START of a comment on the reported
|
|
14
|
+
* node's own line or the line above skips THAT node; `theme-allow-file <id>… — <why>` anywhere in
|
|
15
|
+
* the file skips every node of the named rules in it. Both halves are load-bearing — see
|
|
16
|
+
* {@link parseThemeAllows} for why the prefix matters and {@link hasFileDeclaration} for why file
|
|
17
|
+
* scope has to be spelled. The three boundary rules deliberately honour neither — see their own
|
|
18
|
+
* comment for why.
|
|
16
19
|
*
|
|
17
20
|
* ── The two agent-chat guard rules (`agent-resume-guard` / `agent-no-raw-usechat`) + `ai-sdk-major` ──
|
|
18
21
|
* These are also correctness/architecture boundaries, not design guidance, so they do NOT honour
|
|
@@ -141,27 +144,30 @@ const ALLOW_REASON_SEPARATOR = /^(?:—|–|-{1,2}|:)\s*/
|
|
|
141
144
|
const MIN_ALLOW_REASON_LENGTH = 4
|
|
142
145
|
|
|
143
146
|
/**
|
|
144
|
-
*
|
|
147
|
+
* The annotation token, with its optional `-file` suffix.
|
|
145
148
|
*
|
|
146
|
-
* `
|
|
147
|
-
* `
|
|
148
|
-
*
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*
|
|
154
|
-
* annotation in the wild already writes it. Mirrors `parseAllowAnnotation` in `src/guard/index.ts`.
|
|
149
|
+
* `(?![\w-])` keeps the two forms apart AND stops a longer word from parsing as the bare form:
|
|
150
|
+
* `theme-allow-unscoped` — the KIND NAME, written in prose constantly — used to consume no id, fall
|
|
151
|
+
* through to `rules: []` and read as a blanket waiver.
|
|
152
|
+
*/
|
|
153
|
+
const ALLOW_TOKEN = /theme-allow(-file)?(?![\w-])/
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Everything permitted between the start of a line INSIDE the comment and the token.
|
|
155
157
|
*
|
|
156
|
-
* The
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
*
|
|
158
|
+
* The annotation has to START its comment (or a line of one — the `*` is a block-comment gutter).
|
|
159
|
+
* Without this the parser did a bare `indexOf('theme-allow')`, so a comment that merely MENTIONED
|
|
160
|
+
* the token in prose parsed as the blanket form and switched every rule off on the node below it.
|
|
161
|
+
* linewatch documented its own waivers in a docblock and thereby disarmed the file: a false
|
|
162
|
+
* NEGATIVE, and worse than the whole-file hole 1.20.0 closed. `src/guard`'s `ANNOTATION_PREFIX` is
|
|
163
|
+
* the same test against raw source text (which still carries its `//` / `/*` opener; a comment
|
|
164
|
+
* NODE's `value` does not, hence the two spellings).
|
|
160
165
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
const ALLOW_LINE_PREFIX = /^\s*\*?\s*$/
|
|
167
|
+
|
|
168
|
+
/** Parse the text following the token — ids, then the reason. See {@link parseThemeAllows}. */
|
|
169
|
+
function parseAllowBody(rest, scope) {
|
|
170
|
+
let remainder = rest.replace(/^[\s,]+/, '')
|
|
165
171
|
const rules = []
|
|
166
172
|
const unknownRules = []
|
|
167
173
|
let inIdSlot = true
|
|
@@ -178,48 +184,105 @@ function parseThemeAllow(commentValue) {
|
|
|
178
184
|
inIdSlot = /^\s*,/.test(after)
|
|
179
185
|
}
|
|
180
186
|
const reason = remainder.replace(ALLOW_REASON_SEPARATOR, '').trim()
|
|
181
|
-
return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH }
|
|
187
|
+
return { rules, unknownRules, hasReason: reason.length >= MIN_ALLOW_REASON_LENGTH, scope }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Every annotation one comment carries. A docblock can hold more than one, so this returns a list.
|
|
192
|
+
*
|
|
193
|
+
* The grammar, and it is the same one `src/guard` parses:
|
|
194
|
+
*
|
|
195
|
+
* ```text
|
|
196
|
+
* theme-allow → this node, EVERY rule (the legacy bare form)
|
|
197
|
+
* theme-allow <id>[, <id>…] [— <why>] → this node, those rules
|
|
198
|
+
* theme-allow-file <id>[, <id>…] — <why> → the WHOLE FILE, those rules
|
|
199
|
+
* ```
|
|
200
|
+
*
|
|
201
|
+
* `{ rules: [], unknownRules: [] }` is the bare form; a non-empty `rules` scopes the exception to
|
|
202
|
+
* exactly those ids. A word that occupies the id slot but names no known rule lands in
|
|
203
|
+
* `unknownRules` and FAILS CLOSED — the annotation then waives only the ids it got right, never
|
|
204
|
+
* everything. `theme-allow raw-hexx — reason` used to consume no id, fall through to the
|
|
205
|
+
* empty-`rules` branch and be read as the blanket form, so one mistyped character escalated a
|
|
206
|
+
* scoped waiver into a whole-line one, i.e. weaker than before the scoping existed.
|
|
207
|
+
*
|
|
208
|
+
* A prose reason is therefore introduced with a separator (`—`, `–`, `-`, `:`), which is how every
|
|
209
|
+
* annotation in the wild already writes it.
|
|
210
|
+
*
|
|
211
|
+
* The id slot closes at the first space that no comma opened: the first word is always a claimed
|
|
212
|
+
* id (that is where a typo lands), but after a resolved id only a `,` keeps the list open, so
|
|
213
|
+
* `theme-allow raw-surface sub-scale legend corner` reads `sub-scale` as prose rather than as a
|
|
214
|
+
* typo. See the guard's copy for the full reasoning.
|
|
215
|
+
*/
|
|
216
|
+
function parseThemeAllows(commentValue) {
|
|
217
|
+
const out = []
|
|
218
|
+
for (const rawLine of commentValue.split('\n')) {
|
|
219
|
+
const token = ALLOW_TOKEN.exec(rawLine)
|
|
220
|
+
if (token === null) continue
|
|
221
|
+
if (!ALLOW_LINE_PREFIX.test(rawLine.slice(0, token.index))) continue
|
|
222
|
+
const rest = rawLine.slice(token.index + token[0].length)
|
|
223
|
+
out.push(parseAllowBody(rest, token[1] === undefined ? 'line' : 'file'))
|
|
224
|
+
}
|
|
225
|
+
return out
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Does one annotation cover `ruleId`?
|
|
230
|
+
*
|
|
231
|
+
* Only a BARE **node** annotation covers every rule. A bare `theme-allow-file` covers nothing: the
|
|
232
|
+
* blanket form is tolerable on one line, where a reader sees what it sits on; over a whole file it
|
|
233
|
+
* is a config exemption without the config review, so the widest waiver in the contract is the one
|
|
234
|
+
* that has to name what it waives.
|
|
235
|
+
*/
|
|
236
|
+
function allowCovers(allow, ruleId) {
|
|
237
|
+
if (allow.rules.includes(ruleId)) return true
|
|
238
|
+
if (allow.scope === 'file') return false
|
|
239
|
+
return allow.rules.length === 0 && allow.unknownRules.length === 0
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function allComments(context) {
|
|
243
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode?.()
|
|
244
|
+
return sourceCode?.getAllComments?.() ?? []
|
|
182
245
|
}
|
|
183
246
|
|
|
184
247
|
/**
|
|
185
|
-
* True when a `theme-allow` covering `ruleId` sits on `node`'s own source line or the
|
|
248
|
+
* True when a NODE-scoped `theme-allow` covering `ruleId` sits on `node`'s own source line or the
|
|
249
|
+
* line above — or when the file carries a `theme-allow-file` declaration for it.
|
|
186
250
|
*
|
|
187
251
|
* The `ruleId` argument is what stops one exemption from being a blanket one: a `theme-allow
|
|
188
252
|
* raw-hex — …` written for a color no longer silently switches off `card-inset` on the same line.
|
|
189
|
-
* Only a BARE `theme-allow` still covers everything — that is the shape every existing consumer
|
|
190
|
-
* has, and `src/guard`'s `theme-allow-unscoped` is the (warning) nudge off it, not a hard break.
|
|
191
253
|
* An annotation that reached for an id and missed covers only the ids it got right.
|
|
192
254
|
*/
|
|
193
255
|
function hasThemeAllow(context, node, ruleId) {
|
|
194
|
-
|
|
195
|
-
const comments = sourceCode?.getAllComments?.() ?? []
|
|
256
|
+
if (hasFileDeclaration(context, ruleId)) return true
|
|
196
257
|
const nodeLine = node.loc.start.line
|
|
197
|
-
return
|
|
258
|
+
return allComments(context).some((comment) => {
|
|
198
259
|
if (comment.loc.end.line !== nodeLine && comment.loc.end.line !== nodeLine - 1) return false
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
return allow.rules.length === 0 && allow.unknownRules.length === 0
|
|
260
|
+
return parseThemeAllows(comment.value).some(
|
|
261
|
+
(allow) => allow.scope === 'line' && allowCovers(allow, ruleId),
|
|
262
|
+
)
|
|
203
263
|
})
|
|
204
264
|
}
|
|
205
265
|
|
|
206
266
|
/**
|
|
207
|
-
* True when the FILE carries a written declaration for `ruleId` — a `theme-allow`
|
|
208
|
-
*
|
|
267
|
+
* True when the FILE carries a written declaration for `ruleId` — a `theme-allow-file` naming it,
|
|
268
|
+
* anywhere in the file.
|
|
209
269
|
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
270
|
+
* File scope has to be SPELLED, and that is the second half of the 1.20.0 fix rather than a new
|
|
271
|
+
* idea. The first half landed: `hand-rolled-plot` stopped granting a whole file permanent immunity
|
|
272
|
+
* off any comment that happened to sit on its first assembly node, and now reports every node. The
|
|
273
|
+
* second half did not, because the promotion rule was "names a rule AND gives a reason" — which is
|
|
274
|
+
* exactly what the rule's own message asks a consumer to write, and what `theme-allow-unscoped`
|
|
275
|
+
* reports them for omitting. So there was one legal annotation shape and it was whole-file:
|
|
276
|
+
* `hasFileDeclaration` still returned early for every node, forever. linewatch ended up with one
|
|
277
|
+
* whole-file declaration per chart file — the thing 1.20.0 set out to eliminate — because per-node
|
|
278
|
+
* scoping was not expressible. Now it is: `theme-allow` is the node, `theme-allow-file` is the file.
|
|
215
279
|
*/
|
|
216
280
|
function hasFileDeclaration(context, ruleId) {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
})
|
|
281
|
+
return allComments(context).some((comment) =>
|
|
282
|
+
parseThemeAllows(comment.value).some(
|
|
283
|
+
(allow) => allow.scope === 'file' && allow.rules.includes(ruleId),
|
|
284
|
+
),
|
|
285
|
+
)
|
|
223
286
|
}
|
|
224
287
|
|
|
225
288
|
/** Test/spec files — design guidance does not apply to a fixture. Mirrors `src/cli`'s own SKIP. */
|
|
@@ -973,18 +1036,26 @@ function readPackageJson(pkgPath) {
|
|
|
973
1036
|
* that the file sits under. A declared `basalt.roots` that the file is NOT under is a positive
|
|
974
1037
|
* statement that this code is not basalt's — the walk stops there rather than continuing up to a
|
|
975
1038
|
* workspace root that happens to carry the dependency.
|
|
1039
|
+
*
|
|
1040
|
+
* **`roots` is consulted BEFORE the dependency, within one manifest**, and that ordering is the
|
|
1041
|
+
* whole fix. `init` produces exactly the layout that breaks the other way round: a monorepo ROOT
|
|
1042
|
+
* carrying `basalt-ui` as the devDependency the CLI is run from, AND `basalt.roots: ["apps/web/
|
|
1043
|
+
* src"]` naming where the app actually is. Dep-first returned in-scope on that root before `roots`
|
|
1044
|
+
* was ever read, so rb's `apps/api` — no basalt dependency, deliberately on a different `ai` major,
|
|
1045
|
+
* explicitly outside the declared roots — took three errors and needed a permanent override. The
|
|
1046
|
+
* more specific statement wins: `roots` says WHERE, the dependency only says whether.
|
|
976
1047
|
*/
|
|
977
1048
|
function isBasaltScopedFile(filename) {
|
|
978
1049
|
let dir = dirname(filename)
|
|
979
1050
|
for (;;) {
|
|
980
1051
|
const pkg = readPackageJson(resolvePath(dir, 'package.json'))
|
|
981
1052
|
if (pkg !== null) {
|
|
982
|
-
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }
|
|
983
|
-
if (deps['basalt-ui'] !== undefined) return true
|
|
984
1053
|
const roots = pkg.basalt?.roots
|
|
985
1054
|
if (Array.isArray(roots) && roots.length > 0) {
|
|
986
1055
|
return roots.some((root) => filename.startsWith(`${resolvePath(dir, root)}/`))
|
|
987
1056
|
}
|
|
1057
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies }
|
|
1058
|
+
if (deps['basalt-ui'] !== undefined) return true
|
|
988
1059
|
}
|
|
989
1060
|
const parent = dirname(dir)
|
|
990
1061
|
if (parent === dir) return false
|
|
@@ -1093,7 +1164,9 @@ const HAND_ROLLED_PLOT_MESSAGE =
|
|
|
1093
1164
|
'owns the measured margins, both y scales, the axes, grid, shared cursor, crosshair and ' +
|
|
1094
1165
|
'tooltip. Hand-assembling them is how two charts stop matching. Compose CartesianChart and ' +
|
|
1095
1166
|
'render only marks. A genuinely non-single-plot shape (multi-pane, radial, matrix) composes ' +
|
|
1096
|
-
'ChartFrame instead —
|
|
1167
|
+
'ChartFrame instead — declare that with `theme-allow-file hand-rolled-plot — <why>` anywhere in ' +
|
|
1168
|
+
'the file. To waive just THIS node and stay policed on the rest, write `theme-allow ' +
|
|
1169
|
+
'hand-rolled-plot — <why>` on its own line above it. (basalt/hand-rolled-plot)'
|
|
1097
1170
|
|
|
1098
1171
|
const handRolledPlot = {
|
|
1099
1172
|
meta: {
|
|
@@ -1265,19 +1338,53 @@ const PACKAGE_ROOT = resolvePath(PLUGIN_DIR, '..')
|
|
|
1265
1338
|
const COMPONENT_NAME = /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/
|
|
1266
1339
|
|
|
1267
1340
|
/**
|
|
1268
|
-
*
|
|
1269
|
-
*
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1341
|
+
* The barrels a consumer can import a component FROM. The root one is not the whole surface: a
|
|
1342
|
+
* chart-layer fork (`ChartFrame`, `CartesianChart`, `ChartCard`, …) collided with nothing, because
|
|
1343
|
+
* `./charts` is deliberately NOT re-exported from `.` — so a consumer whose forks all live in the
|
|
1344
|
+
* chart layer, which is the layer forks actually live in, could never trip the rule. It was
|
|
1345
|
+
* correctly silent there for the wrong reason.
|
|
1346
|
+
*
|
|
1347
|
+
* Enumerated rather than globbed: `dist/**\/index.d.ts` would also sweep internal directory
|
|
1348
|
+
* barrels that are not published subpaths, and a name that is not importable is not a fork of
|
|
1349
|
+
* anything. Keep this in step with the `exports` map in package.json.
|
|
1350
|
+
*/
|
|
1351
|
+
const BASALT_BARRELS = [
|
|
1352
|
+
['index.d.ts'],
|
|
1353
|
+
['charts', 'index.d.ts'],
|
|
1354
|
+
['content', 'index.d.ts'],
|
|
1355
|
+
['data', 'index.d.ts'],
|
|
1356
|
+
['forms', 'index.d.ts'],
|
|
1357
|
+
['agent', 'index.d.ts'],
|
|
1358
|
+
['commands', 'index.d.ts'],
|
|
1359
|
+
['notifications', 'index.d.ts'],
|
|
1360
|
+
['connectivity', 'index.d.ts'],
|
|
1361
|
+
]
|
|
1362
|
+
|
|
1363
|
+
/**
|
|
1364
|
+
* Every component-shaped VALUE export of basalt's published barrels, read once at module load from
|
|
1365
|
+
* the real `.d.ts` files — not a hand-maintained list that would go stale the first time an export
|
|
1366
|
+
* was renamed. `type` specifiers are skipped: a local `StatCardProps` is a normal thing to write, a
|
|
1367
|
+
* local `StatCard` is not.
|
|
1272
1368
|
*
|
|
1273
1369
|
* Empty (rule silent) when `dist` is unreadable, exactly like `BASALT_AI_MAJOR` — a lint run must
|
|
1274
1370
|
* not depend on a build having happened.
|
|
1275
1371
|
*/
|
|
1276
|
-
const
|
|
1372
|
+
const BASALT_EXPORTS = (() => {
|
|
1277
1373
|
const names = new Set()
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1374
|
+
for (const segments of BASALT_BARRELS) {
|
|
1375
|
+
let text
|
|
1376
|
+
try {
|
|
1377
|
+
text = readFileSync(resolvePath(PACKAGE_ROOT, 'dist', ...segments), 'utf8')
|
|
1378
|
+
} catch {
|
|
1379
|
+
continue
|
|
1380
|
+
}
|
|
1381
|
+
for (const block of text.matchAll(/export\s*\{([^}]*)\}(?:\s*from\s*['"]([^'"]*)['"])?/g)) {
|
|
1382
|
+
// A pass-through re-export of a THIRD-PARTY name is not a basalt composite. `./charts`
|
|
1383
|
+
// re-exports `Bar`, `Line`, `Pie`, `AreaClosed` … straight from `@visx/shape`, and a local
|
|
1384
|
+
// `Bar` is not a fork of anything basalt wrote — the playground's own demo tripped on it the
|
|
1385
|
+
// moment this rule started reading the charts barrel. Relative sources only.
|
|
1386
|
+
const source = block[2]
|
|
1387
|
+
if (source !== undefined && !source.startsWith('.')) continue
|
|
1281
1388
|
for (const raw of block[1].split(',')) {
|
|
1282
1389
|
const part = raw.trim()
|
|
1283
1390
|
if (part === '' || part.startsWith('type ')) continue
|
|
@@ -1285,8 +1392,6 @@ const BASALT_ROOT_EXPORTS = (() => {
|
|
|
1285
1392
|
if (COMPONENT_NAME.test(name)) names.add(name)
|
|
1286
1393
|
}
|
|
1287
1394
|
}
|
|
1288
|
-
} catch {
|
|
1289
|
-
return names
|
|
1290
1395
|
}
|
|
1291
1396
|
return names
|
|
1292
1397
|
})()
|
|
@@ -1296,7 +1401,10 @@ function shadowBasaltExportMessage(name) {
|
|
|
1296
1401
|
`Local component '${name}' collides with the basalt-ui export of the same name — import it ` +
|
|
1297
1402
|
"from 'basalt-ui' instead of re-rolling it. This is the one signal a forked composite emits: " +
|
|
1298
1403
|
'the palette guard can never see it, because a fork written by a token-fluent author uses ' +
|
|
1299
|
-
'exactly the right tokens and passes every other check.
|
|
1404
|
+
'exactly the right tokens and passes every other check. A NAME collision is the whole of the ' +
|
|
1405
|
+
'signal, so this is a tripwire, not coverage: rename the fork and the rule goes quiet ' +
|
|
1406
|
+
"(linewatch's forks are `Cell` and `Box`, rb's is `Stat`). Silence here is not evidence that " +
|
|
1407
|
+
'nothing is forked. (basalt/shadow-basalt-export)'
|
|
1300
1408
|
)
|
|
1301
1409
|
}
|
|
1302
1410
|
|
|
@@ -1318,11 +1426,11 @@ const shadowBasaltExport = {
|
|
|
1318
1426
|
create(context) {
|
|
1319
1427
|
const filename = getFilename(context)
|
|
1320
1428
|
// basalt's own source DEFINES these names; a consumer's does not.
|
|
1321
|
-
if (
|
|
1429
|
+
if (BASALT_EXPORTS.size === 0 || filename.startsWith(`${PACKAGE_ROOT}/`)) return {}
|
|
1322
1430
|
if (isTestFile(context)) return {}
|
|
1323
1431
|
|
|
1324
1432
|
const report = (node, name) => {
|
|
1325
|
-
if (typeof name !== 'string' || !
|
|
1433
|
+
if (typeof name !== 'string' || !BASALT_EXPORTS.has(name)) return
|
|
1326
1434
|
if (hasThemeAllow(context, node, 'shadow-basalt-export')) return
|
|
1327
1435
|
context.report({ node, message: shadowBasaltExportMessage(name) })
|
|
1328
1436
|
}
|
|
@@ -1356,7 +1464,8 @@ const HAND_ROLLED_SHELL_MESSAGE =
|
|
|
1356
1464
|
'bar and its More sheet, breadcrumbs, the page header and the collapse state, all driven by ONE ' +
|
|
1357
1465
|
'typed nav definition (defineNav + useNav). A hand-rolled AppShell + burger drawer is the exact ' +
|
|
1358
1466
|
'layer the 1.19 nav contract replaces, and no palette guard can see it — it uses the right ' +
|
|
1359
|
-
'tokens.
|
|
1467
|
+
'tokens. Declare a deliberate exception with `theme-allow-file hand-rolled-shell — <why>`. ' +
|
|
1468
|
+
'(basalt/hand-rolled-shell)'
|
|
1360
1469
|
|
|
1361
1470
|
/**
|
|
1362
1471
|
* The `hand-rolled-plot` shape, one layer up. rb shipped 266 lines of `AppShell` + `NavLink` +
|
|
@@ -1429,15 +1538,19 @@ const handRolledShell = {
|
|
|
1429
1538
|
*/
|
|
1430
1539
|
export const PLUGIN_RULE_GRACE = {
|
|
1431
1540
|
'hand-rolled-plot':
|
|
1432
|
-
'widened in the round-
|
|
1433
|
-
'
|
|
1434
|
-
'
|
|
1541
|
+
'widened AGAIN in the round-5 guard minor (file scope must now be spelled `theme-allow-file`, ' +
|
|
1542
|
+
'so the named-with-a-reason shape every consumer wrote at 1.20.0 is a NODE waiver and the ' +
|
|
1543
|
+
'remaining assembly nodes report) — grace restarts with the widening, per the "a rule the ' +
|
|
1544
|
+
'current minor widens does not promote in that minor" doctrine. Promote one minor after ' +
|
|
1545
|
+
'consumers have moved their file declarations to `theme-allow-file`.',
|
|
1435
1546
|
'chart-legend-literal':
|
|
1436
1547
|
'widened in the round-4 guard minor (a `.map()` over a non-series array now counts) — promote ' +
|
|
1437
1548
|
'to error one minor later, for the same reason as hand-rolled-plot above.',
|
|
1438
1549
|
'shadow-basalt-export':
|
|
1439
|
-
'new in the round-4 guard minor
|
|
1440
|
-
'is
|
|
1550
|
+
'new in the round-4 guard minor, widened in round-5 (reads every published barrel, not just ' +
|
|
1551
|
+
'the root one, so a chart-layer fork is visible) — promote to error one minor later, if ever. ' +
|
|
1552
|
+
'A name collision is strong evidence and not proof, and renaming defeats it outright, so this ' +
|
|
1553
|
+
'may be a permanent warn.',
|
|
1441
1554
|
'hand-rolled-shell': 'new in the round-4 guard minor — promote to error one minor later.',
|
|
1442
1555
|
}
|
|
1443
1556
|
|