basalt-ui 1.21.0 → 1.22.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.
@@ -253,7 +253,8 @@ const MAX_CSS_CONTINUATION_LINES = 8;
253
253
  const MAX_COMMENT_BLOCK_LINES = 8;
254
254
  const COMMENT_OPENER_BEFORE = /(?:\/\/|\/\*|<!--)\s*$/;
255
255
  const COMMENT_ONLY_LINE = /^\s*(?:\{\s*\/\*|\/\/|\/\*|\*|<!--)/;
256
- const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*|<!--|^\s*\*)\s*$/;
256
+ const JSX_COMMENT_CLOSE = /\*\/\s*\}\s*$/;
257
+ const ANNOTATION_PREFIX = /(?:^|\/\/|\/\*\*?|<!--|^\s*\*)\s*$/;
257
258
  function escapeRegExp(value) {
258
259
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
259
260
  }
@@ -300,7 +301,8 @@ function collectAllowAnnotations(lines, codeLines, allowComment, syntax) {
300
301
  for (let i = 0; i < lines.length; i++) {
301
302
  const line = lines[i] ?? "";
302
303
  const stripped = codeLines[i] ?? "";
303
- const isCommentOnlyLine = stripped.trim() === "" || COMMENT_ONLY_LINE.test(line);
304
+ const code = stripped.trim();
305
+ const isCommentOnlyLine = code === "" || COMMENT_ONLY_LINE.test(line) || code === "}" && JSX_COMMENT_CLOSE.test(line);
304
306
  if (syntax === "markup") {
305
307
  for (const m of line.matchAll(jsonKey)) {
306
308
  record(i, parseAllowAnnotation(m[2] ?? "", m[1] === void 0 ? "line" : "file"), false);
@@ -997,13 +999,49 @@ function checkSource(text, relPath, cfg) {
997
999
  severity: severityOf(f.kind, cfg)
998
1000
  }));
999
1001
  }
1002
+ function findAllowAnnotations(text, relPath, cfg) {
1003
+ const lines = text.split("\n");
1004
+ const syntax = guardSyntaxFor(relPath);
1005
+ const codeText = syntax === "markup" ? stripMarkupComments(text) : stripComments(text, syntax);
1006
+ const { declared } = collectAllowAnnotations(
1007
+ lines,
1008
+ codeText.split("\n"),
1009
+ cfg.allowComment,
1010
+ syntax
1011
+ );
1012
+ return declared.map(({ line, annotation }) => ({
1013
+ line,
1014
+ scope: annotation.scope,
1015
+ rules: [...annotation.rules],
1016
+ guardKinds: annotation.rules.filter((id) => Object.hasOwn(GUARD_RULES, id)),
1017
+ pluginRules: annotation.rules.filter(
1018
+ (id) => !Object.hasOwn(GUARD_RULES, id) && PLUGIN_RULE_IDS.has(id)
1019
+ ),
1020
+ unknownRules: [...annotation.unknownRules],
1021
+ hasReason: annotation.hasReason,
1022
+ bare: annotation.rules.length === 0 && annotation.unknownRules.length === 0,
1023
+ text: lines[line - 1] ?? ""
1024
+ }));
1025
+ }
1026
+ const NEUTRALIZED_ALLOW_TOKEN = "basalt-audit-neutralized";
1027
+ function neutralizeAllowAnnotation(text, line, cfg) {
1028
+ const lines = text.split("\n");
1029
+ const target = lines[line - 1];
1030
+ if (target === void 0) return text;
1031
+ lines[line - 1] = target.replaceAll(cfg.allowComment, NEUTRALIZED_ALLOW_TOKEN);
1032
+ return lines.join("\n");
1033
+ }
1000
1034
  export {
1001
1035
  DEFAULT_GUARD_CONFIG,
1002
1036
  GENERATED_HEADER_LINE,
1003
1037
  GUARD_RULES,
1038
+ NEUTRALIZED_ALLOW_TOKEN,
1039
+ PLUGIN_RULE_IDS,
1004
1040
  TOKENS_ONLY_DISABLED_KINDS,
1005
1041
  checkSource,
1042
+ findAllowAnnotations,
1006
1043
  guardKindRemedy,
1007
1044
  guardWaiverHint,
1045
+ neutralizeAllowAnnotation,
1008
1046
  unmatchedExemptPatterns
1009
1047
  };
package/dist/vite.d.ts CHANGED
@@ -80,7 +80,10 @@ export type BasaltAppOptions = {
80
80
  scope?: string;
81
81
  /** Manifest `id`. Default: the resolved `startUrl` (default- or explicitly-derived, per above). */
82
82
  id?: string;
83
- /** Icon links + manifest icon paths. `false` skips the head `<link>` icons entirely. */
83
+ /**
84
+ * Icon links + manifest icon paths. `false` skips BOTH — the head `<link>` icons and the
85
+ * manifest's `icons` array — so a manifest is never emitted pointing at files that 404.
86
+ */
84
87
  icons?: false | {
85
88
  dir?: string;
86
89
  };
@@ -1 +1 @@
1
- {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AA6CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAgFpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IAC/C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX,wFAAwF;IACxF,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AAigBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../src/vite.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAA6B,YAAY,EAAE,UAAU,EAAE,MAAM,MAAM,CAAA;AAI/E,MAAM,MAAM,iBAAiB,GAAG;IAC9B,4FAA4F;IAC5F,IAAI,EAAE,MAAM,CAAA;IACZ,6EAA6E;IAC7E,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;IACvB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,kFAAkF;IAClF,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAC5B,CAAA;AA6CD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,iBAAiB,GAAG,UAAU,CAgFpE;AAgBD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,gGAAgG;IAChG,IAAI,EAAE,MAAM,CAAA;IACZ,0DAA0D;IAC1D,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gGAAgG;IAChG,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,GAAG;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;IACrD;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,KAAK,CAAA;IAC/C;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,wDAAwD;IACxD,OAAO,CAAC,EAAE,YAAY,GAAG,YAAY,GAAG,YAAY,GAAG,SAAS,CAAA;IAChE;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,mGAAmG;IACnG,EAAE,CAAC,EAAE,MAAM,CAAA;IACX;;;OAGG;IACH,KAAK,CAAC,EAAE,KAAK,GAAG;QAAE,GAAG,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IAChC,8FAA8F;IAC9F,UAAU,CAAC,EAAE,MAAM,GAAG,KAAK,CAAA;IAC3B,oFAAoF;IACpF,GAAG,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,SAAS,GAAG,qBAAqB,CAAA;KAAE,CAAA;IACvF,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,KAAK,CAAA;IAChB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAClD,CAAA;AA4gBD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,gBAAgB,GAAG,YAAY,EAAE,CAMzE"}
package/dist/vite.js CHANGED
@@ -296,6 +296,7 @@ function buildManifestJson(input) {
296
296
  display,
297
297
  schemeColor,
298
298
  backgroundColor,
299
+ iconsEnabled,
299
300
  iconsDir,
300
301
  base
301
302
  } = input;
@@ -311,19 +312,25 @@ function buildManifestJson(input) {
311
312
  // `<meta name="theme-color">` pair. Unchanged for the default (`'dark'`) and for `'auto'`.
312
313
  theme_color: schemeColor,
313
314
  background_color: backgroundColor,
314
- icons: [
315
- {
316
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest192)),
317
- sizes: "192x192",
318
- type: "image/png"
319
- },
320
- {
321
- src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
322
- sizes: "512x512",
323
- type: "image/png",
324
- purpose: "any maskable"
325
- }
326
- ]
315
+ // `icons: false` reaches HERE too, not just the head links. The array was unconditional, so
316
+ // `{ manifest: true, icons: false }` shipped a manifest pointing at two PNGs the app never
317
+ // ships — an installable app with broken icons, and the reason rb went hybrid (plugin for the
318
+ // head, a hand-written manifest for the rest). A manifest with NO icons member is valid.
319
+ ...iconsEnabled ? {
320
+ icons: [
321
+ {
322
+ src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest192)),
323
+ sizes: "192x192",
324
+ type: "image/png"
325
+ },
326
+ {
327
+ src: withBase(base, withIconPath(iconsDir, ICON_FILES.manifest512)),
328
+ sizes: "512x512",
329
+ type: "image/png",
330
+ purpose: "any maskable"
331
+ }
332
+ ]
333
+ } : {}
327
334
  };
