heroshot 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-Fc-PkcTD.js";
2
+ import { C as intro, D as setVerbose, E as outro, O as spinner, S as error, T as note, _ as loadConfig, a as launchBrowser, c as generateSessionKey, d as loadSession, f as saveLocalKey, g as getConfigPath, h as ensureHeroshotDirectory, k as verbose, l as getSessionPath, m as sessionExists, n as snippetAction, o as DEFAULT_VIEWPORT, p as saveSession, r as sync, s as EDITOR_DIR, u as loadLocalKey, v as saveConfig, w as log, x as generateUid, y as VIEWPORT_PRESETS } from "../snippet-Cm6b-EdU.js";
3
3
  import { existsSync, readFileSync, rmSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
package/dist/mcp/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-Fc-PkcTD.js";
2
+ import { _ as loadConfig, b as screenshotSchema, g as getConfigPath, i as filterScreenshots, r as sync, t as generateSnippets, v as saveConfig, x as generateUid } from "../snippet-Cm6b-EdU.js";
3
3
  import { ZodOptional, z } from "zod";
4
4
  import * as z3rt from "zod/v3";
5
5
  import { ZodFirstPartyTypeKind } from "zod/v3";
@@ -371,7 +371,8 @@ const configSchema = z.object({
371
371
  browser: browserSchema.optional().describe("Default browser settings applied to all screenshots"),
372
372
  workers: z.number().int().min(1).optional().describe("Number of parallel capture workers (default: 1)"),
373
373
  screenshots: z.array(screenshotSchema).default([]).describe("Screenshot definitions"),
374
- hiddenElements: z.record(z.string(), z.array(z.string())).optional().describe("Elements to hide per domain (hostname → CSS selectors)")
374
+ hiddenElements: z.record(z.string(), z.array(z.string())).optional().describe("Elements to hide per domain (hostname → CSS selectors)"),
375
+ locales: z.array(z.string().min(1)).optional().describe("Locale codes (e.g., [\"en\", \"de\"]).")
375
376
  });
376
377
 
377
378
  //#endregion
@@ -731,7 +732,9 @@ async function launchBrowser(options = {}) {
731
732
  ...options.storageState && { storageState: options.storageState },
732
733
  ...options.colorScheme && { colorScheme: options.colorScheme },
733
734
  ...options.reducedMotion && { reducedMotion: options.reducedMotion },
734
- ...options.userAgent && { userAgent: options.userAgent }
735
+ ...options.userAgent && { userAgent: options.userAgent },
736
+ ...options.locale && { locale: options.locale },
737
+ ...options.locale && { extraHTTPHeaders: { "Accept-Language": options.locale } }
735
738
  });
