Ctrl K
ComunidadGuías…

scripts/e2e-diagram-scope.mjs

Rama develop · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
/**
 * Detects which Playwright e2e spec files to run based on changed source files.
 *
 * Convention: every diagram type has a matching subfolder under
 * e2e/rendering/<diagram-name>/. Adding a new spec file to
 * that subfolder requires no configuration here — it is discovered at runtime.
 * Diagram fixtures live under `e2e/diagrams/<area>/*.mmd` and are run by the
 * global runner e2e/rendering/mmd-snapshots.spec.ts; a fixture-only change
 * scopes to that runner.
 *
 * CLI usage (reads changed file paths from stdin, one per line):
 *   git diff --name-only <base> HEAD | node scripts/e2e-diagram-scope.mjs
 *
 * Output:
 *   - A comma-separated list of spec patterns (for playwright test), OR
 *   - An empty string if the full suite should run (shared code changed, or
 *     unable to confidently scope the change)
 *
 * Module usage:
 *   import { detectScope } from './e2e-diagram-scope.mjs';
 *   const spec = detectScope(['packages/mermaid/src/diagrams/flowchart/flowchart.ts']);
 *   // => 'e2e/rendering/flowchart/'
 */

import { createInterface } from 'readline';
import { existsSync, readdirSync } from 'fs';
import { fileURLToPath } from 'url';

// Base directory where diagram spec subfolders live
export const SPEC_BASE_DIR = 'e2e/rendering';

// Where the global mmd snapshot runner's fixtures live.
export const DIAGRAMS_DIR = 'e2e/diagrams';

// Source diagram folders mix kebab- and camelCase (quadrant-chart, treeView)
// while the fixture folders are kebab (tree-view); compare on a canonical
// lowercased, hyphen-stripped name.
const canonicalName = (name) => name.toLowerCase().replace(/-/g, '');

// Sentinel value returned when all changed files are ignorable (e.g.
// docs-only PRs).  Consumers should skip e2e entirely when they receive this.
export const SKIP = 'SKIP';

// ---------------------------------------------------------------------------
// Ignorable paths: files that can NEVER affect rendered diagram output.
// When a changed file matches one of these, it is silently skipped (continue)
// rather than triggering the full suite.
// ---------------------------------------------------------------------------
const IGNORABLE_PREFIXES = [
  // Root-level repo metadata
  'img/',
  'CITATION.cff',
  'FUNDING.json',
  'LICENSE',
  'CODE_OF_CONDUCT.md',
  'CONTRIBUTING.md',
  'README.md',
  'README.zh-CN.md',
  // Config/tooling that doesn't affect rendering
  'renovate.json',
  'docker-compose.yml',
  'Dockerfile',
  'netlify.toml',
  'cspell.config.yaml',
  // Documentation (source and generated)
  'packages/mermaid/src/docs/',
  'packages/mermaid/src/vitepress/',
  'packages/examples/',
  'packages/mermaid-local-editor/',
  'packages/tiny/',
  'docs/',
  // Changeset descriptions
  '.changeset/',
  // AI assistant / agent config
  '.claude/',
  'assistant/',
  // GitHub metadata that doesn't affect rendering
  '.github/workflows/build-docs.yml',
  '.github/workflows/publish-docs.yml',
  '.github/ISSUE_TEMPLATE/',
  '.github/CODEOWNERS',
  '.github/FUNDING.yml',
  '.github/workflows/autofix.yml',
  '.github/workflows/check-readme-in-sync.yml',
  '.github/workflows/codeql.yml',
  '.github/workflows/dependency-review.yml',
  '.github/workflows/issue-triage.yml',
  '.github/workflows/link-checker.yml',
  '.github/workflows/lint.yml',
  '.github/workflows/pr-labeler.yml',
  '.github/workflows/renovatebot-config-lint.yml',
  '.github/workflows/scorecard.yml',
  '.github/workflows/unlock-reopened-issues.yml',
  '.github/workflows/update-browserlist.yml',
  '.github/workflows/validate-lockfile.yml',
  // Doc-related scripts
  'packages/mermaid/scripts/docs',
  // Demos
  'demos/',
];

