From 090ac09d0881038341113acc2a968125ae980435 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 09:46:46 -0500 Subject: [PATCH 1/3] chore: add figma code connect coverage guard to lint pipeline --- libs/core/package.json | 1 + libs/core/scripts/check-component-status.mjs | 33 +----- libs/core/scripts/check-figma-coverage.mjs | 106 +++++++++++++++++++ libs/core/scripts/lib/components.mjs | 43 ++++++++ libs/figma/code-connect-waivers.json | 20 ++++ 5 files changed, 171 insertions(+), 32 deletions(-) create mode 100644 libs/core/scripts/check-figma-coverage.mjs create mode 100644 libs/core/scripts/lib/components.mjs create mode 100644 libs/figma/code-connect-waivers.json diff --git a/libs/core/package.json b/libs/core/package.json index 6e1be3dbb..7dd658467 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -61,6 +61,7 @@ "generate": "stencil generate", "lint": "run-p lint.*", "lint.eslint": "eslint src", + "lint.figma": "node scripts/check-figma-coverage.mjs", "lint.status": "node scripts/check-component-status.mjs", "lint.styles": "stylelint \"./src/**/*.*css\"", "prettier": "prettier \"./src/**/*.{html,ts,tsx,js,jsx}\"", diff --git a/libs/core/scripts/check-component-status.mjs b/libs/core/scripts/check-component-status.mjs index 2938ab034..d1818e1e7 100644 --- a/libs/core/scripts/check-component-status.mjs +++ b/libs/core/scripts/check-component-status.mjs @@ -16,42 +16,11 @@ */ import fs from 'node:fs'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { CORE_ROOT, topLevelComponents, allComponentDirs } from './lib/components.mjs'; -const CORE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); -const COMPONENTS_DIR = path.join(CORE_ROOT, 'src', 'components'); const MANIFEST_PATH = path.join(CORE_ROOT, 'src', 'stories', 'resources', 'component-status.json'); const MANIFEST_REL = path.relative(CORE_ROOT, MANIFEST_PATH); -/** Directory names under `src/components` matching `pds-*`. */ -function pdsDirs(dir) { - return fs - .readdirSync(dir, { withFileTypes: true }) - .filter((entry) => entry.isDirectory() && entry.name.startsWith('pds-')) - .map((entry) => entry.name); -} - -/** Top-level components — these each require a status entry. */ -function topLevelComponents() { - return pdsDirs(COMPONENTS_DIR).sort(); -} - -/** - * Every `pds-*` directory, including nested subcomponents. Used to validate - * manifest entries so a deliberate subcomponent entry (e.g. `pds-filter`) is - * accepted while a typo or removed component is not. - */ -function allComponentDirs() { - const names = new Set(); - for (const top of topLevelComponents()) { - names.add(top); - for (const nested of pdsDirs(path.join(COMPONENTS_DIR, top))) { - names.add(nested); - } - } - return names; -} - function readManifestKeys() { let raw; try { diff --git a/libs/core/scripts/check-figma-coverage.mjs b/libs/core/scripts/check-figma-coverage.mjs new file mode 100644 index 000000000..9b793c9b3 --- /dev/null +++ b/libs/core/scripts/check-figma-coverage.mjs @@ -0,0 +1,106 @@ +/** + * Figma Code Connect coverage check. + * + * Guards that every top-level `pds-*` component is either: + * - mapped: has a `libs/figma/components/.figma.ts`, or + * - explicitly waived: listed in `libs/figma/code-connect-waivers.json`. + * + * This stops a new component from silently shipping with no Code Connect + * mapping and no deliberate decision. It also flags stale waivers (a waived + * component that has since been mapped, or that no longer exists) so the + * waiver list cannot rot. + * + * Runs as `lint.figma` via `run-p lint.*`, gated through the existing + * `nx affected --target=lint` CI job — no workflow changes needed. + * + * Exits 0 when coverage is accounted for, 1 (with an actionable report) otherwise. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { CORE_ROOT, topLevelComponents } from './lib/components.mjs'; + +const FIGMA_ROOT = path.resolve(CORE_ROOT, '..', 'figma'); +const FIGMA_COMPONENTS_DIR = path.join(FIGMA_ROOT, 'components'); +const WAIVERS_PATH = path.join(FIGMA_ROOT, 'code-connect-waivers.json'); +const WAIVERS_REL = path.relative(path.resolve(CORE_ROOT, '..', '..'), WAIVERS_PATH); + +/** Component names with a `.figma.ts` under libs/figma/components. */ +function mappedComponents() { + if (!fs.existsSync(FIGMA_COMPONENTS_DIR)) return new Set(); + const names = fs + .readdirSync(FIGMA_COMPONENTS_DIR, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith('.figma.ts')) + .map((entry) => entry.name.replace(/\.figma\.ts$/, '')); + return new Set(names); +} + +function readWaivers() { + let raw; + try { + raw = fs.readFileSync(WAIVERS_PATH, 'utf8'); + } catch { + throw new Error(`Could not read waivers at ${WAIVERS_REL}`); + } + let parsed; + try { + parsed = JSON.parse(raw); + } catch (err) { + throw new Error(`Waivers file ${WAIVERS_REL} is not valid JSON: ${err.message}`); + } + if (!parsed.waived || typeof parsed.waived !== 'object') { + throw new Error(`Waivers file ${WAIVERS_REL} is missing a "waived" object`); + } + return new Set(Object.keys(parsed.waived)); +} + +function main() { + let waived; + try { + waived = readWaivers(); + } catch (err) { + console.error(`✖ figma-coverage check failed: ${err.message}`); + process.exit(1); + } + + const components = topLevelComponents(); + const mapped = mappedComponents(); + const componentSet = new Set(components); + + // A component with neither a mapping nor a waiver is an unaccounted gap. + const uncovered = components.filter((name) => !mapped.has(name) && !waived.has(name)); + // A waiver for a component that is now mapped should be removed. + const redundant = [...waived].filter((name) => mapped.has(name)).sort(); + // A waiver for a component that no longer exists is stale. + const orphaned = [...waived].filter((name) => !componentSet.has(name)).sort(); + + if (uncovered.length === 0 && redundant.length === 0 && orphaned.length === 0) { + console.log( + `✔ figma code connect coverage accounted for ` + + `(${mapped.size} mapped, ${waived.size} waived, ${components.length} components).`, + ); + return; + } + + console.error(`✖ figma code connect coverage is unaccounted for.\n`); + if (uncovered.length > 0) { + console.error(`Components with no .figma.ts and no waiver:`); + uncovered.forEach((name) => console.error(` - ${name}`)); + console.error( + ` → add libs/figma/components/.figma.ts, ` + + `or add an entry to ${WAIVERS_REL} with a reason.\n`, + ); + } + if (redundant.length > 0) { + console.error(`Waivers for components that are already mapped (remove the waiver):`); + redundant.forEach((name) => console.error(` - ${name}`)); + console.error(''); + } + if (orphaned.length > 0) { + console.error(`Waivers for components that no longer exist (remove or rename):`); + orphaned.forEach((name) => console.error(` - ${name}`)); + console.error(''); + } + process.exit(1); +} + +main(); diff --git a/libs/core/scripts/lib/components.mjs b/libs/core/scripts/lib/components.mjs new file mode 100644 index 000000000..4b8e6502e --- /dev/null +++ b/libs/core/scripts/lib/components.mjs @@ -0,0 +1,43 @@ +/** + * Shared component-enumeration helpers for the maturity lint checks + * (`check-component-status.mjs`, `check-figma-coverage.mjs`). + * + * Single source for "what are the components" so the guards cannot diverge on + * their globbing logic. + */ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +/** Repo `libs/core` root (this file lives at `libs/core/scripts/lib/`). */ +export const CORE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'); +export const COMPONENTS_DIR = path.join(CORE_ROOT, 'src', 'components'); + +/** Directory names directly under `dir` matching `pds-*`. */ +function pdsDirs(dir) { + return fs + .readdirSync(dir, { withFileTypes: true }) + .filter((entry) => entry.isDirectory() && entry.name.startsWith('pds-')) + .map((entry) => entry.name); +} + +/** Top-level components — the public surface that needs status + Figma coverage. */ +export function topLevelComponents() { + return pdsDirs(COMPONENTS_DIR).sort(); +} + +/** + * Every `pds-*` directory, including nested subcomponents (e.g. `pds-filter`, + * `pds-table-cell`). Used to accept deliberate subcomponent references while + * rejecting typos or removed components. + */ +export function allComponentDirs() { + const names = new Set(); + for (const top of topLevelComponents()) { + names.add(top); + for (const nested of pdsDirs(path.join(COMPONENTS_DIR, top))) { + names.add(nested); + } + } + return names; +} diff --git a/libs/figma/code-connect-waivers.json b/libs/figma/code-connect-waivers.json new file mode 100644 index 000000000..a1e753655 --- /dev/null +++ b/libs/figma/code-connect-waivers.json @@ -0,0 +1,20 @@ +{ + "$comment": "Machine-readable source of truth for INTENTIONALLY unmapped Pine components. A top-level pds-* component passes the Figma Code Connect coverage guard (libs/core/scripts/check-figma-coverage.mjs) when it has a matching libs/figma/components/.figma.ts OR an entry here. Remove a component's entry once it is mapped. Keep CODE_CONNECT_COVERAGE.md in sync (a future change can render that doc from this file).", + "waived": { + "pds-accordion": "No Figma Code Connect mapping yet; add components/pds-accordion.figma.ts when the Figma node is ready.", + "pds-box": "Layout primitive; may map to layout frames rather than a single component node.", + "pds-combobox": "No Figma Code Connect mapping yet; add components/pds-combobox.figma.ts when the Figma node is ready.", + "pds-container": "No Figma Code Connect mapping yet; add components/pds-container.figma.ts when the Figma node is ready.", + "pds-copytext": "No Figma Code Connect mapping yet; add components/pds-copytext.figma.ts when the Figma node is ready.", + "pds-dropdown-menu": "No Figma Code Connect mapping yet; add components/pds-dropdown-menu.figma.ts when the Figma node is ready.", + "pds-icon": "Icon set lives in @pine-ds/icons; map if Figma documents a dedicated icon wrapper.", + "pds-image": "No Figma Code Connect mapping yet; add components/pds-image.figma.ts when the Figma node is ready.", + "pds-multiselect": "No Figma Code Connect mapping yet; add components/pds-multiselect.figma.ts when the Figma node is ready.", + "pds-popover": "No Figma Code Connect mapping yet; add components/pds-popover.figma.ts when the Figma node is ready.", + "pds-row": "Often used with pds-box; may share layout documentation.", + "pds-sortable": "Figma substitutions exist for sortable list patterns; wire components/pds-sortable.figma.ts when aligned.", + "pds-table": "Composite; consider per-subcomponent mappings (row, cell, head) if needed.", + "pds-text": "No Figma Code Connect mapping yet; add components/pds-text.figma.ts when the Figma node is ready.", + "pds-tooltip": "No Figma Code Connect mapping yet; add components/pds-tooltip.figma.ts when the Figma node is ready." + } +} From 85ef4b7d4df649804c5f5b45f5a552df5bd0f10d Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Mon, 1 Jun 2026 14:38:49 -0500 Subject: [PATCH 2/3] fix: harden figma-coverage waiver and shared component-dir helper --- libs/core/scripts/check-figma-coverage.mjs | 2 +- libs/core/scripts/lib/components.mjs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/libs/core/scripts/check-figma-coverage.mjs b/libs/core/scripts/check-figma-coverage.mjs index 9b793c9b3..9fb58dbc1 100644 --- a/libs/core/scripts/check-figma-coverage.mjs +++ b/libs/core/scripts/check-figma-coverage.mjs @@ -47,7 +47,7 @@ function readWaivers() { } catch (err) { throw new Error(`Waivers file ${WAIVERS_REL} is not valid JSON: ${err.message}`); } - if (!parsed.waived || typeof parsed.waived !== 'object') { + if (!parsed.waived || typeof parsed.waived !== 'object' || Array.isArray(parsed.waived)) { throw new Error(`Waivers file ${WAIVERS_REL} is missing a "waived" object`); } return new Set(Object.keys(parsed.waived)); diff --git a/libs/core/scripts/lib/components.mjs b/libs/core/scripts/lib/components.mjs index 4b8e6502e..f0d153ecf 100644 --- a/libs/core/scripts/lib/components.mjs +++ b/libs/core/scripts/lib/components.mjs @@ -33,9 +33,16 @@ export function topLevelComponents() { */ export function allComponentDirs() { const names = new Set(); + // Top-level pds-* components. for (const top of topLevelComponents()) { names.add(top); - for (const nested of pdsDirs(path.join(COMPONENTS_DIR, top))) { + } + // Nested pds-* subcomponents under ANY top-level directory — including + // non-pds-* parents like `_internal/` (e.g. `_internal/pds-label`) — so a + // deliberate subcomponent reference is not falsely rejected. + for (const entry of fs.readdirSync(COMPONENTS_DIR, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + for (const nested of pdsDirs(path.join(COMPONENTS_DIR, entry.name))) { names.add(nested); } } From ba14f2de4bde906ed9c4fb85332881e0f3d53e35 Mon Sep 17 00:00:00 2001 From: Quinton Jason Date: Fri, 5 Jun 2026 09:57:27 -0500 Subject: [PATCH 3/3] fix: wire figma paths into nx affected and lint cache inputs Register @pine-ds/figma as a workspace project and declare an implicit dependency from core so figma-only PRs still schedule lint.figma, and add waiver/mapping paths to core lint inputs so Nx cache invalidates when those files change. --- libs/core/package.json | 8 ++++++++ libs/core/scripts/check-figma-coverage.mjs | 5 ++++- libs/figma/package.json | 6 ++++++ package-lock.json | 8 ++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 libs/figma/package.json diff --git a/libs/core/package.json b/libs/core/package.json index 7dd658467..d37573299 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -127,7 +127,15 @@ "webpack": "^5.74.0" }, "nx": { + "implicitDependencies": ["@pine-ds/figma"], "targets": { + "lint": { + "inputs": [ + "default", + "{workspaceRoot}/libs/figma/code-connect-waivers.json", + "{workspaceRoot}/libs/figma/components/**/*.figma.ts" + ] + }, "start.stencil": { "dependsOn": [ { diff --git a/libs/core/scripts/check-figma-coverage.mjs b/libs/core/scripts/check-figma-coverage.mjs index 9fb58dbc1..3c1ae6771 100644 --- a/libs/core/scripts/check-figma-coverage.mjs +++ b/libs/core/scripts/check-figma-coverage.mjs @@ -11,7 +11,10 @@ * waiver list cannot rot. * * Runs as `lint.figma` via `run-p lint.*`, gated through the existing - * `nx affected --target=lint` CI job — no workflow changes needed. + * `nx affected --target=lint` CI job. `@pine-ds/core` declares an implicit + * dependency on `@pine-ds/figma` so figma-only PRs still schedule core lint, + * and the core `lint` target inputs include waiver/mapping paths so Nx cache + * invalidates when those files change. * * Exits 0 when coverage is accounted for, 1 (with an actionable report) otherwise. */ diff --git a/libs/figma/package.json b/libs/figma/package.json new file mode 100644 index 000000000..ba4fa348f --- /dev/null +++ b/libs/figma/package.json @@ -0,0 +1,6 @@ +{ + "name": "@pine-ds/figma", + "version": "0.0.0", + "private": true, + "description": "Figma Code Connect sources for Pine (not published)" +} diff --git a/package-lock.json b/package-lock.json index 1e747a395..82cc8286a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -890,6 +890,10 @@ "loose-envify": "^1.1.0" } }, + "libs/figma": { + "name": "@pine-ds/figma", + "version": "0.0.0" + }, "libs/react": { "name": "@pine-ds/react", "version": "3.26.1", @@ -6834,6 +6838,10 @@ "resolved": "libs/doc-components", "link": true }, + "node_modules/@pine-ds/figma": { + "resolved": "libs/figma", + "link": true + }, "node_modules/@pine-ds/icons": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@pine-ds/icons/-/icons-10.0.0.tgz",