All files / packages/sds-release-manager/src/services core-css-preparation-service.ts

90.74% Statements 49/54
91.93% Branches 57/62
90.9% Functions 10/11
94.11% Lines 48/51

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 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247                                                                                                                                                                          2x 2x       22x 22x 22x                     3x 3x             2x   1x           5x     5x     5x 2x   3x 1x   2x 1x   1x 7x                         1x     1x     1x     1x     1x     1x 7x                                               8x 8x 8x 8x 8x       1x 1x             7x     7x     7x 7x 7x           2x 2x 2x 2x 2x       5x       3x     3x 1x   2x                    
/**
 * Bridge from the release manager to the design-system package's Core CSS tooling.
 *
 * The design-system package owns the actual CSS generation, classification, and
 * evidence-packet production (its `scripts/core/*.js`). This service loads those
 * modules at runtime, builds the package once, and runs the preparation so the
 * audit command gets a verified evidence packet without the release manager
 * duplicating any CSS logic. The loaders are also used by the publish command to
 * re-derive the verdict from the same classifier.
 */
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { execa } from 'execa';
 
// One of the seven canonical V1 CSS artifacts: ids and the paths/storage that
// locate it in the candidate build and in gitcore.
export type CoreCssArtifactManifest = {
  id: string;
  candidatePath: string;
  gitcorePath: string;
  storage: 'git' | 'lfs';
};
 
export type CoreCssArtifact = { id: string; path: string; content: Buffer };
// Per-artifact provenance recorded in the evidence packet (git blob + API sizes,
// and the LFS oid/size for LFS-tracked files).
export type CoreCssArtifactProvenance = {
  id: string;
  path: string;
  storage: 'git' | 'lfs';
  gitBlobSha: string;
  gitBlobSize: number;
  apiSize: number;
  lfsOid?: string;
  lfsSize?: number;
};
// The pinned baseline the audit compared against: repo, target branch, immutable
// base SHA, and the per-artifact provenance.
export type CoreCssBaselineProvenance = {
  repo: string;
  target: string;
  baseSha: string;
  artifacts: CoreCssArtifactProvenance[];
};
// The evidence packet the audit writes and the publish command consumes.
export type CoreCssPreparationResult = {
  schemaVersion: number;
  version: string;
  sourceSha: string;
  parserVersion: string;
  verdict: 'no-change' | 'version-only' | 'content-change';
  reportDir: string;
  baseline: CoreCssBaselineProvenance;
  artifacts: Array<Record<string, string | number | boolean>>;
};
 
export interface ICoreCssPreparationService {
  getManifest(): Promise<CoreCssArtifactManifest[]>;
  prepare(
    baselineArtifacts: CoreCssArtifact[],
    baseline: CoreCssBaselineProvenance,
  ): Promise<CoreCssPreparationResult>;
}
 
// Shape of the dynamically-imported design-system generation module.
type DesignSystemAdapter = {
  manifest: CoreCssArtifactManifest[];
  prepareCoreCssRelease: (options: Record<string, unknown>) => Promise<CoreCssPreparationResult>;
};
// Shape of the dynamically-imported classifier module (used by publish to
// re-derive the verdict from bytes).
export type RepositoryClassifier = {
  manifest: CoreCssArtifactManifest[];
  classifyCoreCssArtifacts: (
    baseline: CoreCssArtifact[],
    candidate: CoreCssArtifact[],
  ) => {
    verdict: 'no-change' | 'version-only' | 'content-change';
    artifacts: Array<{ id: string; rawEqual: boolean; stampNormalizedEqual: boolean }>;
  };
};
type RunResult = { stdout: string; stderr: string };
type RunFn = (command: string, args: string[], options: { cwd: string }) => Promise<RunResult>;
 
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
const defaultDesignSystemRoot = path.resolve(packageRoot, '..', 'design-system');
 
