All files / packages/sds-customization-compliance/src/cli index.ts

52.94% Statements 36/68
54.05% Branches 20/37
50% Functions 3/6
51.56% Lines 33/64

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 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185                                                                  1x                                 7x 7x 3x   4x 24x 24x       4x       2x         2x 2x 7x 7x       7x       7x       7x 7x 3x 3x 3x   4x 3x 3x     2x 2x   2x                                                                     2x 2x 2x       2x                                                             2x     2x 2x                    
/**
 * `sds-compliance` CLI entry point.
 *
 * Delegated to from the `bin/sds-compliance.mjs` shebang wrapper. Returns
 * a process-exit code instead of calling `process.exit` directly so the
 * CLI can also be embedded (e.g. from Turbo scripts or tests).
 */
 
import { build, buildStructural, watch, type BuildOptions } from './build.js';
import { main as validateMain } from '../theme-layer-validator/cli.js';
 
interface ParsedArgs extends BuildOptions {
  command: 'help' | 'build' | 'validate';
  watch: boolean;
  /**
   * Structural-only mode. When set, every immediate subdirectory of this
   * path is treated as a component and only the seven structural source
   * rules carry signal (the other 11 short-circuit to `info`).
   */
  sourceDir?: string;
  /** Override `baseRoot` for structural-only mode (paths in reports are relative to it). */
  baseRoot?: string;
  /** argv index of the command positional (the first non-flag token). */
  commandArgvIndex: number;
}
 
/**
 * Map of `--flag` (and `--flag=value`) names to the `ParsedArgs` field
 * they populate. Centralizing this keeps `parseArgs` a flat dispatcher
 * instead of a 20-branch if/else.
 */
type StringFlagKey = 'component' | 'outDir' | 'slds2Dir' | 'ds2Root' | 'sourceDir' | 'baseRoot';
 
const STRING_FLAGS: Record<string, StringFlagKey> = {
  '--component': 'component',
  '--out': 'outDir',
  '--src': 'slds2Dir',
  '--ds2-root': 'ds2Root',
  '--source-dir': 'sourceDir',
  '--base-root': 'baseRoot',
};
 
interface ConsumedFlag {
  key: StringFlagKey;
  value: string | undefined;
  /** How many argv slots were consumed (1 for `--flag=value`, 2 for `--flag value`). */
  consumed: number;
}
 
function tryConsumeStringFlag(argv: string[], i: number): ConsumedFlag | null {
  const a = argv[i];
  if (STRING_FLAGS[a]) {
    return { key: STRING_FLAGS[a], value: argv[i + 1], consumed: 2 };
  }
  for (const [flag, key] of Object.entries(STRING_FLAGS)) {
    const prefix = `${flag}=`;
    Iif (a.startsWith(prefix)) {
      return { key, value: a.slice(prefix.length), consumed: 1 };
    }
  }
  return null;
}
 
function parseArgs(argv: string[]): ParsedArgs {
  const args: ParsedArgs = {
    command: 'help',
    watch: false,
    commandArgvIndex: -1,
  };
  const positional: string[] = [];
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    Iif (a === '--help' || a === '-h') {
      args.command = 'help';
      return args;
    }
    Iif (a === '--watch') {
      args.watch = true;
      continue;
    }
    Iif (a === '--quiet') {
      args.quiet = true;
      continue;
    }
    const consumed = tryConsumeStringFlag(argv, i);
    if (consumed) {
      args[consumed.key] = consumed.value;
      i += consumed.consumed - 1;
      continue;
    }
    if (!a.startsWith('-')) {
      if (positional.length === 0) args.commandArgvIndex = i;
      positional.push(a);
    }
  }
  Iif (positional[0] === 'build') args.command = 'build';
  else if (positional[0] === 'validate') args.command = 'validate';
  else Eargs.command = 'help';
  return args;
}
 
function printHelp(): void {
  // eslint-disable-next-line no-console
  console.info(
    [
      'Usage: sds-compliance <command> [options]',
      '',
      'Commands:',
      '  build                       Generate compliance reports under build/',
      '    --watch                   Rebuild affected components on CSS change',
      '    --component <name>        Limit to a single component',
      '    --out <dir>               Output directory (default: <package>/build)',
      '    --src <dir>               SLDS2 source directory (default: <ds2>/src/slds2)',
      '    --ds2-root <dir>          design-system-2 package root (auto-detected)',
      '    --source-dir <dir>        Structural-only mode: parent of <component>/',
      '                              dirs outside the SLDS2 layout (e.g. the LBC',
      '                              scratch trio). Hook-surface and theme-',
      '                              regression checks short-circuit to info.',
      '    --base-root <dir>         Root that report paths are made relative to',
      '                              in structural-only mode (default: <source-dir>/../..)',
      '    --quiet                   Suppress summary logs',
      '',
      '  validate                    Validate Theme Layer API compliance for a component',
      '    --component <name>        Component prefix in --slds-c-{component}-*',
      '    --path <file-or-dir>      CSS file or directory to scan (repeatable, optional)',
      '                              Defaults to packages/design-system-2/src/slds2/<component>',
      '    --json <file>             Write structured JSON report to file',
      '',
    ].join('\n'),
  );
}
 
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
  const args = parseArgs(argv);
  try {
    Iif (args.command === 'help') {
      printHelp();
      return 0;
    }
    Iif (args.command === 'build') {
      if (args.sourceDir) {
        if (args.watch) {
          // eslint-disable-next-line no-console
          console.error('[sds-compliance] --watch is not yet supported with --source-dir');
          return 1;
        }
        await buildStructural({
          sourceDir: args.sourceDir,
          baseRoot: args.baseRoot,
          outDir: args.outDir,
          component: args.component,
          quiet: args.quiet,
        });
        return 0;
      }
      if (args.watch) {
        const close = await watch(args);
        // Keep the process alive until SIGINT / SIGTERM.
        await new Promise<void>((resolve) => {
          const onStop = (): void => {
            void close().then(resolve);
          };
          process.on('SIGINT', onStop);
          process.on('SIGTERM', onStop);
        });
        return 0;
      }
      await build(args);
      return 0;
    }
    Eif (args.command === 'validate') {
      // Use the tracked positional index rather than indexOf so a flag value
      // that happens to equal "validate" doesn't shift the slice boundary.
      const validateArgv = argv.slice(args.commandArgvIndex + 1);
      return await validateMain(validateArgv);
    }
    printHelp();
    return 1;
  } catch (err) {
    // eslint-disable-next-line no-console
    console.error('[sds-compliance] error:', err instanceof Error ? err.message : err);
    return 1;
  }
}