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.
176 lines
5.8 KiB
JavaScript
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'
|
|
)
|