All files / packages/design-system-2/scripts buildLegacyHookData.ts

80.85% Statements 38/47
78.57% Branches 11/14
100% Functions 6/6
78.04% Lines 32/41

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133                                                  1x           10x 10x 15x 14x   10x             10x 10x 19x 19x 18x 18x 18x   14x 10x             6x 6x 6x   6x                       9x 6x       4x 1x   3x 3x 3x           3x 3x 3x       4x                 1x 1x                                                  
#!/usr/bin/env node
/**
 * SLDS2 legacy hook audit.
 *
 * Walks the SLDS2 legacy Cosmos bundle (the pre-theme-layer output) and emits a
 * JSON catalog that groups every `--slds-c-*` / `--_slds-c-*` hook by component
 * prefix. The result is the authoritative record of what the legacy SLDS2 CSS
 * exposed as a hook surface — useful for detecting customer hooks that came in
 * through that bundle and need to be renormalized onto the current theme layer.
 *
 * Output shape mirrors slds1-hook-data.json:
 *   { _meta: { hookCount, componentCount, source }, <component>: [hook, ...] }
 *
 * Output path: build/legacy-hooks/slds2-legacy-hook-data.json. Consumed by the
 * catalog build, which re-exports the JSON as `legacyHookData` on the catalog
 * module.
 *
 * Run via `yarn build:legacy-hooks`.
 */
 
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { HOOK_RE } from './constants.js';
 
export const PREFIX_RE = /^--_?slds-c-([a-z][a-z0-9]*)-/;
 
// Pure: extract every distinct `--slds-c-*` hook from a CSS source. Skip
// context hooks (those are parent-set assignments, not the component's
// own surface). Returns a Set.
export function extractHooks(css) {
  const hooks = new Set();
  for (const [name] of css.matchAll(HOOK_RE)) {
    if (name.includes('-context-')) continue;
    hooks.add(name);
  }
  return hooks;
}
 
// Pure: group hooks by their component prefix (the first squashed-lowercase
// segment after `--slds-c-` or `--_slds-c-`). Returns Map<prefix, hook[]>
// with hook arrays alphabetically sorted.
export function groupByComponent(hooks) {
  const out = new Map();
  for (const hook of hooks) {
    const m = hook.match(PREFIX_RE);
    if (!m) continue;
    const key = m[1];
    if (!out.has(key)) out.set(key, []);
    out.get(key).push(hook);
  }
  for (const arr of out.values()) arr.sort();
  return out;
}
 
// Pure-ish: build the final JSON payload from CSS + metadata. The source path
// in `_meta` is normalized to be repo-relative by the caller (we accept it as
// an arg so tests can pass a stable string).
export function buildPayload({ css, sourceRelative, sourceMtime }) {
  const hooks = extractHooks(css);
  const byComponent = groupByComponent(hooks);
  const sorted = [...byComponent.entries()].sort(([a], [b]) => a.localeCompare(b));
 
  const out = {
    _meta: {
      // Record the source and its mtime instead of a wall-clock timestamp so
      // the file only changes when the underlying bundle does.
      hookCount: hooks.size,
      componentCount: sorted.length,
      source: {
        path: sourceRelative,
        mtime: sourceMtime,
      },
    },
  };
  for (const [comp, list] of sorted) out[comp] = list;
  return out;
}
 
export function build({ source, outFile, pkgRoot }) {
  if (!fs.existsSync(source)) {
    throw new Error(`source not found: ${path.relative(pkgRoot, source)}`);
  }
  const stat = fs.statSync(source);
  const css = fs.readFileSync(source, 'utf8');
  const payload = buildPayload({
    css,
    sourceRelative: path.relative(pkgRoot, source),
    sourceMtime: stat.mtime.toISOString(),
  });
 
  fs.mkdirSync(path.dirname(outFile), { recursive: true });
  fs.writeFileSync(outFile, JSON.stringify(payload, null, 2) + '\n');
  return payload;
}
 
export function defaultPaths(pkgRoot) {
  return {
    pkgRoot,
    source: path.join(pkgRoot, 'dist/css/bundled/slds2.cosmos.css'),
    outFile: path.join(pkgRoot, 'build/legacy-hooks/slds2-legacy-hook-data.json'),
  };
}
 
// Only run when invoked as the entry module — keeps unit tests from triggering
// a real file write on import.
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
Iif (isMain) {
  const paths = defaultPaths(path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'));
  // The legacy audit reads the bundled production CSS, which only
  // `build:css:production` emits — `build:dev` produces modular CSS only. This
  // step is wired into both build phases, so skip (not fail) when the bundle is
  // absent: the dev phase has nothing to read, and the catalog build already
  // treats the output as optional (falls back to `{}`). Production runs this
  // after `build:css:production`, so the real data still gets written there.
  if (!fs.existsSync(paths.source)) {
    console.warn(
      `[legacy-hooks] skipped: ${path.relative(paths.pkgRoot, paths.source)} not found ` +
        '(only produced by `build:css:production`).',
    );
    process.exit(0);
  }
  try {
    const payload = build(paths);
    console.log(
      `[legacy-hooks] wrote ${path.relative(paths.pkgRoot, paths.outFile)} (${payload._meta.hookCount} hooks, ${payload._meta.componentCount} components)`,
    );
  } catch (err) {
    console.error(`[legacy-hooks] ${err.message}`);
    process.exit(1);
  }
}