← Files SkillquiverARCHIVED FILE

skills/design-ui/scripts/capture-static-page.cjs

4.86 KB · Oct 3, 2026 · 06:32 UTC

↓ Download file

const fs = require('node:fs');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const { pathToFileURL } = require('node:url');

function parseWidths(values) {
  const widths = values.map(value => Number(value));
  if (widths.length === 0 || widths.some(width =>
    !Number.isInteger(width) || width < 240 || width > 3840)) {
    throw new Error('Viewport widths must be integers between 240 and 3840.');
  }
  return widths;
}

function browserCandidates(env = process.env, platform = process.platform) {
  const candidates = [env.CHROME_PATH].filter(Boolean);
  if (platform === 'win32') {
    for (const root of [env.ProgramFiles, env['ProgramFiles(x86)'], env.LOCALAPPDATA]) {
      if (!root) continue;
      candidates.push(
        path.join(root, 'Google', 'Chrome', 'Application', 'chrome.exe'),
        path.join(root, 'Microsoft', 'Edge', 'Application', 'msedge.exe')
      );
    }
  } else if (platform === 'darwin') {
    candidates.push(
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
      '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'
    );
  } else {
    candidates.push(
      '/usr/bin/google-chrome',
      '/usr/bin/google-chrome-stable',
      '/usr/bin/chromium',
      '/usr/bin/chromium-browser',
      '/usr/bin/microsoft-edge'
    );
  }
  return [...new Set(candidates)];
}

function findBrowser() {
  return browserCandidates().find(candidate => fs.existsSync(candidate));
}

function captureArguments(pagePath, outputPath, width) {
  return [
    '--headless=new',
    '--disable-gpu',
    '--hide-scrollbars',
    `--screenshot=${outputPath}`,
    `--window-size=${width},900`,
    pathToFileURL(pagePath).href
  ];
}

function inspectionFrameHtml(capturePath, width) {
  return `<!doctype html><meta name="viewport" content="width=device-width"><style>` +
    `html,body{margin:0;min-height:100%;background:#dce8e0}` +
    `img{display:block;width:${width}px;height:900px;margin:0 auto}` +
    `</style><img src="${pathToFileURL(capturePath).href}" alt="">`;
}

function captureStaticPage(pagePath, outputDirectory, widths, dependencies = {}) {
  const resolvedPage = path.resolve(pagePath);
  if (!fs.existsSync(resolvedPage)) throw new Error(`Page not found: ${resolvedPage}`);

  const findBrowserImpl = dependencies.findBrowser || findBrowser;
  const spawnSyncImpl = dependencies.spawnSync || spawnSync;
  const browser = findBrowserImpl();
  if (!browser) throw new Error('No installed Chrome, Chromium, or Edge executable was found.');

  const resolvedOutput = path.resolve(outputDirectory);
  fs.mkdirSync(resolvedOutput, { recursive: true });
  const pageName = path.basename(resolvedPage, path.extname(resolvedPage));

  return widths.map(width => {
    const outputPath = path.join(resolvedOutput, `${pageName}-${width}.png`);
    const result = spawnSyncImpl(
      browser,
      captureArguments(resolvedPage, outputPath, width),
      { encoding: 'utf8', windowsHide: true }
    );
    if (result.error || result.status !== 0 || !fs.existsSync(outputPath) ||
        fs.statSync(outputPath).size === 0) {
      const detail = result.error?.message || result.stderr?.trim() || `exit ${result.status}`;
      throw new Error(`Capture failed at ${width}px: ${detail}`);
    }

    let inspectionPath = null;
    if (width < 480) {
      inspectionPath = path.join(resolvedOutput, `${pageName}-${width}-inspect.png`);
      const framePath = path.join(resolvedOutput, `.${pageName}-${width}-inspect.html`);
      fs.writeFileSync(framePath, inspectionFrameHtml(outputPath, width));
      try {
        const inspection = spawnSyncImpl(
          browser,
          captureArguments(framePath, inspectionPath, 720),
          { encoding: 'utf8', windowsHide: true }
        );
        if (inspection.error || inspection.status !== 0 ||
            !fs.existsSync(inspectionPath) || fs.statSync(inspectionPath).size === 0) {
          const detail = inspection.error?.message || inspection.stderr?.trim() ||
            `exit ${inspection.status}`;
          throw new Error(`Inspection frame failed at ${width}px: ${detail}`);
        }
      } finally {
        fs.rmSync(framePath, { force: true });
      }
    }

    return { width, height: 900, outputPath, inspectionPath, browser };
  });
}

if (require.main === module) {
  try {
    const [pagePath, outputDirectory, ...widthValues] = process.argv.slice(2);
    if (!pagePath || !outputDirectory) {
      throw new Error('Usage: capture-static-page.cjs <page.html> <output-dir> <width...>');
    }
    const captures = captureStaticPage(pagePath, outputDirectory, parseWidths(widthValues));
    process.stdout.write(`${JSON.stringify({ captures })}\n`);
  } catch (error) {
    process.stderr.write(`${error.message}\n`);
    process.exitCode = 1;
  }
}

module.exports = {
  browserCandidates,
  captureArguments,
  captureStaticPage,
  findBrowser,
  inspectionFrameHtml,
  parseWidths
};

SHA-256: 0cb88e28bf78c908ee351a34a35da31de4d6f0bcaf6f758b77c4354e1c78af30