736
739
  return {
737
740
  browser,
@@ -778,12 +781,13 @@ function resolveOutputDirectory(configPath, configOutputDirectory, override) {
778
781
  /**
779
782
  * Calculate total number of captures needed.
780
783
  */
781
- function calculateTotalCaptures(screenshots, schemeCount) {
784
+ function calculateTotalCaptures(screenshots, schemeCount, localeCount = 1) {
782
785
  let total = 0;
783
786
  const adjustedSchemeCount = Math.max(1, schemeCount);
787
+ const adjustedLocaleCount = Math.max(1, localeCount);
784
788
  for (const screenshot of screenshots) {
785
789
  const viewportCount = screenshot.viewports?.length ?? 1;
786
- total += viewportCount * adjustedSchemeCount;
790
+ total += viewportCount * adjustedSchemeCount * adjustedLocaleCount;
787
791
  }
788
792
  return total;
789
793
  }
@@ -814,6 +818,23 @@ function buildBrowserOptions(config) {
814
818
  };
815
819
  }
816
820
 
821
+ //#endregion
822
+ //#region src/utils/localeUrl.ts
823
+ /**
824
+ * Apply locale to a URL by replacing the {locale} placeholder.
825
+ * If the URL contains no placeholder, it is returned unchanged.
826
+ *
827
+ * @example
828
+ * applyLocale('http://localhost:5173/{locale}/about', 'de')
829
+ * // → 'http://localhost:5173/de/about'
830
+ *
831
+ * applyLocale('http://localhost:5173/about', 'de')
832
+ * // → 'http://localhost:5173/about'
833
+ */
834
+ function applyLocale(url, locale) {
835
+ return url.replaceAll("{locale}", locale);
836
+ }
837
+
817
838
  //#endregion
818
839
  //#region src/utils/parseViewport.ts
819
840
  /**
@@ -861,7 +882,7 @@ function slugifySegment(text) {
861
882
  * Supports subdirectory paths via forward slashes in the name (e.g., "registry/login-01").
862
883
  */
863
884
  function generateScreenshotFilename(options) {
864
- const { name, viewport, colorScheme, format = "png" } = options;
885
+ const { name, viewport, colorScheme, locale, format = "png" } = options;
865
886
  const segments = name.split("/").map(slugifySegment).filter(Boolean);
866
887
  const directory = segments.length > 1 ? segments.slice(0, -1).join("/") : "";
867
888
  const parts = [segments.at(-1) ?? ""];
@@ -869,7 +890,8 @@ function generateScreenshotFilename(options) {
869
890
  if (colorScheme) parts.push(colorScheme);
870
891
  const extension = format === "jpeg" ? "jpg" : "png";
871
892
  const filename = `${parts.join("-")}.${extension}`;
872
- return directory ? `${directory}/${filename}` : filename;
893
+ const pathWithDirectory = directory ? `${directory}/${filename}` : filename;
894
+ return locale ? `${locale}/${pathWithDirectory}` : pathWithDirectory;
873
895
  }
874
896
 
875
897
  //#endregion
@@ -1749,8 +1771,12 @@ async function captureElementScreenshot(options) {
1749
1771
  /**
1750
1772
  * Build a variant ID suffix.
1751
1773
  */
1752
- function buildVariantSuffix(viewportName, colorScheme) {
1753
- return [viewportName, colorScheme].filter(Boolean).join("-");
1774
+ function buildVariantSuffix(viewportName, colorScheme, locale) {
1775
+ return [
1776
+ locale,
1777
+ viewportName,
1778
+ colorScheme
1779
+ ].filter(Boolean).join("-");
1754
1780
  }
1755
1781
  /**
1756
1782
  * Show capture results and return summary.
@@ -1854,18 +1880,20 @@ async function captureScreenshot(page, screenshot, outputDirectory, captureOptio
1854
1880
  name,
1855
1881
  viewport: variant.viewportName,
1856
1882
  colorScheme: variant.colorScheme,
1883
+ locale: variant.locale,
1857
1884
  format
1858
1885
  });
1859
- const suffix = buildVariantSuffix(variant.viewportName, variant.colorScheme);
1886
+ const suffix = buildVariantSuffix(variant.viewportName, variant.colorScheme, variant.locale);
1860
1887
  verbose(`Capturing: ${name}${suffix ? ` (${suffix})` : ""}`);
1861
- const navResult = await navigateAndPrepare(page, url, variant.colorScheme);
1888
+ const effectiveUrl = variant.localeUrl ?? url;
1889
+ const navResult = await navigateAndPrepare(page, effectiveUrl, variant.colorScheme);
1862
1890
  if (!navResult.success) return {
1863
1891
  ...navResult,
1864
1892
  filename
1865
1893
  };
1866
1894
  if (captureOptions.hiddenElements) {
1867
1895
  const { hiddenElements: hiddenByDomain } = captureOptions;
1868
- const { hostname } = new URL(url);
1896
+ const { hostname } = new URL(effectiveUrl);
1869
1897
  if (hiddenByDomain[hostname]?.length) await executeHide(page, {
1870
1898
  type: "hide",
1871
1899
  selectors: hiddenByDomain[hostname]
@@ -1928,7 +1956,7 @@ async function captureAndLog(page, screenshot, outputDirectory, captureOptions,
1928
1956
  await page.waitForTimeout(delay);
1929
1957
  }
1930
1958
  }
1931
- const suffix = buildVariantSuffix(variant.viewportName, variant.colorScheme);
1959
+ const suffix = buildVariantSuffix(variant.viewportName, variant.colorScheme, variant.locale);
1932
1960
  const displayName = suffix ? `${screenshot.name} (${suffix})` : screenshot.name;
1933
1961
  const idSuffix = suffix ? `-${suffix}` : "";
1934
1962
  return {
@@ -1946,31 +1974,41 @@ async function captureAndLog(page, screenshot, outputDirectory, captureOptions,
1946
1974
  * Parallel screenshot capture with multiple workers.
1947
1975
  */
1948
1976
  /**
1949
- * Build capture jobs from screenshots and schemes.
1977
+ * Build capture jobs from screenshots, schemes, and locales.
1950
1978
  * Each job represents a single screenshot capture task.
1951
1979
  */
1952
- function buildCaptureJobs(screenshots, schemes) {
1980
+ function buildCaptureJobs(screenshots, schemes, locales = []) {
1953
1981
  const jobs = [];
1954
1982
  const hasMultipleSchemes = schemes.length > 1;
1955
1983
  const schemesToCapture = schemes.length === 0 ? [void 0] : schemes;
1984
+ const localesToCapture = locales.length === 0 ? [void 0] : locales;
1956
1985
  for (const screenshot of screenshots) {
1957
1986
  const viewportVariants = screenshot.viewports ?? [];
1958
1987
  const hasMultipleViewports = viewportVariants.length > 1;
1959
- for (const scheme of schemesToCapture) if (viewportVariants.length === 0) jobs.push({
1960
- screenshot,
1961
- colorScheme: scheme,
1962
- hasMultipleSchemes,
1963
- hasMultipleViewports: false
1964
- });
1965
- else for (const viewportVariant of viewportVariants) {
1966
- const parsedViewport = parseViewport(viewportVariant);
1967
- jobs.push({
1988
+ for (const locale of localesToCapture) {
1989
+ const localeUrl = locale && screenshot.url.includes("{locale}") ? applyLocale(screenshot.url, locale) : void 0;
1990
+ for (const scheme of schemesToCapture) if (viewportVariants.length === 0) jobs.push({
1968
1991
  screenshot,
1969
1992
  colorScheme: scheme,
1970
- viewport: parsedViewport,
1971
1993
  hasMultipleSchemes,
1972
- hasMultipleViewports
1994
+ hasMultipleViewports: false,
1995
+ locale,
1996
+ locales,
1997
+ localeUrl
1973
1998
  });
1999
+ else for (const viewportVariant of viewportVariants) {
2000
+ const parsedViewport = parseViewport(viewportVariant);
2001
+ jobs.push({
2002
+ screenshot,
2003
+ colorScheme: scheme,
2004
+ viewport: parsedViewport,
2005
+ hasMultipleSchemes,
2006
+ hasMultipleViewports,
2007
+ locale,
2008
+ locales,
2009
+ localeUrl
2010
+ });
2011
+ }
1974
2012
  }
1975
2013
  }
1976
2014
  return jobs;
@@ -1987,12 +2025,13 @@ async function executeBatch(jobs, outputDirectory, captureOptions, browserOption
1987
2025
  storageState: browserOptions.storageState,
1988
2026
  bypassCSP: browserOptions.bypassCSP,
1989
2027
  reducedMotion: browserOptions.reducedMotion,
1990
- userAgent: browserOptions.userAgent
2028
+ userAgent: browserOptions.userAgent,
2029
+ locale: jobs[0]?.locale
1991
2030
  });
1992
2031
  const page = await context.newPage();
1993
2032
  try {
1994
2033
  for (const job of jobs) {
1995
- const { screenshot, colorScheme, viewport, hasMultipleSchemes, hasMultipleViewports } = job;
2034
+ const { screenshot, colorScheme, viewport, hasMultipleSchemes, hasMultipleViewports, locale, locales } = job;
1996
2035
  if (viewport) await page.setViewportSize({
1997
2036
  width: viewport.width,
1998
2037
  height: viewport.height
@@ -2000,7 +2039,9 @@ async function executeBatch(jobs, outputDirectory, captureOptions, browserOption
2000
2039
  if (colorScheme) await page.emulateMedia({ colorScheme });
2001
2040
  const result = await captureAndLog(page, screenshot, outputDirectory, captureOptions, {
2002
2041
  viewportName: hasMultipleViewports ? viewport?.name : void 0,
2003
- colorScheme: hasMultipleSchemes ? colorScheme : void 0
2042
+ colorScheme: hasMultipleSchemes ? colorScheme : void 0,
2043
+ locale: (locales ?? []).length > 1 ? locale : void 0,
2044
+ localeUrl: job.localeUrl
2004
2045
  });
2005
2046
  results.push(result);
2006
2047
  onProgress(result);
@@ -2011,16 +2052,17 @@ async function executeBatch(jobs, outputDirectory, captureOptions, browserOption
2011
2052
  return results;
2012
2053
  }
2013
2054
  /**
2014
- * Group jobs by URL to minimize page navigations.
2015
- * Jobs for the same URL are kept together.
2055
+ * Group jobs by effective URL + locale to minimize page navigations.
2056
+ * Jobs for the same URL AND same locale are kept together.
2057
+ * Locale is a browser context-level setting, so different locales must be separate groups.
2016
2058
  */
2017
2059
  function groupJobsByUrl(jobs) {
2018
2060
  const groups = /* @__PURE__ */ new Map();
2019
2061
  for (const job of jobs) {
2020
- const { url } = job.screenshot;
2021
- const group = groups.get(url) ?? [];
2062
+ const key = `${job.localeUrl ?? job.screenshot.url}::${job.locale ?? ""}`;
2063
+ const group = groups.get(key) ?? [];
2022
2064
  group.push(job);
2023
- groups.set(url, group);
2065
+ groups.set(key, group);
2024
2066
  }
2025
2067
  return groups;
2026
2068
  }
@@ -2065,11 +2107,15 @@ async function captureParallel(options) {
2065
2107
  //#endregion
2066
2108
  //#region src/sync/schemeCapture.ts
2067
2109
  /**
2068
- * Capture screenshots for a single color scheme.
2110
+ * Capture screenshots for a single color scheme + locale combination.
2069
2111
  * Launches a browser, captures all screenshots, and closes the browser.
2112
+ *
2113
+ * Locale is a browser context-level setting in Playwright, so each locale
2114
+ * gets its own browser launch. This ensures Accept-Language and JS locale APIs
2115
+ * (Intl, navigator.language) reflect the correct locale for all pages captured.
2070
2116
  */
2071
2117
  async function captureWithScheme(options) {
2072
- const { screenshots, outputDirectory, captureOptions, browserOptions, colorScheme, schemes, captureSpinner, progress } = options;
2118
+ const { screenshots, outputDirectory, captureOptions, browserOptions, colorScheme, schemes, locale, locales, captureSpinner, progress } = options;
2073
2119
  const results = [];
2074
2120
  const { browser, context } = await launchBrowser({
2075
2121
  headless: !browserOptions.headed,
@@ -2079,22 +2125,32 @@ async function captureWithScheme(options) {
2079
2125
  colorScheme,
2080
2126
  bypassCSP: browserOptions.bypassCSP,
2081
2127
  reducedMotion: browserOptions.reducedMotion,
2082
- userAgent: browserOptions.userAgent
2128
+ userAgent: browserOptions.userAgent,
2129
+ locale
2083
2130
  });
2084
2131
  const page = await context.newPage();
2085
2132
  if (colorScheme) await page.emulateMedia({ colorScheme });
2086
2133
  const hasMultipleSchemes = schemes.length > 1;
2134
+ const hasMultipleLocales = locales.length > 1;
2087
2135
  for (const screenshot of screenshots) {
2088
2136
  const viewportVariants = screenshot.viewports ?? [];
2089
2137
  const hasMultipleViewports = viewportVariants.length > 1;
2138
+ const localeUrl = locale && screenshot.url.includes("{locale}") ? applyLocale(screenshot.url, locale) : void 0;
2090
2139
  if (viewportVariants.length === 0) {
2091
2140
  progress.captured++;
2092
- const variant = { colorScheme: hasMultipleSchemes ? colorScheme : void 0 };
2093
- const suffix = variant.colorScheme ? ` (${variant.colorScheme})` : "";
2094
- captureSpinner.message(`Capturing ${progress.captured}/${progress.total}: ${screenshot.name}${suffix}`);
2141
+ const variant = {
2142
+ colorScheme: hasMultipleSchemes ? colorScheme : void 0,
2143
+ locale: hasMultipleLocales ? locale : void 0,
2144
+ localeUrl
2145
+ };
2146
+ const suffix = [variant.locale, variant.colorScheme].filter(Boolean).join(", ");
2147
+ const suffixDisplay = suffix ? ` (${suffix})` : "";
2148
+ captureSpinner.message(`Capturing ${progress.captured}/${progress.total}: ${screenshot.name}${suffixDisplay}`);
2095
2149
  const result = await captureAndLog(page, screenshot, outputDirectory, captureOptions, variant);
2096
2150
  results.push(result);
2097
- } else for (const viewportVariant of viewportVariants) {
2151
+ continue;
2152
+ }
2153
+ for (const viewportVariant of viewportVariants) {
2098
2154
  const parsedViewport = parseViewport(viewportVariant);
2099
2155
  await page.setViewportSize({
2100
2156
  width: parsedViewport.width,
@@ -2103,9 +2159,15 @@ async function captureWithScheme(options) {
2103
2159
  progress.captured++;
2104
2160
  const variant = {
2105
2161
  viewportName: hasMultipleViewports ? parsedViewport.name : void 0,
2106
- colorScheme: hasMultipleSchemes ? colorScheme : void 0
2162
+ colorScheme: hasMultipleSchemes ? colorScheme : void 0,
2163
+ locale: hasMultipleLocales ? locale : void 0,
2164
+ localeUrl
2107
2165
  };
2108
- const suffix = [variant.viewportName, variant.colorScheme].filter(Boolean).join(", ");
2166
+ const suffix = [
2167
+ variant.locale,
2168
+ variant.viewportName,
2169
+ variant.colorScheme
2170
+ ].filter(Boolean).join(", ");
2109
2171
  const suffixDisplay = suffix ? ` (${suffix})` : "";
2110
2172
  captureSpinner.message(`Capturing ${progress.captured}/${progress.total}: ${screenshot.name}${suffixDisplay}`);
2111
2173
  const result = await captureAndLog(page, screenshot, outputDirectory, captureOptions, variant);
@@ -2204,9 +2266,9 @@ function handleStaleFiles(outputDirectory, results, options) {
2204
2266
  * Execute screenshot capture (parallel or sequential based on workers).
2205
2267
  */
2206
2268
  async function executeCapture(context) {
2207
- const { screenshots, outputDirectory, captureOptions, browserOptions, schemes, workers, captureSpinner, progress } = context;
2269
+ const { screenshots, outputDirectory, captureOptions, browserOptions, schemes, locales, workers, captureSpinner, progress } = context;
2208
2270
  if (workers > 1) return captureParallel({
2209
- jobs: buildCaptureJobs(screenshots, schemes),
2271
+ jobs: buildCaptureJobs(screenshots, schemes, locales),
2210
2272
  outputDirectory,
2211
2273
  captureOptions,
2212
2274
  browserOptions,
@@ -2216,7 +2278,8 @@ async function executeCapture(context) {
2216
2278
  });
2217
2279
  const results = [];
2218
2280
  const schemesToCapture = schemes.length === 0 ? [void 0] : schemes;
2219
- for (const colorScheme of schemesToCapture) {
2281
+ const localesToCapture = locales.length === 0 ? [void 0] : locales;
2282
+ for (const locale of localesToCapture) for (const colorScheme of schemesToCapture) {
2220
2283
  const schemeResults = await captureWithScheme({
2221
2284
  screenshots,
2222
2285
  outputDirectory,
@@ -2224,6 +2287,8 @@ async function executeCapture(context) {
2224
2287
  browserOptions,
2225
2288
  colorScheme,
2226
2289
  schemes,
2290
+ locale,
2291
+ locales,
2227
2292
  captureSpinner,
2228
2293
  progress
2229
2294
  });
@@ -2265,8 +2330,9 @@ async function sync(options = {}) {
2265
2330
  const outputDirectory = resolveOutputDirectory(configPath, config.outputDirectory, options.outputDirectory);
2266
2331
  const storageState = loadEncryptedSession(options.sessionKey);
2267
2332
  const schemes = getColorSchemes(config.browser?.colorScheme);
2333
+ const locales = config.locales ?? [];
2268
2334
  const captureOptions = buildCaptureOptions(config, options.viewportOnly);
2269
- const totalToCapture = calculateTotalCaptures(screenshots, schemes.length);
2335
+ const totalToCapture = calculateTotalCaptures(screenshots, schemes.length, Math.max(1, locales.length));
2270
2336
  const browserOptions = {
2271
2337
  ...buildBrowserOptions(config),
2272
2338
  storageState,
@@ -2284,6 +2350,7 @@ async function sync(options = {}) {
2284
2350
  captureOptions,
2285
2351
  browserOptions,
2286
2352
  schemes,
2353
+ locales,
2287
2354
  workers,
2288
2355
  captureSpinner,
2289
2356
  progress: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "heroshot",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Define your screenshots once, update them forever with one command",
5
5
  "type": "module",
6
6
  "author": "Ondrej Machala",