// Runtime guard: a dynamically-imported manifest entry has the expected shape.
function isCoreCssArtifactManifest(value: unknown): value is CoreCssArtifactManifest {
  Iif (typeof value !== 'object' || value === null) return false;
  const artifact = value as Record<string, unknown>;
  return (
    typeof artifact.id === 'string' &&
    typeof artifact.candidatePath === 'string' &&
    typeof artifact.gitcorePath === 'string' &&
    (artifact.storage === 'git' || artifact.storage === 'lfs')
  );
}
 
// Read the built package version from the design-system dist, validating it is a
// non-empty string (it becomes the released version stamped into the packet).
export async function readDesignSystemPackageVersion(root: string): Promise<string> {
  const json: unknown = JSON.parse(await fs.readFile(path.join(root, 'dist/package.json'), 'utf8'));
  if (
    typeof json !== 'object' ||
    json === null ||
    !('version' in json) ||
    typeof json.version !== 'string' ||
    json.version.length === 0
  ) {
    throw new Error('Design system dist/package.json version must be a non-empty string');
  }
  return json.version;
}
 
// Dynamically import the design-system manifest + generation entry point,
// validating both exports before trusting them.
export async function loadRepositoryDesignSystem(root: string): Promise<DesignSystemAdapter> {
  const manifestModule = (await import(
    pathToFileURL(path.join(root, 'scripts/core/core-css-artifacts.js')).href
  )) as { CORE_CSS_ARTIFACTS?: unknown };
  const preparationModule = (await import(
    pathToFileURL(path.join(root, 'scripts/core/prepare-core-css-release.js')).href
  )) as { prepareCoreCssRelease?: unknown };
  if (!Array.isArray(manifestModule.CORE_CSS_ARTIFACTS) || manifestModule.CORE_CSS_ARTIFACTS.length !== 7) {
    throw new Error('Design system CORE_CSS_ARTIFACTS export must be an array of 7 artifacts');
  }
  if (!manifestModule.CORE_CSS_ARTIFACTS.every(isCoreCssArtifactManifest)) {
    throw new Error('Design system CORE_CSS_ARTIFACTS export has an invalid artifact shape');
  }
  if (typeof preparationModule.prepareCoreCssRelease !== 'function') {
    throw new Error('Design system prepareCoreCssRelease export must be a function');
  }
  return {
    manifest: (manifestModule.CORE_CSS_ARTIFACTS as CoreCssArtifactManifest[]).map((artifact) => ({
      ...artifact,
    })),
    prepareCoreCssRelease:
      preparationModule.prepareCoreCssRelease as DesignSystemAdapter['prepareCoreCssRelease'],
  };
}
 
// Dynamically import the design-system manifest + classifier, validating both
// exports. Used by the publish command to re-derive the verdict from bytes.
export async function loadRepositoryClassifier(
  root = defaultDesignSystemRoot,
): Promise<RepositoryClassifier> {
  const manifestModule = (await import(
    pathToFileURL(path.join(root, 'scripts/core/core-css-artifacts.js')).href
  )) as { CORE_CSS_ARTIFACTS?: unknown };
  const classifierModule = (await import(
    pathToFileURL(path.join(root, 'scripts/core/core-css-classifier.js')).href
  )) as { classifyCoreCssArtifacts?: unknown };
  Iif (!Array.isArray(manifestModule.CORE_CSS_ARTIFACTS) || manifestModule.CORE_CSS_ARTIFACTS.length !== 7) {
    throw new Error('Design system CORE_CSS_ARTIFACTS export must be an array of 7 artifacts');
  }
  Iif (!manifestModule.CORE_CSS_ARTIFACTS.every(isCoreCssArtifactManifest)) {
    throw new Error('Design system CORE_CSS_ARTIFACTS export has an invalid artifact shape');
  }
  Iif (typeof classifierModule.classifyCoreCssArtifacts !== 'function') {
    throw new Error('Design system classifyCoreCssArtifacts export must be a function');
  }
  return {
    manifest: (manifestModule.CORE_CSS_ARTIFACTS as CoreCssArtifactManifest[]).map((artifact) => ({
      ...artifact,
    })),
    classifyCoreCssArtifacts:
      classifierModule.classifyCoreCssArtifacts as RepositoryClassifier['classifyCoreCssArtifacts'],
  };
}
 
