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

0% Statements 0/59
0% Branches 0/31
0% Functions 0/6
0% Lines 0/57

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                                                                                                                                                                                                                                                                                                                                       
/**
 * `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';
 
interface ParsedArgs extends BuildOptions {
  command: 'help' | 'build';
  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;
}
 
/**
 * 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}=`;
    if (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,
  };
  const positional: string[] = [];
  for (let i = 0; i < argv.length; i++) {
    const a = argv[i];
    if (a === '--help' || a === '-h') {
      args.command = 'help';
      return args;
    }
    if (a === '--watch') {
      args.watch = true;
      continue;
    }
    if (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('-')) positional.push(a);
  }
  args.command = positional[0] === 'build' ? 'build' : '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',
      '',
    ].join('\n'),
  );
}
 
export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
  const args = parseArgs(argv);
  try {
    if (args.command === 'help') {
      printHelp();
      return 0;
    }
    if (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;
    }
    printHelp();
    return 1;
  } catch (err) {
    // eslint-disable-next-line no-console
    console.error('[sds-compliance] error:', err instanceof Error ? err.message : err);
    return 1;
  }
}