328
335
  return JSON.stringify(manifest, null, 2);
329
336
  }
@@ -364,6 +371,7 @@ function createMainPlugin(options) {
364
371
  display,
365
372
  schemeColor,
366
373
  backgroundColor,
374
+ iconsEnabled,
367
375
  iconsDir,
368
376
  base
369
377
  });
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # basalt-ui v1.21.0
1
+ # basalt-ui v1.22.0
2
2
  # Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets
3
3
  #
4
4
  # basalt-ui is an opinionated framework for Mantine-based React apps.
@@ -19,7 +19,7 @@
19
19
  # ./tokens.css subpath. Guide: docs/FRAMEWORK-FREE.md in the repo.
20
20
 
21
21
  Package: basalt-ui
22
- Version: 1.21.0
22
+ Version: 1.22.0
23
23
 
24
24
  ## basalt-ui
25
25
  Import: import { ... } from 'basalt-ui'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "basalt-ui",
3
- "version": "1.21.0",
3
+ "version": "1.22.0",
4
4
  "description": "Opinionated framework for Mantine-based React apps — theme, app shell, visx charts, toolchain presets",
5
5
  "keywords": [
6
6
  "charts",
@@ -742,3 +742,38 @@ describe('severity — warnings report, errors fail', () => {
742
742
  expect(err).toContain('✖ Theme guard')
743
743
  })
744
744
  })
