Files
dc/client/scripts/generate-clr-shapes.js
T
dc e869e17717
Build / Build-and-ng-test (pull_request) Successful in 3m40s
Build / lighthouse (pull_request) Successful in 6m48s
Build / Build-and-test-development (pull_request) Successful in 21m22s
fix(client): wire the icon generator into the hooks that exist, and cover the components' shapes
Three review findings, all correct:

- `prebuild` was already taken: line 35 runs the version, EULA and licence
  step, and JSON keeps the last key, so the generator I added never ran and a
  new shape would have shipped an empty icon against a stale committed file. The
  call is folded into the existing `prebuild`, and into `prestart`, which is the
  only hook `ng serve` runs.
- Five shapes Clarity's own components draw were missing: `calendar` (the date
  container and soft-select), `event` (the picker's month and decade buttons),
  `arrow` (a sortable datagrid column's indicator), `ellipsis-vertical` (the
  action overflow) and `window-close` (the signpost content). Each renders an
  empty box rather than failing, which is what the component list exists to
  stop.
- The generator's comment claimed a render test caught a missing shape and
  there was none. `src/app/clr-shapes.spec.ts` is that test: it renders every
  name in the generated list and fails on any that paints no `<svg>`, plus a
  list assertion for the five component shapes - against the generated list
  rather than `ClarityIcons.get()`, because a neighbour's import can register a
  shape incidentally and a check that passes for that reason proves nothing.

The spec was probed rather than trusted: with `arrow` removed from the generated
file it fails (1 failed), and with it restored the suite is 898 SUCCESS. My first
version of the component assertion passed under that probe - it queried the
registry, which another import had already populated - which is why it now
asserts the list.
2026-11-08 16:03:48 +00:00

176 lines
5.8 KiB
JavaScript

/*
Writes client/src/app/clr-shapes.ts from the installed @clr/icons package.
The package only ships shapes in bulk modules - all-shapes.js registers the
core set plus eight themed sets, about 718 kB of SVG that every visitor
downloads before the login screen has finished. There is no per-shape entry
point, so this script lifts the shapes the app actually uses out of
all-shapes.js and emits them as a plain object the app registers itself.
Run after changing an icon shape in a template, or after bumping @clr/icons:
node scripts/generate-clr-shapes.js
It is deliberately a text reader rather than a require(): the package is UMD
and touches the DOM at import time, so it cannot be loaded in Node.
*/
const fs = require('fs')
const path = require('path')
const CLIENT = path.resolve(__dirname, '..')
const PKG = path.join(CLIENT, 'node_modules/@clr/icons/shapes/all-shapes.js')
const SRC = path.join(CLIENT, 'src')
const OUT = path.join(SRC, 'app/clr-shapes.ts')
// Shapes the app names in its own templates. Collected at generation time so a
// new `shape="..."` cannot be forgotten: the script reads the sources itself.
function shapesInTemplates() {
const shapes = new Set()
const walk = (dir) => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) {
if (entry.name !== 'node_modules') walk(full)
} else if (
/\.(html|ts)$/.test(entry.name) &&
!/\.spec\.ts$/.test(entry.name)
) {
const text = fs.readFileSync(full, 'utf8')
for (const m of text.matchAll(/shape="([a-zA-Z0-9_-]+)"/g)) {
shapes.add(m[1])
}
}
}
}
walk(SRC)
return shapes
}
/*
Shapes Clarity's own components render, which no template of ours names:
the datagrid sorts and filters, the tree expands, alerts carry status icons,
the modal closes, the wizard marks steps. They are listed here rather than
discovered because a missing one renders an empty icon silently - the check
that catches it is src/app/clr-shapes.spec.ts, which renders every name in the
generated list and fails on an empty element.
*/
const COMPONENT_SHAPES = [
'angle',
'caret',
'check',
'close',
'times',
'plus',
'minus',
'search',
'filter',
'info-standard',
'success-standard',
'error-standard',
'warning-standard',
'exclamation-triangle',
'exclamation-circle',
'info-circle',
'check-circle',
'times-circle',
'help',
'help-info',
'unknown-status',
// the date container and its picker (soft-select.component.html:23)
'calendar',
'event',
// clr-dg-column's sort indicator, on any sortable column
// (submitter.component.html:53)
'arrow',
// clr-dg-action-overflow
'ellipsis-vertical',
// ClrSignpostContent (loading-indicator.component.html:2)
'window-close'
]
const source = fs.readFileSync(PKG, 'utf8')
// exports.ClrShapeAngle = (0, svg_tag_generator_1.clrIconSVG)("<path .../>")
// - the package stores the inner markup and wraps it in this exact tag, so the
// generated file carries the same wrapper and clr-icon renders it unchanged
const SVG_OPEN =
'<svg version="1.1" viewBox="0 0 36 36" preserveAspectRatio="xMidYMid meet" ' +
'xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" ' +
'focusable="false" role="img">'
const constants = new Map()
for (const m of source.matchAll(
/exports\.(ClrShape[A-Za-z0-9_]+)\s*=\s*\(0,\s*svg_tag_generator_1\.clrIconSVG\)\(\s*("(?:[^"\\]|\\.)*")/g
)) {
constants.set(m[1], SVG_OPEN + JSON.parse(m[2]) + '</svg>')
}
// '<name>': exports.ClrShapeX, - the name the clr-icon element looks up
const byName = new Map()
for (const m of source.matchAll(
/'?([a-z0-9-]+)'?:\s*exports\.(ClrShape[A-Za-z0-9_]+),/g
)) {
byName.set(m[1], m[2])
}
// aliases: Object.defineProperty(..., 'close', ...descriptorConfig(...times))
// - 'close' is 'times' under another name, and templates use both
const aliases = new Map()
for (const m of source.matchAll(
/Object\.defineProperty\([^,]+,\s*'([a-z0-9-]+)',\s*\(0,\s*descriptor_config_1\.descriptorConfig\)\([^)]*\.([a-z0-9-]+)\)\)/g
)) {
aliases.set(m[1], m[2])
}
// some aliases point at a dashed name with bracket notation:
// defineProperty(..., 'warning', descriptorConfig(exports.CoreShapes['exclamation-triangle']))
for (const m of source.matchAll(
/Object\.defineProperty\([^,]+,\s*'([a-z0-9-]+)',\s*\(0,\s*descriptor_config_1\.descriptorConfig\)\([^)]*\['([a-z0-9-]+)'\]\)\)/g
)) {
aliases.set(m[1], m[2])
}
const lookup = (name, seen = new Set()) => {
if (byName.has(name)) return constants.get(byName.get(name))
const target = aliases.get(name)
if (target && !seen.has(target)) return lookup(target, seen.add(name) || seen)
return undefined
}
const wanted = new Set([...shapesInTemplates(), ...COMPONENT_SHAPES])
const missing = [...wanted].filter((name) => !lookup(name))
if (missing.length) {
console.error('not a shape in @clr/icons: ' + missing.join(', '))
process.exit(1)
}
const entries = [...wanted].sort().map((name) => {
const svg = lookup(name)
if (!svg) throw new Error('no SVG for ' + name)
return ' ' + JSON.stringify(name) + ': ' + JSON.stringify(svg)
})
const banner = `/*
GENERATED by scripts/generate-clr-shapes.js - do not edit.
Only the Clarity icon shapes this app uses. \`@clr/icons/shapes/all-shapes\`
registers about 718 kB of SVG, of which these ${entries.length} are the ones
any screen here can ask for; registering them directly keeps the rest out of
the initial bundle. Regenerate after changing a shape in a template.
*/
export const CLR_SHAPES: Record<string, string> = {
${entries.join(',\n')}
}
`
fs.writeFileSync(OUT, banner)
console.log(
'wrote ' +
path.relative(CLIENT, OUT) +
' with ' +
entries.length +
' shapes, ' +
Math.round(Buffer.byteLength(banner) / 1024) +
' kB'
)