fix(webui): defuse scaffold config traps, tighten inline guards

Micro-fix from re-review:

1. webui/components.json: deleted the font/iconLibrary keys instead of
   picking a value. Re-review checked shadcn-vue's actual parse schema
   (node_modules/shadcn-vue/dist/schema/index.js's rawConfigSchema):
   both fields are z.string().optional(), not an enum - the five-choice
   enum lives only on the init CLI's --font/--icon-library flags, not on
   the components.json schema. The previous commit's "schema forbids a
   fix" rationale was wrong; corrected here. Verified empirically, not
   just by reading the schema: ran a real `npx shadcn-vue add badge -o
   -y` against components.json with both keys removed - src/assets/
   main.css came back byte-for-byte unchanged (previously this exact
   class of call re-added a Google Fonts import and the .dark-class
   toggle). That same run did still re-add @lucide/vue to package.json's
   dependencies (some components' registry entries list an icon package
   regardless of iconLibrary) - reverted before committing, and
   webui/README.md's warning section rewritten to explain the omission
   (so nobody "helpfully" restores the keys), fix the schema claim, and
   note the narrower remaining package.json trap.

2. admin.go:27-28: adminCSP's doc comment now names all three set-sites
   (GET /admin, every GET /admin/assets/*, the three JSON routes) -
   previously only mentioned the page and JSON routes, dropped after the
   asset route gained the same header in the prior fix round.

3. Both inline-script guards (webui/generate.mjs's assertNoInlineAssets,
   admin_test.go's hasInlineScript) tightened from a bare \bsrc\s*=
   pattern to (^|\s)src\s*= - \b alone matches inside "data-src=" too
   (the "-" before "src" is a non-word character, so \b sits right
   before "src"), which would wrongly treat a data-src/nosrc attribute
   as the real src= that makes a <script> external. New Go test
   TestHasInlineScript_DataSrcIsNotExternal: <script data-src="x">
   is still flagged as inline.

No webui/src/ change this round - components.json isn't bundled, and
generate.mjs's edit only touches its own build-time guard, not the
Go-source-generation path. admin_assets_gen.go NOT regenerated
(confirmed via a throwaway `node generate.mjs` + gofmt -w run: output
is byte-identical to what's already committed).
This commit is contained in:
2026-08-21 09:54:48 +01:00
parent b98416d1e9
commit 1aa2b43932
5 changed files with 96 additions and 38 deletions
+39 -25
View File
@@ -12,33 +12,47 @@ built, baked-in output, never `webui/` itself.
## Warning: `shadcn-vue add`/`init` regressions
`components.json`'s `iconLibrary` and `font` fields are locked to
`"lucide"` and `"inter"` — **not this project's real choices** (this
project uses FontAwesome only, per `vue-web-development` house skill, and
no external font at all). The shadcn-vue CLI's schema does not accept
`"none"`/`"fontawesome"`/`"system"` for either field (checked directly:
`node_modules/shadcn-vue/dist/preset-*.js`'s `PRESET_ICON_LIBRARIES` and
`PRESET_FONTS` arrays — five fixed choices each, no escape hatch), and
JSON supports no comment syntax to warn inline (the repo's own
pre-commit JSON validator rejects JSONC comments — see `tsconfig.*.json`
history). This warning is the only place the trap could be recorded.
`components.json` has no `iconLibrary` or `font` key. **Do not add them
back.** Their omission is the fix, not an oversight — read on before you
"helpfully" restore either one.
Both `npx shadcn-vue init` and `npx shadcn-vue add <component>` read
these fields and **silently rewrite `src/assets/main.css`** — hit twice
in this project's own history: `init` first, then a follow-up `add`
call, both re-added a `@import url('https://fonts.googleapis.com/...')`
line and switched the dark-mode `@custom-variant` back to a `.dark`-class
toggle. If you ever run either command again:
Earlier revisions of this file set `"iconLibrary": "lucide"` and
`"font": "inter"`, both wrong for this project (FontAwesome only, per
the `vue-web-development` house skill; no external font at all). Both
`npx shadcn-vue init` and `npx shadcn-vue add <component>` read those two
fields and, when set, rewrite `src/assets/main.css` — hit twice in this
project's own history, `init` then a later `add`, both re-adding a
`@import url('https://fonts.googleapis.com/...')` line and switching the
dark-mode `@custom-variant` back to a `.dark`-class toggle.
1. **Diff `src/assets/main.css` before committing.** Remove any
re-added `@import url('https://fonts.googleapis...')` line (external
font, blocked by the admin CSP's `style-src 'self'` anyway) and
restore the `@custom-variant dark` removal (see the file's own header
comment for why: `prefers-color-scheme`, not a class toggle).
2. **Check the new component for a `lucide-vue-next`/`@lucide/vue`
import** and swap it for the FontAwesome equivalent (see
`src/components/ui/select/Select*.vue` for the pattern already
applied there).
The actual shadcn-vue config schema
(`node_modules/shadcn-vue/dist/schema/index.js`'s `rawConfigSchema`)
declares both fields as `z.string().optional()` — not an enum, contrary
to an earlier, incorrect claim in this project's own review notes that
the CLI schema forced one of five fixed values. The CLI's `init --font`/
`--icon-library` flags do offer five fixed choices each, but that is a
flag-parsing constraint, not a `components.json` schema constraint.
Deleting both keys leaves them `undefined` after parse, a valid state:
verified empirically, running `npx shadcn-vue add badge -o -y` against a
`components.json` with both keys removed left `src/assets/main.css`
byte-for-byte unchanged.
That verification also surfaced a narrower, remaining trap: the same run
still added `@lucide/vue` back to `package.json`'s dependencies (Badge
itself does not use it — some components' registry entries list an icon
package regardless of `iconLibrary`). So after any future `shadcn-vue
add`/`init` call:
1. **Check `git diff webui/package.json`** for a re-added
`lucide-vue-next`/`@lucide/vue` dependency, and remove it
(`npm uninstall`) if nothing in `src/` imports from it.
2. **Check the new component's own file** for a
`lucide-vue-next`/`@lucide/vue` import and swap it for the
FontAwesome equivalent (see `src/components/ui/select/Select*.vue`
for the pattern already applied there).
3. **Diff `src/assets/main.css` anyway** before committing, in case a
future shadcn-vue release starts reading a different config key for
the same font-injection step.
## Local development
-2
View File
@@ -1,7 +1,6 @@
{
"$schema": "https://shadcn-vue.com/schema.json",
"style": "reka-nova",
"font": "inter",
"typescript": true,
"tailwind": {
"config": "",
@@ -10,7 +9,6 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"pointer": false,
"aliases": {
+7 -1
View File
@@ -80,7 +80,13 @@ function assertNoInlineAssets(html, sourceLabel) {
const scriptTagRe = /<script\b([^>]*)>/gi
let match
while ((match = scriptTagRe.exec(html)) !== null) {
if (!/\bsrc\s*=/i.test(match[1])) {
// Anchored to attribute-start (start-of-string or preceding
// whitespace), not a bare \b word boundary: \b alone matches inside
// "data-src=" too (the "-" before "src" is itself a non-word
// character, so \b sits right before "src"), which would wrongly
// treat a data-src/nosrc/whatever-src attribute as the real src= that
// makes a <script> external.
if (!/(^|\s)src\s*=/i.test(match[1])) {
console.error(
`generate.mjs: ${sourceLabel} contains an inline <script> (no src= attribute): ${match[0]}\n` +
`The admin CSP ships script-src 'self' with no 'unsafe-inline' — every script must be external.`,