Poster sizes: one shared ladder for every print surface (paper, 3x3 ft, 3x5 ft, 5.5x5.5 ft, 5x8 ft)
library/constants/posterSizes.js gains POSTER_LADDER: five portrait sizes keyed WxH in inches, each with the sheets a printer feeds (named as panelSheets.js planPanels() takes them), whether a photo slot fits, and how it reaches paper (the browser's print for paper, a print-shop PDF for the rest). Legacy 24x36 and 36x48 still resolve, older names (3x3ft, 5x8ft, letter, a4...) alias to the ladder, and paperFor() picks Letter or A4 from a locale's region. The existing map-export presets are unchanged. check:poster-sizes (gates:pre) audits the table: the order matches the ladder, keys are dot-free, every size is portrait, the sheets glue up to the finished size, photos on exactly the two large sizes, paths, aliases and the paper fixtures. Its --self-test refuses 8 broken tables.
This commit is contained in:
parent
7303483fd2
commit
aace7cf884
3 changed files with 132 additions and 1 deletions
70
internal/check-poster-sizes.mjs
Normal file
70
internal/check-poster-sizes.mjs
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env node
|
||||
/**
|
||||
* check-poster-sizes.mjs — the poster-size LADDER (library/constants/posterSizes.js) says what it promises.
|
||||
*
|
||||
* Every print surface offers these sizes through one picker, and each renderer maps a key to its own
|
||||
* geometry. So the table itself must be right: a wrong sheet count or overlap prints a poster that does not
|
||||
* glue up to its size, and a landscape size breaks the portrait-only rule.
|
||||
*
|
||||
* ASSERTS: the picker order lists exactly the ladder; every key is dot-free (i18n paths, URLs); every size is
|
||||
* PORTRAIT (h ≥ w); the sheets glue up to the finished size (count × sheet − overlaps, in `direction`); a
|
||||
* photo slot is allowed on exactly the large sizes (66x66, 60x96); paper prints through the browser and the
|
||||
* rest through a print-shop PDF; every alias resolves; paperFor() picks Letter or A4 by region.
|
||||
* --self-test feeds broken copies of the table and must see each one refused.
|
||||
*/
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const S = await import(pathToFileURL(join(ROOT, 'library/constants/posterSizes.js')).href)
|
||||
|
||||
const PHOTO_SIZES = ['66x66', '60x96']
|
||||
function audit({ ladder, order, legacy, aliases }) {
|
||||
const bad = []
|
||||
const keys = Object.keys(ladder)
|
||||
if (order.join() !== keys.join()) bad.push(`the picker order [${order}] is not the ladder [${keys}]`)
|
||||
for (const [k, e] of [...Object.entries(ladder), ...Object.entries(legacy)]) {
|
||||
if (!/^[a-z0-9]+$/.test(k)) bad.push(`${k}: a key must be dot-free lowercase letters/digits`)
|
||||
if (e.id !== k) bad.push(`${k}: its id is "${e.id}"`)
|
||||
const { w, h } = e.inches
|
||||
if (!(h >= w)) bad.push(`${k}: ${w}×${h} in is landscape; every poster is portrait`)
|
||||
const s = e.sheets
|
||||
const W = s.direction === 'horizontal' ? s.count * s.sheetW - (s.count - 1) * s.joinIn : s.sheetW
|
||||
const H = s.direction === 'vertical' ? s.count * s.sheetH - (s.count - 1) * s.joinIn : s.sheetH
|
||||
if (W !== w || H !== h) bad.push(`${k}: ${s.count} sheet(s) of ${s.sheetW}×${s.sheetH} (${s.direction}, ${s.joinIn} in overlap) glue up to ${W}×${H}, not ${w}×${h}`)
|
||||
if (s.count === 1 && s.joinIn !== 0) bad.push(`${k}: one sheet cannot have a ${s.joinIn} in overlap`)
|
||||
}
|
||||
const photos = keys.filter((k) => ladder[k].photos)
|
||||
if (photos.join() !== PHOTO_SIZES.join()) bad.push(`photo slots are allowed on [${photos}], expected [${PHOTO_SIZES}] (the large sizes)`)
|
||||
for (const k of keys) {
|
||||
const want = ladder[k].paper ? 'print' : 'pdf'
|
||||
if (ladder[k].path !== want) bad.push(`${k}: path "${ladder[k].path}", expected "${want}"`)
|
||||
}
|
||||
for (const [a, t] of Object.entries(aliases)) if (!ladder[t] && !legacy[t]) bad.push(`alias ${a} → ${t}, which is not a size`)
|
||||
return bad
|
||||
}
|
||||
const table = { ladder: S.POSTER_LADDER, order: S.POSTER_LADDER_ORDER, legacy: S.POSTER_LADDER_LEGACY, aliases: S.POSTER_LADDER_ALIASES }
|
||||
const problems = audit(table)
|
||||
const paper = [['en-US', 'letter'], ['es-MX', 'letter'], ['fr-CA', 'letter'], ['en-GB', 'a4'], ['ar', 'a4'], ['zh-CN', 'a4'], ['en', 'letter']]
|
||||
for (const [tag, want] of paper) if (S.paperFor(tag) !== want) problems.push(`paperFor('${tag}') is ${S.paperFor(tag)}, expected ${want}`)
|
||||
if (S.resolvePosterSize('5x8ft')?.id !== '60x96' || S.resolvePosterSize('nope') !== null) problems.push('resolvePosterSize does not resolve an alias, or invents an unknown size')
|
||||
|
||||
if (process.argv.includes('--self-test')) {
|
||||
if (problems.length) { console.error('POSTER-SIZES SELF-TEST: FAIL — the real table already fails; fix that first.'); process.exit(1) }
|
||||
const clone = () => JSON.parse(JSON.stringify(table))
|
||||
const poisons = [
|
||||
['one sheet short', (t) => { t.ladder['60x96'].sheets.count = 2 }],
|
||||
['no overlap', (t) => { t.ladder['66x66'].sheets.joinIn = 0 }],
|
||||
['landscape', (t) => { t.ladder['36x60'].inches = { w: 60, h: 36 }; t.ladder['36x60'].sheets = { sheetW: 60, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 } }],
|
||||
['photos on a small size', (t) => { t.ladder['36x60'].photos = true }],
|
||||
['a dotted key', (t) => { t.ladder['5.5x5.5'] = { ...t.ladder['66x66'], id: '5.5x5.5' }; t.order.push('5.5x5.5') }],
|
||||
['order drift', (t) => { t.order.reverse() }],
|
||||
['a dangling alias', (t) => { t.aliases['4x6ft'] = '48x72' }],
|
||||
['paper through a PDF', (t) => { t.ladder.paper.path = 'pdf' }],
|
||||
]
|
||||
let missed = 0
|
||||
for (const [name, poison] of poisons) { const t = clone(); poison(t); const caught = audit(t).length > 0; console.log(` ${name}: ${caught ? 'caught' : 'NOT CAUGHT'}`); if (!caught) missed++ }
|
||||
if (missed) { console.error(`POSTER-SIZES SELF-TEST: FAIL — ${missed} poison(s) not caught.`); process.exit(1) }
|
||||
console.log(`POSTER-SIZES SELF-TEST: PASS — all ${poisons.length} broken tables refused.`); process.exit(0)
|
||||
}
|
||||
if (problems.length) { console.error('POSTER-SIZES: FAIL\n' + problems.map((p) => ' ✗ ' + p).join('\n')); process.exit(1) }
|
||||
console.log(`POSTER-SIZES: PASS — ${S.POSTER_LADDER_ORDER.length} sizes (${S.POSTER_LADDER_ORDER.join(' · ')}), portrait, sheets glue up, photos on ${PHOTO_SIZES.join(' + ')}, ${Object.keys(S.POSTER_LADDER_ALIASES).length} aliases resolve, paper by region.`)
|
||||
|
|
@ -96,3 +96,61 @@ export function listPosterSizes() {
|
|||
export function listByCategory(category) {
|
||||
return listPosterSizes().filter((s) => s.category === category);
|
||||
}
|
||||
|
||||
// ─── THE POSTER-SIZE LADDER (every print surface, 2026-09-30) ────────────────────────────────────
|
||||
// ONE table of the sizes a READER can pick for a poster, shared by every print surface (the poster
|
||||
// walls, the map's export, Bless, the map-print template) through ONE print picker. It is separate from
|
||||
// POSTER_SIZES above (the map exporter's US/ISO list), and written as plain literals, so a bundle that
|
||||
// never imports it carries none of it.
|
||||
//
|
||||
// KEYS are WxH in INCHES (this file's existing convention: '24x36', '27x40'); they are dot-free, so
|
||||
// they are safe in i18n key paths, URLs and stored choices. PORTRAIT ONLY: h ≥ w for every size (66x66
|
||||
// is square). `sheets` is what a printer actually feeds, named exactly as panelSheets.js's planPanels()
|
||||
// parameters, so a consumer passes it straight in: `count` sheets of sheetW × sheetH, stacked in
|
||||
// `direction`, each overlapping the next by `joinIn` (glued), which gives the finished w × h.
|
||||
// `photos` means a photo slot beside each name is ALLOWED at this size (there is room); which photos
|
||||
// print is the renderer's call. `path` is how the size reaches paper: 'print' = the browser's own print
|
||||
// (any home printer; a phone prints Letter/A4 only), 'pdf' = a print-shop PDF, one page per sheet at
|
||||
// exact inches, since no phone and few home printers take 3-8 ft paper.
|
||||
export const POSTER_LADDER = {
|
||||
paper: { id: 'paper', inches: { w: 8.5, h: 11 }, paper: true,
|
||||
sheets: { sheetW: 8.5, sheetH: 11, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'print' },
|
||||
'36x36': { id: '36x36', inches: { w: 36, h: 36 },
|
||||
sheets: { sheetW: 36, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||
'36x60': { id: '36x60', inches: { w: 36, h: 60 },
|
||||
sheets: { sheetW: 36, sheetH: 60, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||
'66x66': { id: '66x66', inches: { w: 66, h: 66 },
|
||||
sheets: { sheetW: 36, sheetH: 66, count: 2, direction: 'horizontal', joinIn: 6 }, photos: true, path: 'pdf' },
|
||||
'60x96': { id: '60x96', inches: { w: 60, h: 96 },
|
||||
sheets: { sheetW: 60, sheetH: 36, count: 3, direction: 'vertical', joinIn: 6 }, photos: true, path: 'pdf' },
|
||||
}
|
||||
/** The picker's order, smallest first. */
|
||||
export const POSTER_LADDER_ORDER = ['paper', '36x36', '36x60', '66x66', '60x96']
|
||||
/** A size that still resolves (stored choices, today's defaults) but is NOT offered in the picker. */
|
||||
export const POSTER_LADDER_LEGACY = {
|
||||
'24x36': { id: '24x36', inches: { w: 24, h: 36 },
|
||||
sheets: { sheetW: 24, sheetH: 36, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||
'36x48': { id: '36x48', inches: { w: 36, h: 48 },
|
||||
sheets: { sheetW: 36, sheetH: 48, count: 1, direction: 'vertical', joinIn: 0 }, photos: false, path: 'pdf' },
|
||||
}
|
||||
/** Older names for the same sizes (poster-render's FORMATS, Bless's PRESETS, the map exporter). */
|
||||
export const POSTER_LADDER_ALIASES = {
|
||||
'3x3': '36x36', '3x3ft': '36x36', '3x5': '36x60', '3x5ft': '36x60',
|
||||
'3x4': '36x48', '3x4ft': '36x48', '5x8': '60x96', '5x8ft': '60x96',
|
||||
'2x3': '24x36', '2x3ft': '24x36', letter: 'paper', a4: 'paper',
|
||||
}
|
||||
/** A key, an alias or a legacy key → its ladder entry; null when unknown. */
|
||||
export function resolvePosterSize(key) {
|
||||
const k = POSTER_LADDER_ALIASES[key] || key
|
||||
return POSTER_LADDER[k] || POSTER_LADDER_LEGACY[k] || null
|
||||
}
|
||||
// Countries that print on US Letter; everywhere else "Standard printer paper" means A4. Keyed by the
|
||||
// REGION of a locale tag (en-US, es-MX); a tag without a region falls back to its language's usual paper.
|
||||
const LETTER_REGIONS = new Set(['US', 'CA', 'MX', 'PH', 'CL', 'CO', 'VE', 'CR', 'GT', 'PA', 'DO', 'PR', 'SV', 'NI', 'HN', 'BO'])
|
||||
/** 'letter' or 'a4' for a locale tag (e.g. navigator.language). */
|
||||
export function paperFor(localeTag) {
|
||||
const m = String(localeTag || '').match(/^([a-z]{2,3})(?:[-_]([A-Za-z]{2}))?/i)
|
||||
if (!m) return 'letter'
|
||||
if (m[2]) return LETTER_REGIONS.has(m[2].toUpperCase()) ? 'letter' : 'a4'
|
||||
return m[1].toLowerCase() === 'en' ? 'letter' : 'a4'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,8 @@
|
|||
"check:coverage-consistent:poison": "node internal/check-gospel-coverage-consistent.mjs --poison",
|
||||
"check:csv-shape": "node internal/check-csv-export-shape.mjs",
|
||||
"check:csv-shape:self-test": "node internal/check-csv-export-shape.mjs --self-test",
|
||||
"check:poster-sizes": "node internal/check-poster-sizes.mjs",
|
||||
"check:poster-sizes:self-test": "node internal/check-poster-sizes.mjs --self-test",
|
||||
"check:dashboard-filter-a11y": "node plugin-designs/research-dashboard/scripts/check-dashboard-filter-a11y.mjs",
|
||||
"check:dashboard-geocoder-provider": "node plugin-designs/research-dashboard/scripts/check-dashboard-geocoder-provider.mjs",
|
||||
"check:deck-click": "node internal/check-deck-click.mjs",
|
||||
|
|
@ -347,7 +349,7 @@
|
|||
"fix:sri": "node internal/fix-sri.mjs",
|
||||
"gallery": "node internal/generate-gallery.mjs",
|
||||
"gates:post": "node internal/run-parallel.mjs --label=post -- check:capability-derived check:drop check:parse verify check:sri check:public-hygiene check:label-joins check:facet-cascade check:facet-one-field check:heartbeat check:kpi-unit check:composite-reg check:descriptions check:no-abs-paths check:no-codename check:dep-names check:composite-srcs check:embed-targets check:nested-token check:i18n-parity check:tabs-localized check:packets-all check:packet-strings-parity check:zone-census check:zoning-declared check:share-policy check:library-index check:csv-shape check:capability-citations check:catalog-identity check:no-orphan-gates check:quoted-scripts check:done-evidence check:script-flags check:backlog-status check:capability-exemplars check:packet-fixture check:packet-browser check:provider-switch check:locale-reachable check:url-resolver check:manifest-covers check:one-palette check:pin-dim-slider check:legend-source check:backlog-refs check:clustering-modes check:composite-embed-knobs check:composite-locale-leaks check:linked-vocabulary check:wp-embed check:dashboard-geocoder-provider check:leaf-engine-per-provider check:auto-engine check:engine-contract check:map-props-unit check:composites-mount check:composite-interop check:no-free-identifiers check:map-fly-reduced-motion check:mst-clustering check:list-prop-unit check:profiles-mount check:cdn-pins check:no-eval check:popup-escaping check:i18n-treeshake check:pinned-umd check:one-palette:self-test check:no-cross-plugin-imports check:no-cross-plugin-imports:self-test check:map-attribution check:studio-rail check:approx-pin-note check:embedder-keys check:provider-for check:legend-logical-insets check:legend-logical-insets:self-test check:optional-t-equivalence check:text-highlight check:religion-label-not-value check:i18n-stopped-scope check:no-silent-engine check:no-silent-engine:self-test check:regions-uses-library check:shadow-css-parses check:square-map-controls check:preset-config-merge check:instance-config-keys check:frozen-labels check:plural-forms check:region-fill-repaint check:region-paint-single-source check:poster-print-fallback check:posters-exportable check:country-names check:reduced-motion check:no-dead-library check:universal-props check:readme-size check:tree-relative check:leaf-hints check:snippet-engine check:snippet-engine:self-test check:embed-base check:link-coverage check:no-public-binds check:shared-reached check:tree-weight",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams",
|
||||
"gates:pre": "node internal/run-parallel.mjs --label=pre -- check:template-literal check:mjs-parse check:sfc-parse check:tab-radius check:synced-css check:doc-scripts check:setup-order check:package-json-unique-keys check:no-repo-pb-hooks check:no-credentials-omit check:no-credentials-omit:self-test check:shadow-reduced-motion check:poster-action-print check:targets check:targets:self-test check:browser-gates-guarded check:pin-selection check:facet-shape check:hardcoded-english:self-test check:list-selection check:selector-fields check:upg-zone-joins check:gospel-set-charts check:gospel-set-fit check:describe-group check:one-scroll-container check:packet-charts check:composite-token-url check:language-tree-layout check:i18n-keys-exist check:i18n-never-translated check:packet-variants check:profile-table check:element-bootstrap check:embed-url-absolute check:frame-target check:language-tree-model check:map-menu-anchor check:selection-kinds check:tiered-points check:whiteboard-projection check:mercator-slice check:ring-vocabulary check:map-props-unit check:mobile-gating check:provide-shadowing check:legend-top-offset check:cdn-cache check:plural-forms check:select-zone-race check:script-registry check:map-control-overlap check:poster-attribution check:board-map-options check:gospel-whiteboard check:packet-map-missing-loud check:poster-layout-optin check:ported-globals-identical check:ring-bands check:packet-a1-structure check:india-states check:batch-selection-export check:zone-title-tier-a check:evangelical-share check:map-labels check:packet-offline check:help-reachable check:gated-not-in-tree check:window-globals-released check:legend-data-contract check:no-new-driver-references check:poster-fetch-guard check:people-group-records check:sentence-slots check:popup-href-escaped check:popup-pan-controls check:country-map-refetch check:pane-config-reforward check:viewport-meta check:viewport-meta:self-test check:label-key-coverage check:poster-browser-equiv check:poster-render-pinned check:country-sheet-one-path check:country-sheet-one-path:self-test check:posterverse-shelf check:host-locale-fetch check:print-frame-reclaim check:click-sheet check:coverage-consistent:chained check:coverage-consistent:poison check:dashboard-filter-a11y check:filter-multivalue check:hover-channel check:kit-options-reachable check:layout-key-namespaced check:legend-mirrors check:map-unmount-race check:no-conflict-markers check:series-isolation check:snapshot-guard check:stale-sweep-claim check:surfaces-read-the-tier check:translator-contacts check:dxlife check:wall-wysiwyg check:wall-print-watch check:wall-no-bg-photos check:wall-pan check:template-i18n check:list-pattern-doc check:shadow-style-labels-unique check:grid-image-loading check:retry-fetch check:panel-sheets check:poster-wall-host-seams check:poster-sizes",
|
||||
"gen:iso3-to-alpha2": "node library/scripts/generate-iso3-to-alpha2.mjs",
|
||||
"internet:adm1": "node plugin-designs/cities-internet/scripts/build-adm1-internet.mjs",
|
||||
"library-index": "node internal/generate-library-index.mjs",
|
||||
|
|
@ -455,6 +457,7 @@
|
|||
"check:country-sheet-one-path:self-test": "plants each defect check:country-sheet-one-path guards (a direct generateCountrySheet call, makeCountrySheet without countries, the old guard that skipped India, a position given to an absent code) and requires each to FAIL; a comment naming generateCountrySheet must NOT.",
|
||||
"check:coverage-consistent": "the gospel-resources surfaces must agree: no resource on an ISO RESERVED code (und/mul/mis/zxx), every resourced code NAMED, table reach recounted off the pins, no invented URL, tiers reconcile. Static, sub-second. Self-test: `bun run check:coverage-consistent:poison`, all 12 clauses (A-L) must fail. Clause L is the one that is not about consistency: it fails when the link-verification record goes stale, because 'link checked' is a claim about the present tense. Written after the same drift defect was found by hand three times.",
|
||||
"check:csv-shape": "the export must OPEN as a table: header on line 0, every row matching. Run the `check:csv-shape:self-test` script for the NEGATIVE test (forces the provenance comment back to line 0 IN MEMORY and requires this gate to REJECT it) — the header used to claim 'NEGATIVE-TESTED' with no way to re-run it.",
|
||||
"check:poster-sizes": "the shared poster-size ladder (library/constants/posterSizes.js) is right: picker order = ladder, dot-free keys, portrait only, the sheets glue up to the finished size, photo slots on exactly 66x66 + 60x96, paper prints and the rest go to a print-shop PDF, every alias resolves, paperFor() picks Letter or A4 by region. Run the `check:poster-sizes:self-test` script for the NEGATIVE test",
|
||||
"check:dashboard-filter-a11y": "the dashboard's cross-filter is operable and audible: every filter chip is a <button type=\"button\"> with an aria-label naming the ACTION (a filter that can be set by keyboard must be clearable by keyboard), the shell carries exactly ONE always-rendered polite role=\"status\" region so a brush that changes a number far from the control is announced once rather than six times or not at all, and no resize handle's accessible name is a raw English literal. AST over the parsed templates plus the built bundle — no browser (rega's alone).",
|
||||
"check:dashboard-geocoder-provider": "the research dashboard leaf at ?provider=maplibre makes ZERO requests to api.mapbox.com, and its search bars still suggest (built tree; browser gate). It was kept out of gates:post while the generated leaf hard-linked the Mapbox engine; the leaf now loads only the engine it is asked for.",
|
||||
"check:dep-names": "every declared dep is a REAL package name (a rename once rewrote @capacitor/* and broke bun install in a clone)",
|
||||
|
|
|
|||
Loading…
Reference in a new issue