// Files ending with these suffixes are ignorable UNLESS they live inside a
// diagram source folder (where even a .md might be a samples file that
// signals intent to test).
const IGNORABLE_SUFFIXES = ['.md'];

// ---------------------------------------------------------------------------
// Paths: if ANY changed file matches one of these prefixes, fall back to the
// full suite.
// ---------------------------------------------------------------------------
const SHARED_PREFIXES = [
  // Shared diagram utilities (used by all diagram types)
  'packages/mermaid/src/diagrams/common/',
  // Shared rendering utilities (shapes, edges, layout — affects all diagrams)
  'packages/mermaid/src/rendering-util/',
  // Themes (affects all visual output)
  'packages/mermaid/src/themes/',
  // Core API and config (affects all diagrams)
  'packages/mermaid/src/config',
  'packages/mermaid/src/mermaid',
  'packages/mermaid/src/mermaidAPI',
  'packages/mermaid/src/diagram-api/',
  'packages/mermaid/src/Diagram.',
  'packages/mermaid/src/schemas/',
  // Parser package (used by multiple diagram types via Langium)
  'packages/parser/',
  // Layout engine packages
  'packages/dagre-wrapper/',
  'packages/mermaid-layout-elk/',
  'packages/mermaid-layout-tidy-tree/',
  // Plugin packages (separate bundles but tested in main suite)
  'packages/mermaid-zenuml/',
  'packages/mermaid-example-diagram/',
  // Playwright: config and cross-cutting test utilities
  'e2e/other/',
  'e2e/helpers/',
  'playwright.config',
  // Build and tooling (may affect all output)
  '.esbuild/',
  '.vite/',
  'vitest.',
  'tsconfig',
];

// Regex: extract diagram name from paths like
// packages/mermaid/src/diagrams/<name>/...
const DIAGRAM_PATH_RE = /^packages\/mermaid\/src\/diagrams\/([^/]+)\//;

// ---------------------------------------------------------------------------
// Core detection function
// ---------------------------------------------------------------------------

/**
 * Given a list of changed file paths (relative to repo root), returns the
 * comma-separated spec pattern to pass to `playwright test`, or an empty
 * string if the full suite should run.
 *
 * The function uses the filesystem to discover which diagram spec subfolders
 * exist. No mapping table is maintained — adding a spec to a subfolder is
 * sufficient.
 *
 * @param {string[]} files - Changed file paths (relative to repo root)
 * @param {{ specBaseDir?: string }} [options]
 * @returns {string} Spec pattern, or '' for full suite
 */