export class CoreCssPreparationService implements ICoreCssPreparationService {
  private readonly designSystemRoot: string;
  private readonly reportRoot: string;
  private readonly loadDesignSystem: (root: string) => Promise<DesignSystemAdapter>;
  private readonly run: RunFn;
  private readonly readPackageVersion: (root: string) => Promise<string>;
 
  constructor(
    options: {
      designSystemRoot?: string;
      reportRoot?: string;
      loadDesignSystem?: (root: string) => Promise<DesignSystemAdapter>;
      run?: RunFn;
      readPackageVersion?: (root: string) => Promise<string>;
    } = {},
  ) {
    this.designSystemRoot = options.designSystemRoot ?? defaultDesignSystemRoot;
    this.reportRoot = options.reportRoot ?? path.join(this.designSystemRoot, '.validation-reports/core-css');
    this.loadDesignSystem = options.loadDesignSystem ?? loadRepositoryDesignSystem;
    this.run = options.run ?? (async (command, args, runOptions) => execa(command, args, runOptions));
    this.readPackageVersion = options.readPackageVersion ?? readDesignSystemPackageVersion;
  }
 
  async getManifest(): Promise<CoreCssArtifactManifest[]> {
    const adapter = await this.loadDesignSystem(this.designSystemRoot);
    return adapter.manifest.map((artifact) => ({ ...artifact }));
  }
 
  async prepare(
    baselineArtifacts: CoreCssArtifact[],
    baseline: CoreCssBaselineProvenance,
  ): Promise<CoreCssPreparationResult> {
    const adapter = await this.loadDesignSystem(this.designSystemRoot);
    // Capture the source commit BEFORE the build so the recorded sourceSha
    // reflects the tree the candidate was actually generated from.
    const { stdout: sourceShaOutput } = await this.run('git', ['rev-parse', 'HEAD'], {
      cwd: this.designSystemRoot,
    });
    const sourceSha = sourceShaOutput.trim();
    try {
      await this.run('yarn', ['workspace', '@salesforce-ux/design-system', 'build'], {
        cwd: path.resolve(this.designSystemRoot, '..', '..'),
      });
    } catch (error) {
      // Summarize the build failure without assuming the thrown value's shape,
      // keeping the original as `cause`.
      const output = typeof error === 'object' && error !== null ? error : {};
      const message = 'message' in output && typeof output.message === 'string' ? output.message : undefined;
      const stdout = 'stdout' in output && typeof output.stdout === 'string' ? output.stdout : undefined;
      const stderr = 'stderr' in output && typeof output.stderr === 'string' ? output.stderr : undefined;
      throw new Error(['Design system build failed.', message, stdout, stderr].filter(Boolean).join('\n'), {
        cause: error,
      });
    }
    const [{ stdout: currentShaOutput }, version] = await Promise.all([
      this.run('git', ['rev-parse', 'HEAD'], { cwd: this.designSystemRoot }),
      this.readPackageVersion(this.designSystemRoot),
    ]);
    const currentSha = currentShaOutput.trim();
    // Guard against the checkout moving mid-build, which would make the candidate
    // bytes and the recorded sourceSha disagree.
    if (currentSha !== sourceSha) {
      throw new Error(`Design system HEAD changed during build: ${sourceSha} -> ${currentSha}`);
    }
    return adapter.prepareCoreCssRelease({
      baselineArtifacts,
      baseline,
      distPath: path.join(this.designSystemRoot, 'dist'),
      reportRoot: this.reportRoot,
      version,
      sourceSha,
    });
  }
}