From 03650cac879aaac1faba2112d703574b73882121 Mon Sep 17 00:00:00 2001 From: u8array Date: Wed, 14 Oct 2026 18:10:31 +0200 Subject: [PATCH] chore: a knip sweep drops unused dependencies, dead files and dead exports --- knip.json | 32 +++++ package.json | 5 +- packages/core/package.json | 3 + packages/core/src/catalog/index.ts | 2 +- packages/core/src/lib/customFonts.ts | 13 -- packages/core/src/lib/footprintProber.ts | 5 - packages/core/src/lib/gs1ModeDFns.ts | 16 --- packages/core/src/lib/storagePath.ts | 1 - packages/core/src/lib/templateObjects.ts | 10 -- packages/core/src/lib/zplParser.ts | 1 - packages/core/src/registry/qrcode.ts | 2 +- packages/core/src/types/LabelConfig.ts | 1 - packages/core/src/types/PrinterProfile.ts | 1 - packages/mcp-server/package.json | 3 +- packages/mcp-server/src/appBridge.ts | 2 +- pnpm-lock.yaml | 70 +--------- scratch-labelary/compare2.mjs | 98 ------------- scratch-labelary/compare3.mjs | 92 ------------- scratch-labelary/t4_template_only.png | 1 - scripts/gen-attribution.mjs | 2 +- scripts/lib/pnpm-licenses.mjs | 18 +-- src/components/Canvas/bwipHelpers.ts | 14 +- src/components/Canvas/canvasActions.ts | 2 +- .../Canvas/hooks/useCanvasPanZoom.ts | 2 - src/components/Palette/paletteGroups.ts | 2 - .../Properties/DensityRescaleModal.tsx | 2 - src/components/Properties/SectionCard.tsx | 68 +-------- src/lib/zplParser.test.ts | 10 +- src/store/labelStore.ts | 7 +- src/test/font0PlacementMatch.test.ts | 5 +- tests/fixtures/font0GlyphCoverageCases.ts | 10 +- tests/scripts/report_font0_glyph_drift.ts | 129 ------------------ 32 files changed, 73 insertions(+), 556 deletions(-) create mode 100644 knip.json delete mode 100644 scratch-labelary/compare2.mjs delete mode 100644 scratch-labelary/compare3.mjs delete mode 100644 scratch-labelary/t4_template_only.png delete mode 100644 tests/scripts/report_font0_glyph_drift.ts diff --git a/knip.json b/knip.json new file mode 100644 index 00000000..8681020c --- /dev/null +++ b/knip.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://unpkg.com/knip@6/schema.json", + "ignore": [ + "src-tauri/**" + ], + "ignoreBinaries": [ + "reg" + ], + "ignoreExportsUsedInFile": true, + "workspaces": { + ".": { + "entry": [ + "scripts/*.d.mts", + "scripts/catalog-reference-merge.mjs", + "scripts/gen-gs1-catalog.mjs", + "tests/scripts/*.ts" + ] + }, + "packages/core": { + "includeEntryExports": true + }, + "packages/mcp-server": { + "entry": [ + "scripts/*.d.mts" + ], + "ignoreDependencies": [ + "esbuild", + "postject" + ] + } + } +} diff --git a/package.json b/package.json index 3c071fe3..36595a11 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "store:listing": "node scripts/build-store-listing.mjs", "coverage:gen": "node scripts/gen-coverage.mjs", "coverage:check": "node scripts/gen-coverage.mjs --check", + "knip": "pnpm dlx knip@6.41.0", "preview": "vite preview", "tauri": "tauri", "e2e": "pnpm build && playwright test" @@ -54,7 +55,6 @@ "react": "^19.3.0", "react-dom": "^19.3.0", "react-konva": "^19.3.0", - "zod": "^4.6.5", "zundo": "^2.3.0", "zustand": "^5.0.15" }, @@ -68,11 +68,8 @@ "@tauri-apps/cli": "^2.12.1", "@testing-library/dom": "^10.4.2", "@testing-library/react": "^16.3.3", - "@testing-library/user-event": "^14.6.7", - "@types/babel__core": "^7.20.5", "@types/node": "^26.6.4", "@types/papaparse": "^5.5.2", - "@types/pixelmatch": "^7.1.0", "@types/pngjs": "^6.0.5", "@types/react": "^19.3.0", "@types/react-dom": "^19.3.0", diff --git a/packages/core/package.json b/packages/core/package.json index c0743ae6..2a41964e 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -21,5 +21,8 @@ "bwip-js": "^4.11.4", "fflate": "^0.8.3", "zod": "^4.6.5" + }, + "devDependencies": { + "vitest": "^4.1.11" } } diff --git a/packages/core/src/catalog/index.ts b/packages/core/src/catalog/index.ts index 6eaa162d..3fc2939a 100644 --- a/packages/core/src/catalog/index.ts +++ b/packages/core/src/catalog/index.ts @@ -3,7 +3,7 @@ import raw from "./commands.json"; import type { Catalog, CatalogSection, ZplCommandEntry } from "./schema"; // Types only: a value re-export pulls zod into the app bundle. -export type { CatalogSection, CommandParam, CommandReference, CommandSupport, ImportLossCause, SupportLevel, ZplCommandEntry } from "./schema"; +export type { CatalogSection, CommandParam, CommandSupport, ImportLossCause, SupportLevel, ZplCommandEntry } from "./schema"; // Cast, not parse: catalog.test.ts validates the JSON. const catalog = raw as unknown as Catalog; diff --git a/packages/core/src/lib/customFonts.ts b/packages/core/src/lib/customFonts.ts index 7056e811..7e0d097e 100644 --- a/packages/core/src/lib/customFonts.ts +++ b/packages/core/src/lib/customFonts.ts @@ -49,19 +49,6 @@ export function isTrueTypeFileName(path: string): boolean { /** E=flash, R=RAM, A=removable, B=on-board flash. */ export const ZPL_DRIVE_PREFIXES = ["E:", "R:", "A:", "B:"] as const; -/** Firmware built-ins; excluded from nextFreeAlias auto-pick range. */ -export const ZPL_BUILTIN_FONT_IDS = [ - "0", - "A", - "B", - "C", - "D", - "E", - "F", - "G", - "H", -] as const; - /** The printer path for a locally picked file: a real device, up to 8 name chars, and an extension * ^A@ and ^CW can reference (spec p.63, p.168). .TTE stays, .OTF and the rest become .TTF (p.184). */ export function printerFontFileName(fileName: string): string | undefined { diff --git a/packages/core/src/lib/footprintProber.ts b/packages/core/src/lib/footprintProber.ts index 2dbdf818..cfc9a33a 100644 --- a/packages/core/src/lib/footprintProber.ts +++ b/packages/core/src/lib/footprintProber.ts @@ -25,11 +25,6 @@ export function registerFootprintMeasurer(m: FootprintMeasurer | null): void { cache = new WeakMap(); } -/** Clears only if `m` is still active (stale cleanup must not null a successor). */ -export function unregisterFootprintMeasurer(m: FootprintMeasurer): void { - if (measurer === m) measurer = null; -} - /** Drop the memo. The cache keys on the props reference, not the resolution * binding, so a caller that re-measures the same props under a new binding * (withFootprintBinding) must reset it or read a stale width. */ diff --git a/packages/core/src/lib/gs1ModeDFns.ts b/packages/core/src/lib/gs1ModeDFns.ts index c43ffef6..dd22319b 100644 --- a/packages/core/src/lib/gs1ModeDFns.ts +++ b/packages/core/src/lib/gs1ModeDFns.ts @@ -77,19 +77,3 @@ export function code128PlainExclusiveFns( ): Set { return fnConsumerBuckets(objects, variables).plainExclusive; } - -/** Mode-D slots shared with another field type. */ -export function gs1ModeDSharedFns( - objects: readonly LabelObject[], - variables: readonly Variable[], -): Set { - return fnConsumerBuckets(objects, variables).modeDShared; -} - -/** Plain-^BC slots shared with another field type. */ -export function code128PlainSharedFns( - objects: readonly LabelObject[], - variables: readonly Variable[], -): Set { - return fnConsumerBuckets(objects, variables).plainShared; -} diff --git a/packages/core/src/lib/storagePath.ts b/packages/core/src/lib/storagePath.ts index 11630428..ba31e49d 100644 --- a/packages/core/src/lib/storagePath.ts +++ b/packages/core/src/lib/storagePath.ts @@ -6,7 +6,6 @@ import { newId } from "./ids"; /** R volatile RAM, E flash, B alt flash, A alias. */ export const STORAGE_DEVICES = ["R", "E", "B", "A"] as const; -export type StorageDevice = (typeof STORAGE_DEVICES)[number]; /** Where a locally picked font goes. */ export const DEFAULT_FONT_DEVICE = "E"; diff --git a/packages/core/src/lib/templateObjects.ts b/packages/core/src/lib/templateObjects.ts index ff70ed90..9a51fb8b 100644 --- a/packages/core/src/lib/templateObjects.ts +++ b/packages/core/src/lib/templateObjects.ts @@ -50,16 +50,6 @@ export function rewriteTemplateMarkersMap( return mapLeafContent(objects, (content) => renameTemplateMarkers(content, renames)); } -/** Replace every `«name»` marker with `replacement` across a subtree's leaf - * `content`. Used on variable deletion. */ -export function substituteTemplateMarkers( - objects: LabelObject[], - name: string, - replacement: string, -): LabelObject[] { - return substituteTemplateMarkersMap(objects, new Map([[name, replacement]])); -} - /** Substitute many names in ONE pass per leaf. */ export function substituteTemplateMarkersMap( objects: LabelObject[], diff --git a/packages/core/src/lib/zplParser.ts b/packages/core/src/lib/zplParser.ts index acdc775a..9bd2e603 100644 --- a/packages/core/src/lib/zplParser.ts +++ b/packages/core/src/lib/zplParser.ts @@ -39,7 +39,6 @@ export type { ImportFindingKind, ImportFinding, ImportReport, - ParsedPage, ParsedZPL, FontLossReason, UnbalancedFormat, diff --git a/packages/core/src/registry/qrcode.ts b/packages/core/src/registry/qrcode.ts index 0d6478a5..ba27cedb 100644 --- a/packages/core/src/registry/qrcode.ts +++ b/packages/core/src/registry/qrcode.ts @@ -16,7 +16,7 @@ const qrFdTransform = qrFdWire(isQrEcLevel(obj.props.errorCorrection) ? obj.props.errorCorrection : QR_EC_DEFAULT, s); // Re-exported from lib/qrBy (the cycle-free spot) because this is the prop's home. -export { QR_BY_DEFAULT_HEIGHT, isQrByHeight, qrByHeight } from '../lib/qrBy'; +export { QR_BY_DEFAULT_HEIGHT, qrByHeight } from '../lib/qrBy'; const DEFAULT_MAGNIFICATION = 4; /** ^BQ b per spec p.128: only 1 and 2 exist, anything else is the default. */ diff --git a/packages/core/src/types/LabelConfig.ts b/packages/core/src/types/LabelConfig.ts index 4ea48753..58f7ded9 100644 --- a/packages/core/src/types/LabelConfig.ts +++ b/packages/core/src/types/LabelConfig.ts @@ -116,7 +116,6 @@ export const RFID_RETRIES_RANGE = { min: 1, max: 10 } as const; /** ^RW r/w numeric power band; some firmware takes H/M/L instead. */ export const RFID_POWER_RANGE = { min: 0, max: 30 } as const; export const RFID_POWER_LEVELS = ['H', 'M', 'L'] as const; -export type RfidPowerLevel = (typeof RFID_POWER_LEVELS)[number]; export const isRfidPowerLevel = makeEnumGuard(RFID_POWER_LEVELS); const rfidPowerSchema = z.union([ intInRange(RFID_POWER_RANGE), diff --git a/packages/core/src/types/PrinterProfile.ts b/packages/core/src/types/PrinterProfile.ts index 0152978e..8bdab190 100644 --- a/packages/core/src/types/PrinterProfile.ts +++ b/packages/core/src/types/PrinterProfile.ts @@ -96,7 +96,6 @@ export const isMaintenanceAlertType = makeEnumGuard(MAINTENANCE_ALERT_TYPES); /** ^MA print slot: Y=emit warning label, N=skip. */ export const MAINTENANCE_ALERT_PRINT_VALUES = ['Y', 'N'] as const; -export type MaintenanceAlertPrint = (typeof MAINTENANCE_ALERT_PRINT_VALUES)[number]; export const isMaintenanceAlertPrint = makeEnumGuard(MAINTENANCE_ALERT_PRINT_VALUES); /** ^MA units slot: C=centimeters, I=inches, M=meters. */ diff --git a/packages/mcp-server/package.json b/packages/mcp-server/package.json index 71edd102..e2adbb09 100644 --- a/packages/mcp-server/package.json +++ b/packages/mcp-server/package.json @@ -17,6 +17,7 @@ "devDependencies": { "esbuild": "^0.28.2", "postject": "^1.0.0-alpha.6", - "tsx": "^4.23.15" + "tsx": "^4.23.15", + "vitest": "^4.1.11" } } diff --git a/packages/mcp-server/src/appBridge.ts b/packages/mcp-server/src/appBridge.ts index 6b686222..0ea7afe7 100644 --- a/packages/mcp-server/src/appBridge.ts +++ b/packages/mcp-server/src/appBridge.ts @@ -86,7 +86,7 @@ export const RASTER_TIMEOUT_MS = 20_000; export const OPEN_DRAFT_TIMEOUT_MS = 20_000; /** Waits for the canvas to settle like a design read-back. */ -export const EDIT_DESIGN_TIMEOUT_MS = DESIGN_RESPONSE_TIMEOUT_MS; +const EDIT_DESIGN_TIMEOUT_MS = DESIGN_RESPONSE_TIMEOUT_MS; /** The event a pending entry is waiting on. Kept per request because the * reply schemas are mutually satisfiable: a draft receipt, a raster response diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3ec2223..e216e08a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,9 +89,6 @@ importers: react-konva: specifier: ^19.3.0 version: 19.3.0(@types/react@19.3.0)(konva@10.7.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - zod: - specifier: ^4.6.5 - version: 4.6.5 zundo: specifier: ^2.3.0 version: 2.3.0(zustand@5.0.15(@types/react@19.3.0)(react@19.3.0)(use-sync-external-store@1.6.0(react@19.3.0))) @@ -126,21 +123,12 @@ importers: '@testing-library/react': specifier: ^16.3.3 version: 16.3.3(@testing-library/dom@10.4.2)(@types/react-dom@19.3.0(@types/react@19.3.0))(@types/react@19.3.0)(react-dom@19.3.0(react@19.3.0))(react@19.3.0) - '@testing-library/user-event': - specifier: ^14.6.7 - version: 14.6.7(@testing-library/dom@10.4.2) - '@types/babel__core': - specifier: ^7.20.5 - version: 7.20.5 '@types/node': specifier: ^26.6.4 version: 26.6.4 '@types/papaparse': specifier: ^5.5.2 version: 5.5.2 - '@types/pixelmatch': - specifier: ^7.1.0 - version: 7.1.0 '@types/pngjs': specifier: ^6.0.5 version: 6.0.5 @@ -210,6 +198,10 @@ importers: zod: specifier: ^4.6.5 version: 4.6.5 + devDependencies: + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.6.4)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.1)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)) packages/mcp-server: dependencies: @@ -235,6 +227,9 @@ importers: tsx: specifier: ^4.23.15 version: 4.23.15 + vitest: + specifier: ^4.1.11 + version: 4.1.11(@types/node@26.6.4)(@vitest/coverage-v8@4.1.11)(jsdom@30.1.1)(vite@8.3.2(@types/node@26.6.4)(esbuild@0.28.2)(jiti@2.7.0)(tsx@4.23.15)) packages: @@ -1227,27 +1222,9 @@ packages: '@types/react-dom': optional: true - '@testing-library/user-event@14.6.7': - resolution: {integrity: sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg==} - engines: {node: '>=12', npm: '>=6'} - peerDependencies: - '@testing-library/dom': '>=7.21.4' - '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/babel__core@7.20.5': - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - - '@types/babel__generator@7.27.0': - resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} - - '@types/babel__template@7.4.4': - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - - '@types/babel__traverse@7.28.0': - resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1275,10 +1252,6 @@ packages: '@types/papaparse@5.5.2': resolution: {integrity: sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==} - '@types/pixelmatch@7.1.0': - resolution: {integrity: sha512-0QezS/yQirZ++KVOHu3a49ZB3zeRxGh3PRd9z9+FUNO+14N3oL0vibN3ADEaIvSeY8hsdChi09U6fjTcTcqowQ==} - deprecated: This is a stub types definition. pixelmatch provides its own type definitions, so you do not need this installed. - '@types/pngjs@6.0.5': resolution: {integrity: sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==} @@ -3628,33 +3601,8 @@ snapshots: '@types/react': 19.3.0 '@types/react-dom': 19.3.0(@types/react@19.3.0) - '@testing-library/user-event@14.6.7(@testing-library/dom@10.4.2)': - dependencies: - '@testing-library/dom': 10.4.2 - '@types/aria-query@5.0.4': {} - '@types/babel__core@7.20.5': - dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - '@types/babel__generator': 7.27.0 - '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.28.0 - - '@types/babel__generator@7.27.0': - dependencies: - '@babel/types': 7.29.8 - - '@types/babel__template@7.4.4': - dependencies: - '@babel/parser': 7.29.8 - '@babel/types': 7.29.8 - - '@types/babel__traverse@7.28.0': - dependencies: - '@babel/types': 7.29.8 - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -3680,10 +3628,6 @@ snapshots: dependencies: '@types/node': 26.6.4 - '@types/pixelmatch@7.1.0': - dependencies: - pixelmatch: 7.2.0 - '@types/pngjs@6.0.5': dependencies: '@types/node': 26.6.4 diff --git a/scratch-labelary/compare2.mjs b/scratch-labelary/compare2.mjs deleted file mode 100644 index dbd484b6..00000000 --- a/scratch-labelary/compare2.mjs +++ /dev/null @@ -1,98 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import zlib from 'node:zlib'; - -const dir = import.meta.dirname; -const png = (n) => decodePng(join(dir, n)); - -function decodePng(path) { - const buf = readFileSync(path); - let off = 8; - const chunks = []; - while (off < buf.length) { - const len = buf.readUInt32BE(off); - const type = buf.toString('ascii', off + 4, off + 8); - chunks.push({ type, data: buf.subarray(off + 8, off + 8 + len) }); - off += 12 + len; - } - const ihdr = chunks.find((c) => c.type === 'IHDR').data; - const width = ihdr.readUInt32BE(0); - const height = ihdr.readUInt32BE(4); - const colorType = ihdr[9]; - const idat = Buffer.concat(chunks.filter((c) => c.type === 'IDAT').map((c) => c.data)); - const raw = zlib.inflateSync(idat); - const bpp = colorType === 6 ? 4 : colorType === 2 ? 3 : 1; - const stride = width * bpp; - const out = Buffer.alloc(stride * height); - let prev = Buffer.alloc(stride); - for (let y = 0; y < height; y++) { - const filter = raw[y * (stride + 1)]; - const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1)); - const cur = out.subarray(y * stride, (y + 1) * stride); - for (let x = 0; x < stride; x++) { - const a = x >= bpp ? cur[x - bpp] : 0; - const b = prev[x]; - const c = x >= bpp ? prev[x - bpp] : 0; - let v = line[x]; - if (filter === 1) v = (v + a) & 0xff; - else if (filter === 2) v = (v + b) & 0xff; - else if (filter === 3) v = (v + ((a + b) >> 1)) & 0xff; - else if (filter === 4) { - const p = a + b - c; - const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); - const pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; - v = (v + pr) & 0xff; - } - cur[x] = v; - } - prev = cur; - } - return { width, height, bpp, pixels: out }; -} - -function diffPng(a, b, labelA, labelB) { - if (a.width !== b.width || a.height !== b.height) { - console.log(`${labelA} vs ${labelB}: DIFFERENT DIMENSIONS ${a.width}x${a.height} vs ${b.width}x${b.height}`); - return null; - } - let diff = 0; - let minX = a.width, maxX = 0, minY = a.height, maxY = 0; - for (let y = 0; y < a.height; y++) { - for (let x = 0; x < a.width; x++) { - const i = (y * a.width + x) * (a.bpp || 1); - if (a.pixels[i] !== b.pixels[i]) { - diff++; - if (x < minX) minX = x; - if (x > maxX) maxX = x; - if (y < minY) minY = y; - if (y > maxY) maxY = y; - } - } - } - const total = a.pixels.length; - const pct = (100 * diff / total).toFixed(3); - console.log(`${labelA} vs ${labelB}: ${diff} px of ${total} (${pct}%) bbox x[${minX}..${maxX}] y[${minY}..${maxY}]`); - return diff; -} - -const t1 = png('t1_bare.png'); -const t2 = png('t2_sealed.png'); -const t6 = png('t6_embed.png'); -const t7 = png('t7_qr.png'); -const ctrl_sku = png('ctrl_sku.png'); -const ctrl_def = png('ctrl_default.png'); -const ctrl_embed = png('ctrl_embed.png'); -const ctrl_qr = png('ctrl_qr.png'); - -console.log('--- bare slot + recall vs flat render (the fix) ---'); -diffPng(t1, ctrl_sku, 't1 bare+recall', 'ctrl SKU-42'); -console.log('--- sealed default + recall: which text prints? ---'); -const d1 = diffPng(t2, ctrl_def, 't2 sealed+recall', 'ctrl DEFAULT'); -const d2 = diffPng(t2, ctrl_sku, 't2 sealed+recall', 'ctrl SKU-42'); -console.log(' => recall overrides sealed default on Labelary:', d2 === 0, '| keeps default:', d1 === 0); -console.log('--- embed case ---'); -diffPng(t6, ctrl_embed, 't6 embed+recall', 'ctrl PRESKU-42POST'); -console.log('--- QR case ---'); -diffPng(t7, ctrl_qr, 't7 qr+recall', 'ctrl QR MA,A1'); -console.log('--- baseline: DEFAULT vs SKU-42 flat renders ---'); -diffPng(ctrl_def, ctrl_sku, 'ctrl DEFAULT', 'ctrl SKU-42'); diff --git a/scratch-labelary/compare3.mjs b/scratch-labelary/compare3.mjs deleted file mode 100644 index 73f9293e..00000000 --- a/scratch-labelary/compare3.mjs +++ /dev/null @@ -1,92 +0,0 @@ -import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import zlib from 'node:zlib'; - -const dir = import.meta.dirname; -function decodePng(name) { - const buf = readFileSync(join(dir, name)); - let off = 8; - const chunks = []; - while (off < buf.length) { - const len = buf.readUInt32BE(off); - const type = buf.toString('ascii', off + 4, off + 8); - chunks.push({ type, data: buf.subarray(off + 8, off + 8 + len) }); - off += 12 + len; - } - const ihdr = chunks.find((c) => c.type === 'IHDR').data; - const width = ihdr.readUInt32BE(0); - const height = ihdr.readUInt32BE(4); - const colorType = ihdr[9]; - const idat = Buffer.concat(chunks.filter((c) => c.type === 'IDAT').map((c) => c.data)); - const raw = zlib.inflateSync(idat); - const bpp = colorType === 6 ? 4 : colorType === 2 ? 3 : 1; - const stride = width * bpp; - const out = Buffer.alloc(stride * height); - let prev = Buffer.alloc(stride); - for (let y = 0; y < height; y++) { - const filter = raw[y * (stride + 1)]; - const line = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1)); - const cur = out.subarray(y * stride, (y + 1) * stride); - for (let x = 0; x < stride; x++) { - const a = x >= bpp ? cur[x - bpp] : 0; - const b = prev[x]; - const c = x >= bpp ? prev[x - bpp] : 0; - let v = line[x]; - if (filter === 1) v = (v + a) & 0xff; - else if (filter === 2) v = (v + b) & 0xff; - else if (filter === 3) v = (v + ((a + b) >> 1)) & 0xff; - else if (filter === 4) { - const p = a + b - c; - const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c); - const pr = pa <= pb && pa <= pc ? a : pb <= pc ? b : c; - v = (v + pr) & 0xff; - } - cur[x] = v; - } - prev = cur; - } - return { width, height, bpp, pixels: out }; -} - -function diffPng(a, b, labelA, labelB) { - if (a.width !== b.width || a.height !== b.height) { - console.log(`${labelA} vs ${labelB}: DIFFERENT DIMENSIONS`); - return; - } - let diff = 0; - let minX = a.width, maxX = 0, minY = a.height, maxY = 0; - for (let y = 0; y < a.height; y++) { - for (let x = 0; x < a.width; x++) { - const i = (y * a.width + x) * (a.bpp || 1); - if (a.pixels[i] !== b.pixels[i]) { - diff++; - if (x < minX) minX = x; - if (x > maxX) maxX = x; - if (y < minY) minY = y; - if (y > maxY) maxY = y; - } - } - } - console.log(`${labelA} vs ${labelB}: ${diff} px bbox x[${minX}..${maxX}] y[${minY}..${maxY}]`); -} - -const p = (n) => decodePng(n); -const t6 = p('t6_embed.png'); -const e1 = p('e1_flat_embed_default.png'); -const e2 = p('e2_stored_embed_recall.png'); -const e4 = p('e4_flat_unresolved.png'); -const e5 = p('e5_ctrl_predefaultpost.png'); -const ctrl_sku_embed = p('ctrl_embed.png'); - -console.log('--- flat ^FE embed + header default vs flat PREDEFAULTPOST ---'); -diffPng(e1, e5, 'e1 flat embed+default', 'ctrl PREDEFAULTPOST'); -console.log('--- stored embed (header default) + recall SKU-42 vs flat PREDEFAULTPOST ---'); -diffPng(e2, e5, 'e2 stored+recall', 'ctrl PREDEFAULTPOST'); -console.log('--- stored embed (header default) + recall SKU-42 vs flat PRESKU-42POST ---'); -diffPng(e2, ctrl_sku_embed, 'e2 stored+recall', 'ctrl PRESKU-42POST'); -console.log('--- stored embed (header default) + recall vs flat embed + default ---'); -diffPng(e2, e1, 'e2 stored+recall', 'e1 flat embed+default'); -console.log('--- t6 (bare embed) + recall vs unresolved flat embed ---'); -diffPng(t6, e4, 't6 bare+recall', 'e4 unresolved'); -console.log('--- t6 (bare embed) + recall vs PRESKU-42POST ---'); -diffPng(t6, ctrl_sku_embed, 't6 bare+recall', 'ctrl PRESKU-42POST'); diff --git a/scratch-labelary/t4_template_only.png b/scratch-labelary/t4_template_only.png deleted file mode 100644 index cdb1d14a..00000000 --- a/scratch-labelary/t4_template_only.png +++ /dev/null @@ -1 +0,0 @@ -ERROR: Requested 1st label but ZPL generated no labels \ No newline at end of file diff --git a/scripts/gen-attribution.mjs b/scripts/gen-attribution.mjs index 17912f93..951c647a 100644 --- a/scripts/gen-attribution.mjs +++ b/scripts/gen-attribution.mjs @@ -1,6 +1,6 @@ // Generates THIRD-PARTY-LICENSES.md from the actual production dependency tree, // so attribution stays exact as deps come and go. The npm section is derived -// from `pnpm licenses list --prod --json`, reading each package's own license +// from the scoped pnpm licenses read in lib/pnpm-licenses.mjs, reading each package's own license // text out of its install path; the vendored-font notices (assets npm doesn't // track) are read from src/assets/fonts so editing one is caught by the drift // check too. The preamble and IBM Plex note live here as the source of truth. diff --git a/scripts/lib/pnpm-licenses.mjs b/scripts/lib/pnpm-licenses.mjs index 87940169..56650422 100644 --- a/scripts/lib/pnpm-licenses.mjs +++ b/scripts/lib/pnpm-licenses.mjs @@ -1,10 +1,12 @@ // Single source for reading the production dependency license tree. Both the // license gate and the attribution generator depend on the exact same scope: -// the shipped app only (--filter zplab). Since the workspace split, -// an unscoped read would also pull packages/mcp-server's tree, which runs as a -// separate process and never lands in dist/. +// the shipped app and the workspace packages it bundles. An unscoped read would +// also pull packages/mcp-server's tree, which runs as a separate process and +// never lands in dist/. import { execSync } from 'node:child_process'; +const LICENSES_CMD = 'pnpm licenses list --prod --json --filter zplab...'; + // Returns pnpm's grouped output: { [spdxId]: Array<{name, versions, paths, // license, author, homepage, description}> }. Fails closed (exit 2) if the // list can't be read or is empty, so neither tool ever silently checks nothing. @@ -13,25 +15,25 @@ export function readProdLicenses() { try { // maxBuffer well above the current ~20 KB so a growing tree can't hit the // 1 MB default and turn a license read into an ENOBUFS crash. - raw = execSync('pnpm licenses list --prod --json --filter zplab', { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); + raw = execSync(LICENSES_CMD, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }); } catch (e) { - console.error('Could not read `pnpm licenses list --prod --json`:', e.message); + console.error(`Could not read \`${LICENSES_CMD}\`:`, e.message); process.exit(2); } let byLicense; try { byLicense = JSON.parse(raw); } catch (e) { - console.error('`pnpm licenses list --prod --json` did not return valid JSON:', e.message); + console.error(`\`${LICENSES_CMD}\` did not return valid JSON:`, e.message); process.exit(2); } if (!byLicense || typeof byLicense !== 'object' || Array.isArray(byLicense)) { - console.error('`pnpm licenses list --prod --json` did not return a JSON object.'); + console.error(`\`${LICENSES_CMD}\` did not return a JSON object.`); process.exit(2); } const total = Object.values(byLicense).reduce((n, list) => n + (Array.isArray(list) ? list.length : 0), 0); if (total === 0) { - console.error('`pnpm licenses list --prod --json` returned no packages; refusing to pass a check that inspected nothing.'); + console.error(`\`${LICENSES_CMD}\` returned no packages; refusing to pass a check that inspected nothing.`); process.exit(2); } return byLicense; diff --git a/src/components/Canvas/bwipHelpers.ts b/src/components/Canvas/bwipHelpers.ts index ec6ef221..dd07de7f 100644 --- a/src/components/Canvas/bwipHelpers.ts +++ b/src/components/Canvas/bwipHelpers.ts @@ -39,16 +39,8 @@ import { } from "@zplab/core/lib/barcodeDims"; import type { DataMatrixProps } from "@zplab/core/registry/datamatrix"; -export { ZEBRA_WIDTH_BAR_TYPES }; -export { - buildBwipOptions, - BWIP_SCALE, - get1DBwipScale, - getDisplaySize, - splitTlc39Content, -} from "@zplab/core/lib/barcodeDims"; +export { buildBwipOptions, get1DBwipScale, getDisplaySize } from "@zplab/core/lib/barcodeDims"; export type { BarcodeDisplaySize, EanUpcType, EanUpcHriFragment } from "@zplab/core/lib/barcodeDims"; -export { eanCheckDigit, upceCheckDigit } from "@zplab/core/lib/barcodeCheckDigits"; const bwipEngine = bwipjs as unknown as BwipEngine; @@ -65,7 +57,7 @@ function getValidationCanvas(): HTMLCanvasElement { } /** Strip `bwip-js: bwipp.symbology:` prefixes from encoder errors for UI display. */ -export function cleanBwipError(e: unknown): string { +function cleanBwipError(e: unknown): string { const raw = errorMessage(e); return raw.replace(/^bwip-js:\s*/i, "").replace(/^bwipp\.[^:]+:\s*/i, ""); } @@ -191,7 +183,7 @@ export function measureBarcodeFootprintDots( /** TLC39 composite (ZD230-true): MicroPDF417 indented 1*w1 on top, Code 39 * below, and in linked form the lone linkage T behind a 10-module gap * printed 1.4*h1 tall (see tlc39Code39Runs / tlc39CompositeLayout). */ -export function renderTlc39Canvas( +function renderTlc39Canvas( props: Tlc39RenderProps, scale: number, dpmm: number, diff --git a/src/components/Canvas/canvasActions.ts b/src/components/Canvas/canvasActions.ts index 3a489558..3c5aa55f 100644 --- a/src/components/Canvas/canvasActions.ts +++ b/src/components/Canvas/canvasActions.ts @@ -1,7 +1,7 @@ import type { ZOrderDir } from "../../lib/zorder"; import type { MenuAction, MenuSection } from "../ui/ContextMenu"; -export type { MenuAction, MenuSection }; +export type { MenuSection }; /** What the right-click targeted plus the capabilities + dispatchers needed to * build the menu. Kept data-only (no Konva/React) so the builder is pure and diff --git a/src/components/Canvas/hooks/useCanvasPanZoom.ts b/src/components/Canvas/hooks/useCanvasPanZoom.ts index ccb9e6a1..509ed4fa 100644 --- a/src/components/Canvas/hooks/useCanvasPanZoom.ts +++ b/src/components/Canvas/hooks/useCanvasPanZoom.ts @@ -4,8 +4,6 @@ const ZOOM_STEPS = [0.25, 0.5, 0.75, 1, 1.25, 1.5, 2, 3, 4]; const ZOOM_MIN = 0.25; const ZOOM_MAX = 4; -export { ZOOM_MIN, ZOOM_MAX, ZOOM_STEPS }; - interface Options { zoom: number; onZoomChange: (zoom: number) => void; diff --git a/src/components/Palette/paletteGroups.ts b/src/components/Palette/paletteGroups.ts index b1bdb21f..a167c526 100644 --- a/src/components/Palette/paletteGroups.ts +++ b/src/components/Palette/paletteGroups.ts @@ -12,8 +12,6 @@ export const PALETTE_GROUPS = [ { key: 'legacy' as ObjectGroup, labelKey: 'groupLegacy' }, ] as const; -export type PaletteGroupLabelKey = typeof PALETTE_GROUPS[number]['labelKey']; - /** Single source for the palette groups; each consumer maps entries onto its * own shape (flat browse, canvas add-here, favorites add). */ export function addableGroupsFor( diff --git a/src/components/Properties/DensityRescaleModal.tsx b/src/components/Properties/DensityRescaleModal.tsx index 673a8d52..1ac95b9d 100644 --- a/src/components/Properties/DensityRescaleModal.tsx +++ b/src/components/Properties/DensityRescaleModal.tsx @@ -3,8 +3,6 @@ import { useT } from "../../hooks/useT"; import { useLabelStore } from "../../store/labelStore"; import { rescaleDesign, rescaleParamsFor, type PendingDensity } from "../../lib/densityRescale"; -export type { PendingDensity } from "../../lib/densityRescale"; - interface Props { pending: PendingDensity; onClose: () => void; diff --git a/src/components/Properties/SectionCard.tsx b/src/components/Properties/SectionCard.tsx index 94f45de2..84ccd76f 100644 --- a/src/components/Properties/SectionCard.tsx +++ b/src/components/Properties/SectionCard.tsx @@ -15,23 +15,11 @@ function Dot({ active }: { active: boolean }) { ); } -/** Dot + title, shared by all three card header variants. `titleId` lets a - * control (e.g. the toggle switch) reference the title as its accessible name. */ -function CardLabel({ - active, - title, - titleId, -}: { - active: boolean; - title: ReactNode; - titleId?: string; -}) { +function CardLabel({ active, title }: { active: boolean; title: ReactNode }) { return ( <> - - {title} - + {title} ); } @@ -141,55 +129,3 @@ export function StaticSectionCard({ ); } - -/** - * Card whose header switch toggles a feature on/off (e.g. ^FB field block). - * The switch is the section's enable control, so the body shows only while - * on; there is no separate collapse and no persisted open-state (the toggled - * feature itself is the source of truth). - */ -export function ToggleSectionCard({ - title, - cmd, - checked, - onCheckedChange, - children, -}: { - title: ReactNode; - /** ZPL command this toggle controls (e.g. ^FB); shown when showZplCommands is on. */ - cmd?: string; - checked: boolean; - onCheckedChange: (checked: boolean) => void; - children: ReactNode; -}) { - const contentId = useId(); - const titleId = useId(); - return ( -
-
- -
- - -
-
- {checked && ( -
- {children} -
- )} -
- ); -} diff --git a/src/lib/zplParser.test.ts b/src/lib/zplParser.test.ts index cd5f836f..aa6438ed 100644 --- a/src/lib/zplParser.test.ts +++ b/src/lib/zplParser.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect, beforeAll } from 'vitest'; +import { splitTlc39Content } from '@zplab/core/lib/barcodeDims'; import { parseZPL, BY_CONSUMING_BARCODE_TYPES } from '@zplab/core/lib/zplParser'; import { generateZPL } from '@zplab/core/lib/zplGenerator'; import { formatLabelMetaComment } from '@zplab/core/lib/zplLabelMeta'; @@ -2724,18 +2725,15 @@ describe('parseZPL — ^BF MicroPDF417 mode', () => { }); describe('splitTlc39Content', () => { - it('splits on first comma; ECI before, serial after', async () => { - const { splitTlc39Content } = await import('../components/Canvas/bwipHelpers'); + it('splits on first comma; ECI before, serial after', () => { expect(splitTlc39Content('123456,ABC123')).toEqual({ eci: '123456', serial: 'ABC123' }); }); - it('keeps a leading S in the serial (spec p.141: stored verbatim)', async () => { - const { splitTlc39Content } = await import('../components/Canvas/bwipHelpers'); + it('keeps a leading S in the serial (spec p.141: stored verbatim)', () => { expect(splitTlc39Content('123456,SXYZ789')).toEqual({ eci: '123456', serial: 'SXYZ789' }); }); - it('returns empty serial when no comma is present', async () => { - const { splitTlc39Content } = await import('../components/Canvas/bwipHelpers'); + it('returns empty serial when no comma is present', () => { expect(splitTlc39Content('123456')).toEqual({ eci: '123456', serial: '' }); }); }); diff --git a/src/store/labelStore.ts b/src/store/labelStore.ts index 80687677..cb8b32a9 100644 --- a/src/store/labelStore.ts +++ b/src/store/labelStore.ts @@ -42,15 +42,10 @@ import { createLifecycleSlice, type LifecycleSlice } from './slices/lifecycleSli import { createSourceEditSlice, type SourceEditSlice } from './slices/sourceEditSlice'; import { createRestoreSlice, type ReplacedDesign, type RestoreSlice } from './slices/restoreSlice'; import { createAgentEditSlice, type AgentEditSlice } from './slices/agentEditSlice'; -import { - type ColumnMapping, - type Variable, - type VariableInput, -} from '@zplab/core/types/Variable'; +import { type ColumnMapping, type Variable } from '@zplab/core/types/Variable'; export { __resetPreviewCacheForTests } from './slices/previewSlice'; export type { ObjectChanges }; -export type { Variable, VariableInput }; /** Composed store shape; intersection of every slice. */ export type LabelState = diff --git a/src/test/font0PlacementMatch.test.ts b/src/test/font0PlacementMatch.test.ts index 07bd6a22..9446252a 100644 --- a/src/test/font0PlacementMatch.test.ts +++ b/src/test/font0PlacementMatch.test.ts @@ -16,9 +16,8 @@ import { /** * Font-0 placement gate: size AND position of the calibrated glyphs vs * Labelary, through the production chain (^FO anchor -> zplAnchorToModel - * -> Konva placement). Guards the font-patch results that the drift - * report only measures; a rebuilt TTF without the patch pipeline turns - * this red. + * -> Konva placement). Guards the font-patch results: a rebuilt TTF + * without the patch pipeline turns this red. */ const FIXTURES_DIR = path.resolve( diff --git a/tests/fixtures/font0GlyphCoverageCases.ts b/tests/fixtures/font0GlyphCoverageCases.ts index ced66a28..41b351ac 100644 --- a/tests/fixtures/font0GlyphCoverageCases.ts +++ b/tests/fixtures/font0GlyphCoverageCases.ts @@ -1,17 +1,9 @@ -/** Measurement input for the per-glyph drift report, deliberately NOT +/** Glyph cases the Labelary fetch keeps fixtures for, deliberately NOT * wired into the strict box-match test: measurement precedes gating. * Ids reuse textBoxMatchCases' scheme (see fixtureIdContract). */ import type { TextBoxMatchCase } from './textBoxMatchCases'; import { CHAR_SLUGS, charSlug } from './charSlug'; -export const UNSUPPORTED_CHARS = ['\u212e']; - -/** Residuals the calibration deliberately leaves: <=3 dots of accent - * ink on the I family and fraction/ellipsis inner spacing, plus the - * spacing accents, whose shared outlines cannot move to Zebra's - * standalone positions without breaking the accented composites. */ -export const KNOWN_DRIFT = [...'ÍÎÏÌìíîïýÿðøÙÚÛÜ«²³¹¼½¾…`\u00b4\u00b8\u2018\u2019\u201c\u201d']; - /** Ink-less space, NBSP and soft hyphen excluded; a missing slug * throws instead of silently dropping coverage. */ const LATIN1_PRINTABLE: string[] = []; diff --git a/tests/scripts/report_font0_glyph_drift.ts b/tests/scripts/report_font0_glyph_drift.ts deleted file mode 100644 index d233301f..00000000 --- a/tests/scripts/report_font0_glyph_drift.ts +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Per-glyph ink drift report: PrintLab ZPL (our Font-0 substitute) vs - * Labelary's default CG Triumvirate render, over the full coverage set - * (tests/fixtures/font0GlyphCoverageCases.ts). - * - * Decision tool for font-pipeline work: advance widths are already baked - * into the TTF, so what this surfaces is glyph SHAPE drift. - * - * Run: pnpm dlx tsx tests/scripts/report_font0_glyph_drift.ts - * (fetch fixtures first via fetch_labelary_default_text_fixtures.ts) - */ -import * as fs from 'fs'; -import * as path from 'path'; -import { PNG } from 'pngjs'; -import { zplAnchorToModel } from '@zplab/core/lib/labelGeometry/textPositionTransforms'; -import { font0GlyphCoverageCases, KNOWN_DRIFT } from '../fixtures/font0GlyphCoverageCases'; -import { darkBBox } from '../lib/darkBBox'; -import { drawKonvaText } from '../lib/drawKonvaText'; -import { inkCanvas } from '../lib/inkCanvas'; -import { - PRINTLAB_FONT_FAMILY, - registerPrintLabZpl, -} from '../lib/printLabFont'; - -const FIXTURES_DIR = path.resolve('tests/fixtures/labelary_text_default_images'); - -// Production placement: ^FO anchor -> zplAnchorToModel -> Konva line -// placement, so dY reads as real on-canvas position drift, not harness -// offset. -function renderLocal(tc: (typeof font0GlyphCoverageCases)[number]) { - const { canvas, ctx } = inkCanvas(); - const model = zplAnchorToModel( - tc.x, - tc.y, - { fontHeight: tc.fontHeight, rotation: tc.rotation }, - 'FO', - ); - drawKonvaText(ctx, { - text: tc.text, - x: model.x, - y: model.y, - fontSizePx: tc.fontHeight, - fontFamily: PRINTLAB_FONT_FAMILY, - fontStyle: 'bold', - }); - return darkBBox(PNG.sync.read(canvas.toBuffer('image/png'))); -} - -function main(): void { - registerPrintLabZpl(); - - interface Row { - id: string; - char: string; - dW: number; - dH: number; - dY: number; - relW: number; - relH: number; - local: ReturnType; - labelary: ReturnType; - } - const rows: Row[] = []; - const missing: string[] = []; - - for (const tc of font0GlyphCoverageCases) { - const fixture = path.join(FIXTURES_DIR, `${tc.id}.png`); - if (!fs.existsSync(fixture)) { - missing.push(tc.id); - continue; - } - const labelary = darkBBox(PNG.sync.read(fs.readFileSync(fixture))); - const local = renderLocal(tc); - rows.push({ - id: tc.id, - char: tc.text[0] ?? '', - dW: local.width - labelary.width, - dH: local.height - labelary.height, - dY: local.y - labelary.y, - relW: labelary.width > 0 ? (local.width - labelary.width) / labelary.width : 0, - relH: labelary.height > 0 ? (local.height - labelary.height) / labelary.height : 0, - local, - labelary, - }); - } - - rows.sort((a, b) => Math.abs(b.relW) - Math.abs(a.relW)); - - console.log('char local WxH labelary WxH dW dH dY relW% relH%'); - for (const r of rows) { - const flag = Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6 ? ' <<<' : ''; - console.log( - `${r.char.padEnd(5)} ${`${r.local.width}x${r.local.height}`.padEnd(11)} ` + - `${`${r.labelary.width}x${r.labelary.height}`.padEnd(14)} ` + - `${String(r.dW).padStart(3)} ${String(r.dH).padStart(3)} ` + - `${String(r.dY).padStart(3)} ` + - `${(r.relW * 100).toFixed(1).padStart(6)} ${(r.relH * 100).toFixed(1).padStart(6)}${flag}`, - ); - } - - const known = new Set(KNOWN_DRIFT); - const outliers = rows.filter( - (r) => - !known.has(r.char) && - (Math.abs(r.dW) > 6 || Math.abs(r.dH) > 6 || Math.abs(r.dY) > 6), - ); - const moderate = rows.filter( - (r) => - !outliers.includes(r) && - !known.has(r.char) && - (Math.abs(r.dW) > 3 || Math.abs(r.dH) > 3 || Math.abs(r.dY) > 3), - ); - console.log(`\n${rows.length} glyphs measured at h=50 (char x3).`); - console.log( - `Outliers >6 dots (box-match tolerance): ${outliers.length}` + - (outliers.length ? ` -> ${outliers.map((r) => r.char).join(' ')}` : ''), - ); - console.log( - `Moderate 4-6 dots: ${moderate.length}` + - (moderate.length ? ` -> ${moderate.map((r) => r.char).join(' ')}` : ''), - ); - if (missing.length) { - console.log( - `\nWARNING: ${missing.length} fixture(s) missing, run the fetch script: ${missing.join(', ')}`, - ); - } -} - -main();