export function detectScope(files, options = {}) {
  const specBaseDir = options.specBaseDir ?? SPEC_BASE_DIR;

  if (files.length === 0) {
    return '';
  }

  /** @type {Set<string>} */
  const diagramNames = new Set();
  let touchesShared = false;
  // A changed e2e/diagrams/**/*.mmd fixture is exercised by the global mmd
  // snapshot runner (e2e/rendering/mmd-snapshots.spec.ts), not by a per-diagram
  // spec — track it so such a change scopes to that runner instead of the full
  // suite.
  let touchesMmdFixtures = false;
  /** @type {string[]} */
  const directlyChangedSpecs = [];

  for (const file of files) {
    const trimmed = file.trim();
    if (!trimmed) {
      continue;
    }

    // Shared infrastructure → full suite
    if (SHARED_PREFIXES.some((prefix) => trimmed.startsWith(prefix))) {
      touchesShared = true;
      break;
    }

    // File inside a diagram folder
    const diagramMatch = DIAGRAM_PATH_RE.exec(trimmed);
    if (diagramMatch) {
      const name = diagramMatch[1];
      // 'common' and 'globalStyles' are shared — already caught above,
      // but guard here for safety
      if (name === 'common' || name === 'globalStyles') {
        touchesShared = true;
        break;
      }
      diagramNames.add(name);
      continue;
    }

    // File under the spec base directory
    const specFolderPrefix = `${specBaseDir}/`;
    if (trimmed.startsWith(specFolderPrefix)) {
      const rest = trimmed.slice(specFolderPrefix.length);
      const slashIdx = rest.indexOf('/');

      if (slashIdx === -1) {
        // No subfolder separator → file sits at the root of SPEC_BASE_DIR.
        // Root-level specs are cross-cutting by convention; full suite.
        touchesShared = true;
        break;
      }

      // File inside a diagram subfolder → scope to that subfolder. Playwright
      // treats CLI filters as regular expressions, so keep this glob-free.
      const subFolder = rest.slice(0, slashIdx);
      directlyChangedSpecs.push(`${specFolderPrefix}${subFolder}/`);
      continue;
    }

    // A diagram fixture (.mmd) consumed by the global mmd snapshot runner.
    if (trimmed.startsWith('e2e/diagrams/')) {
      touchesMmdFixtures = true;
      continue;
    }

    // Ignorable files (docs, changesets, AI config, etc.) → skip silently.
    // Guard: .md files inside a diagram source folder are NOT ignorable — they
    // may be samples or signal intent, and their diagram folder was already
    // handled by DIAGRAM_PATH_RE above.
    if (
      IGNORABLE_PREFIXES.some((prefix) => trimmed.startsWith(prefix)) ||
      (IGNORABLE_SUFFIXES.some((suffix) => trimmed.endsWith(suffix)) &&
        !DIAGRAM_PATH_RE.test(trimmed))
    ) {
      continue;
    }

    // Anything else (root config, CI YAML, e2e/other, etc.) → full suite
    touchesShared = true;
    break;
  }

  if (touchesShared) {
    return '';
  }

  // Build regular-expression-safe spec patterns from diagram names using
  // filesystem discovery. A trailing slash matches every file in the folder.
  const specs = new Set(directlyChangedSpecs);

  /** @type {Set<string> | null} */
  let fixtureFolders = null;
  const hasFixtures = (name) => {
    if (fixtureFolders === null) {
      try {
        fixtureFolders = new Set(
          readdirSync(DIAGRAMS_DIR, { withFileTypes: true })
            .filter((entry) => entry.isDirectory())
            .map((entry) => canonicalName(entry.name))
        );
      } catch {
        fixtureFolders = new Set();
      }
    }
    return fixtureFolders.has(canonicalName(name));
  };

  for (const name of diagramNames) {
    const folder = `${specBaseDir}/${name}`;
    if (existsSync(folder)) {
      specs.add(`${folder}/`);
    } else if (hasFixtures(name)) {
      // Fixtures-only diagram (no per-diagram spec subfolder) — the global mmd
      // snapshot runner exercises it.
      touchesMmdFixtures = true;
    } else {
      // Neither a spec subfolder nor fixtures — be safe and run the full suite
      // rather than silently skip tests for an unrecognised diagram.
      return '';
    }
  }

  if (specs.size === 0 && !touchesMmdFixtures) {
    // All files were either ignorable or empty — no e2e tests needed.
    return SKIP;
  }

  // Any scoped diagram run — and any fixture-only change — executes the shared
  // mmd snapshot runner.
  specs.add(`${specBaseDir}/mmd-snapshots.spec.ts`);

  return [...specs].join(',');
}

// ---------------------------------------------------------------------------
// CLI entry point: read changed files from stdin, write spec to stdout
// ---------------------------------------------------------------------------

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const lines = [];
  const rl = createInterface({ input: process.stdin, crlfDelay: Infinity });
  for await (const line of rl) {
    lines.push(line);
  }
  const result = detectScope(lines);
  process.stdout.write(result);
}