745
+
746
+ // ── the manifest waiver hint, gated on the profile ────────────────────────────────────────────
747
+ //
748
+ // rollhook. The hint leads with `basaltAppPlugin`, which is right for a Mantine app — a hex in a
749
+ // manifest can never be right, and the plugin removes the hand-copy entirely. A `tokens-only`
750
+ // consumer has definitionally opted out of that layer: rollhook's Astro site has no index.html to
751
+ // transform, imports no basalt JavaScript at all, and owns maskable icons the plugin does not emit.
752
+ // Leading with it there is advice the consumer cannot take.
753
+
754
+ describe('check-theme — the manifest waiver hint follows the profile', () => {
755
+ function manifestFixture(basalt: Record<string, unknown>): void {
756
+ writeFileSync(resolve(dir, 'package.json'), JSON.stringify({ name: 'fixture', basalt }))
757
+ mkdirSync(resolve(dir, 'src'), { recursive: true })
758
+ writeFileSync(resolve(dir, 'src', 'app.tsx'), 'export const App = () => null\n')
759
+ mkdirSync(resolve(dir, 'public'), { recursive: true })
760
+ writeFileSync(
761
+ resolve(dir, 'public', 'site.webmanifest'),
762
+ JSON.stringify({ name: 'x', theme_color: '#18181b' }, null, 2),
763
+ )
764
+ }
765
+
766
+ it('offers basaltAppPlugin first to a framework consumer', () => {
767
+ manifestFixture({ roots: ['src'] })
768
+ const { err } = run()
769
+ expect(err).toContain('let basaltAppPlugin emit it')
770
+ })
771
+
772
+ it('leads with the member for a tokens-only consumer, and says which remedy it withheld', () => {
773
+ manifestFixture({ roots: ['src'], profile: 'tokens-only' })
774
+ const { err } = run()
775
+ expect(err).toContain('JSON has no comments — declare a deliberate exception')
776
+ expect(err).not.toContain('let basaltAppPlugin emit it')
777
+ expect(err).toContain('a tokens-only consumer has opted out of')
778
+ })
779
+ })
@@ -4,7 +4,7 @@
4
4
  * matched `cmd === 'sync'` first and `--help` was just another item in `flags`, ignored).
5
5
  */
6
6
  import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
7
- import { existsSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
7
+ import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
8
8
  import { tmpdir } from 'node:os'
9
9
  import { resolve } from 'node:path'
10
10
 
@@ -86,6 +86,10 @@ describe('run() --help short-circuits before dispatch', () => {
86
86
  })
87
87
 
88
88
  it('`sync` without --help still runs for real and writes the manifest (control case)', () => {
89
+ // `sync` refuses to scaffold where nothing was ever installed, so the control case has to be a
90
+ // repo that HAS an install — which is what `sync` is for.
91
+ mkdirSync(resolve(dir, '.basalt'), { recursive: true })
92
+ writeFileSync(resolve(dir, MANIFEST_PATH), JSON.stringify({ version: 1, files: {} }))
89
93
  const { code } = capture(() => run(['sync'], dir))
90
94
  expect(code).toBe(0)
91
95
  expect(existsSync(resolve(dir, MANIFEST_PATH))).toBe(true)