@kernocal/viteshot 0.1.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aaron
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # @kernocal/viteshot
2
+
3
+ Generate store screenshots and promo images with HTML, powered by Vite.
4
+
5
+ > Fork of [@aklinker1/viteshot](https://github.com/aklinker1/viteshot)
6
+
7
+
8
+ ## lots of stuff is hardcoded for me, sorry!
9
+
10
+ you can probably change it in `src/core/config.ts`
11
+
12
+ also it uses pnpm now careful
@@ -0,0 +1,197 @@
1
+ /** Main JS module for displaying the different screenshots */
2
+ import screenshots from "viteshot-virtual/screenshots";
3
+ import locales from "viteshot-virtual/locales";
4
+
5
+ declare const app: HTMLDivElement;
6
+
7
+ // Icons
8
+
9
+ const LOCALE_FLAGS: Record<string, string> = {
10
+ be: "🇧🇪",
11
+ br: "🇧🇷",
12
+ de: "🇩🇪",
13
+ en: "🇺🇸",
14
+ en_gb: "🇬🇧",
15
+ en_us: "🇺🇸",
16
+ es: "🇪🇸",
17
+ es_mx: "🇲🇽",
18
+ fr: "🇫🇷",
19
+ it: "🇮🇹",
20
+ ja: "🇯🇵",
21
+ pt: "🇧🇷",
22
+ pt_br: "🇧🇷",
23
+ ru: "🇷🇺",
24
+ zh: "🇨🇳",
25
+ zh_tw: "🇹🇼",
26
+ };
27
+
28
+ const HEROICONS_ARROW_UP_RIGHT_16_SOLID = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="currentColor" fill-rule="evenodd" d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0" clip-rule="evenodd"/></svg>`;
29
+ const HEROICONS_SUN = `<svg class="light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0a3.75 3.75 0 0 1 7.5 0"/></svg>`;
30
+ const HEROICONS_MOON = `<svg class="dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21.752 15.002A9.7 9.7 0 0 1 18 15.75A9.75 9.75 0 0 1 8.25 6c0-1.33.266-2.597.748-3.752A9.75 9.75 0 0 0 3 11.25A9.75 9.75 0 0 0 12.75 21a9.75 9.75 0 0 0 9.002-5.998"/></svg>`;
31
+
32
+ // Language management
33
+
34
+ const LANGUAGE_STORAGE_KEY = "viteshot:language";
35
+
36
+ let currentLanguageId: string | undefined =
37
+ getStoredLanguage() ?? locales[0]?.id;
38
+ function getStoredLanguage() {
39
+ const prevId = localStorage.getItem(LANGUAGE_STORAGE_KEY);
40
+ if (!prevId || !locales.some((l) => l.id === prevId)) return;
41
+
42
+ return prevId;
43
+ }
44
+ function setLanguage(languageId: string): void {
45
+ currentLanguageId = languageId;
46
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, languageId);
47
+ }
48
+
49
+ // Theme
50
+
51
+ const THEME_STORAGE_KEY = "viteshot:theme";
52
+
53
+ let currentTheme: string = getStoredTheme() ?? "dark";
54
+
55
+ function getStoredTheme() {
56
+ const prevTheme = localStorage.getItem(THEME_STORAGE_KEY);
57
+ if (prevTheme !== "light" && prevTheme !== "dark") return;
58
+
59
+ return prevTheme;
60
+ }
61
+ function toggleTheme(): void {
62
+ const theme = currentTheme === "dark" ? "light" : "dark";
63
+
64
+ currentTheme = theme;
65
+ updateTheme();
66
+ localStorage.setItem(THEME_STORAGE_KEY, theme);
67
+ }
68
+ function updateTheme() {
69
+ document.documentElement.setAttribute("data-theme", currentTheme);
70
+ }
71
+
72
+ // UI Rendering
73
+
74
+ type Child = string | HTMLElement | SVGElement | false | undefined | null;
75
+
76
+ interface ElementTagMap extends HTMLElementTagNameMap {
77
+ svg: SVGElement;
78
+ }
79
+
80
+ function h<TTag extends keyof ElementTagMap>(
81
+ tag: TTag,
82
+ children: Child[],
83
+ ): ElementTagMap[TTag];
84
+ function h<TTag extends keyof ElementTagMap>(
85
+ tag: TTag,
86
+ props: Partial<ElementTagMap[TTag]>,
87
+ children?: Child[],
88
+ ): ElementTagMap[TTag];
89
+ function h<TTag extends keyof ElementTagMap>(
90
+ tag: TTag,
91
+ arg1?: any,
92
+ arg2?: any,
93
+ ): ElementTagMap[TTag] {
94
+ const hasProps =
95
+ typeof arg1 === "object" &&
96
+ !Array.isArray(arg1) &&
97
+ !(arg1 instanceof HTMLElement);
98
+ const props: Record<string, any> = hasProps ? arg1 : undefined;
99
+ const children: Child[] = hasProps ? arg2 : arg1;
100
+
101
+ const el = document.createElement(tag) as ElementTagMap[TTag];
102
+
103
+ if (props) {
104
+ for (const [key, value] of Object.entries(props)) {
105
+ if (key in el) (el as any)[key] = value;
106
+ else el.setAttribute(key, value);
107
+ }
108
+ }
109
+
110
+ if (children) {
111
+ for (const child of children) {
112
+ if (child != null && child !== false) el.append(child);
113
+ }
114
+ }
115
+
116
+ return el;
117
+ }
118
+
119
+ function svg(outerHtml: string): SVGElement {
120
+ const temp = document.createElement("div");
121
+ temp.innerHTML = outerHtml;
122
+ return temp.firstElementChild as SVGElement;
123
+ }
124
+
125
+ function renderHeader() {
126
+ app.append(
127
+ h("div", { className: "header" }, [
128
+ h("div", { className: "left" }, [
129
+ h("h1", "ViteShot"),
130
+ h(
131
+ "a",
132
+ { href: "https://github.com/aklinker1/viteshot", target: "_blank" },
133
+ [h("span", ["Docs"]), svg(HEROICONS_ARROW_UP_RIGHT_16_SOLID)],
134
+ ),
135
+ ]),
136
+ locales.length > 0 &&
137
+ h(
138
+ "select",
139
+ {
140
+ className: "language-select",
141
+ onchange: (e) => {
142
+ setLanguage((e.target as HTMLSelectElement).value);
143
+ renderScreenshots();
144
+ },
145
+ },
146
+ locales.map((l) =>
147
+ h("option", { value: l.id, selected: currentLanguageId === l.id }, [
148
+ `${LOCALE_FLAGS[l.language.replaceAll("-", "_").toLowerCase()] || "🌐"} ${l.language}`,
149
+ ]),
150
+ ),
151
+ ),
152
+ h("button", { className: "theme-toggle", onclick: toggleTheme }, [
153
+ svg(HEROICONS_SUN),
154
+ svg(HEROICONS_MOON),
155
+ ]),
156
+ ]),
157
+ );
158
+ }
159
+
160
+ function renderScreenshots() {
161
+ const existingUl = app.querySelector("& > ul");
162
+
163
+ const newUl = h(
164
+ "ul",
165
+ screenshots.map((ss) =>
166
+ h("li", { id: ss.id, className: "list-item" }, [
167
+ h("h2", { className: "title-row" }, [
168
+ h("a", { className: "name", href: `#${ss.id}` }, [ss.name]),
169
+ ss.size && h("span", " "),
170
+ ss.size && h("span", { className: "size" }, [ss.size]),
171
+ ]),
172
+
173
+ h("div", { className: "iframe-wrapper" }, [
174
+ h(
175
+ "iframe",
176
+ {
177
+ width: String(ss.width),
178
+ height: String(ss.height),
179
+ src: `/screenshot/${currentLanguageId ? encodeURIComponent(currentLanguageId) : "null"}/${encodeURIComponent(ss.id)}.html`,
180
+ },
181
+ [],
182
+ ),
183
+ ]),
184
+ ]),
185
+ ),
186
+ );
187
+
188
+ if (existingUl) {
189
+ existingUl.replaceWith(newUl);
190
+ } else {
191
+ app.append(newUl);
192
+ }
193
+ }
194
+
195
+ updateTheme();
196
+ renderHeader();
197
+ renderScreenshots();
@@ -0,0 +1,180 @@
1
+ import { t as getLocales } from "./get-locales-DgKRepYE.mjs";
2
+ import { join, relative, resolve } from "node:path";
3
+ import { readdir } from "node:fs/promises";
4
+ import natsort from "natural-compare-lite";
5
+ import { styleText } from "node:util";
6
+ import puppeteer from "puppeteer-core";
7
+
8
+ //#region src/core/get-screenshots.ts
9
+ const FILENAME_REGEX = /^(?<name>.*?)@(?<size>(?<width>[0-9]+)x(?<height>[0-9]+)).(?<ext>.*)$/;
10
+ async function getScreenshots(designsDir) {
11
+ return (await readdir(designsDir, {
12
+ recursive: true,
13
+ withFileTypes: true
14
+ })).filter((file) => file.isFile()).map((file) => {
15
+ const match = FILENAME_REGEX.exec(file.name);
16
+ if (!match) return;
17
+ const path = join(file.parentPath, file.name);
18
+ const name = match.groups.name;
19
+ const size = match.groups.size;
20
+ const width = Number(match.groups.width);
21
+ const height = Number(match.groups.height);
22
+ const ext = match.groups.ext;
23
+ return {
24
+ id: relative(designsDir, path),
25
+ path,
26
+ ext,
27
+ name,
28
+ size,
29
+ width,
30
+ height
31
+ };
32
+ }).filter((file) => file != null).toSorted((a, b) => natsort(a.id, b.id));
33
+ }
34
+ async function logInvalidDesignFiles(designsDir) {
35
+ const invalid = (await readdir(designsDir, {
36
+ recursive: true,
37
+ withFileTypes: true
38
+ })).filter((file) => file.isFile()).map((file) => FILENAME_REGEX.exec(file.name) ? void 0 : file).filter((file) => file != null).map((file) => file.name);
39
+ if (invalid.length > 0) console.warn(`${styleText(["bold", "yellow"], "Invalid design file names:")}\n - ${invalid.join("\n - ")}`);
40
+ }
41
+
42
+ //#endregion
43
+ //#region src/core/capture-extension-screenshots.ts
44
+ /**
45
+ * Capture popup/sidebar/options screenshots for a single locale.
46
+ * Pass `language: "_default"` (or omit) to capture without `--lang`.
47
+ */
48
+ async function captureLocale(options) {
49
+ const language = options.language ?? "_default";
50
+ const waitSelector = options.waitSelector ?? "body";
51
+ const settleDelay = options.settleDelay ?? 300;
52
+ if (language !== "_default") console.log(` \x1b[1m[${language}]\x1b[0m Capturing extension screenshots...`);
53
+ else console.log(` Capturing extension screenshots...`);
54
+ let browser;
55
+ try {
56
+ browser = await puppeteer.launch({
57
+ executablePath: options.chromePath,
58
+ headless: false,
59
+ args: [
60
+ `--disable-extensions-except=${options.extensionPath}`,
61
+ `--load-extension=${options.extensionPath}`,
62
+ "--window-size=1280,800",
63
+ ...language !== "_default" ? [`--lang=${language}`] : []
64
+ ]
65
+ });
66
+ const page = (await browser.pages())[0] ?? await browser.newPage();
67
+ await page.goto("data:text/html,<h1>viteshot</h1>");
68
+ const client = await page.createCDPSession();
69
+ const extensionId = await new Promise((resolve, reject) => {
70
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for extension to load")), 2e4);
71
+ const poll = async () => {
72
+ const { targetInfos } = await client.send("Target.getTargets");
73
+ const ext = targetInfos.find((t) => t.url.startsWith("chrome-extension://"));
74
+ if (ext) {
75
+ clearTimeout(timeout);
76
+ resolve(new URL(ext.url).hostname);
77
+ } else setTimeout(poll, 200);
78
+ };
79
+ poll();
80
+ });
81
+ await client.detach();
82
+ console.log(` Extension ID: ${extensionId}`);
83
+ await page.goto("https://example.com", {
84
+ waitUntil: "domcontentloaded",
85
+ timeout: 2e4
86
+ });
87
+ await new Promise((r) => setTimeout(r, 1e3));
88
+ const captured = {};
89
+ let hasPopup = false;
90
+ let hasSidebar = false;
91
+ try {
92
+ const worker = await (await browser.waitForTarget((target) => target.type() === "service_worker" && target.url().includes(`chrome-extension://${extensionId}`), { timeout: 1e4 })).worker();
93
+ if (!worker) throw new Error("Service worker not available");
94
+ await worker.evaluate("chrome.action.openPopup()");
95
+ const popupPage = await (await browser.waitForTarget((target) => target.type() === "page" && target.url().includes(`chrome-extension://${extensionId}`), { timeout: 5e3 })).asPage();
96
+ await popupPage.waitForSelector(waitSelector);
97
+ await new Promise((r) => setTimeout(r, settleDelay));
98
+ captured.popup = await popupPage.screenshot({
99
+ type: "png",
100
+ fullPage: true
101
+ });
102
+ hasPopup = true;
103
+ console.log(` ✅ popup captured`);
104
+ } catch {
105
+ console.log(" ⚠ No popup action found");
106
+ }
107
+ try {
108
+ const sidebarPage = await browser.newPage();
109
+ const sidePanelUrl = `chrome-extension://${extensionId}/sidepanel.html`;
110
+ const response = await sidebarPage.goto(sidePanelUrl, {
111
+ waitUntil: "domcontentloaded",
112
+ timeout: 5e3
113
+ });
114
+ if (!response || !response.ok()) throw new Error("No sidebar");
115
+ await sidebarPage.waitForSelector(waitSelector);
116
+ await new Promise((r) => setTimeout(r, settleDelay));
117
+ captured.sidebar = await sidebarPage.screenshot({
118
+ type: "png",
119
+ fullPage: true
120
+ });
121
+ hasSidebar = true;
122
+ console.log(` ✅ sidebar captured`);
123
+ } catch {
124
+ console.log(" ⚠ No sidebar found");
125
+ }
126
+ if (!hasPopup && !hasSidebar) console.warn("\n \x1B[33m⚠⚠⚠ WARNING: Extension has neither a popup nor a sidebar!\x1B[0m\n");
127
+ try {
128
+ const optionsPage = await browser.newPage();
129
+ const optionsUrl = `chrome-extension://${extensionId}/options.html`;
130
+ const response = await optionsPage.goto(optionsUrl, {
131
+ waitUntil: "domcontentloaded",
132
+ timeout: 5e3
133
+ });
134
+ if (response && response.ok()) {
135
+ await optionsPage.waitForSelector(waitSelector);
136
+ await new Promise((r) => setTimeout(r, settleDelay));
137
+ captured.options = await optionsPage.screenshot({
138
+ type: "png",
139
+ fullPage: true
140
+ });
141
+ console.log(` ✅ options captured`);
142
+ }
143
+ } catch {
144
+ console.log(" ⚠ No options found");
145
+ }
146
+ return captured;
147
+ } catch (err) {
148
+ if (err?.message === "An `executablePath` or `channel` must be specified for `puppeteer-core`") throw new Error("Chromium not detected. Set the VITESHOT_CHROME_PATH env var to your Chromium executable.");
149
+ else throw err;
150
+ } finally {
151
+ await browser?.close().catch(() => {});
152
+ }
153
+ }
154
+ /**
155
+ * Capture popup/sidebar/options screenshots for all locales (batch).
156
+ * Used by the `export` command.
157
+ */
158
+ async function captureExtensionScreenshots(options) {
159
+ const cwd = process.cwd();
160
+ const extensionPath = resolve(cwd, options.extensionPath);
161
+ const locales = options.localesDir ? await getLocales(resolve(cwd, options.localesDir)) : [];
162
+ const languages = locales.length > 0 ? locales.map((l) => l.language) : ["_default"];
163
+ console.log(`\n\x1b[1mCapturing extension screenshots${locales.length > 0 ? ` for ${locales.length} locales` : ""}...\x1b[0m\n`);
164
+ const result = /* @__PURE__ */ new Map();
165
+ for (const language of languages) {
166
+ const captured = await captureLocale({
167
+ extensionPath,
168
+ chromePath: options.chromePath,
169
+ language,
170
+ waitSelector: options.waitSelector,
171
+ settleDelay: options.settleDelay
172
+ });
173
+ result.set(language, captured);
174
+ }
175
+ console.log("");
176
+ return result;
177
+ }
178
+
179
+ //#endregion
180
+ export { logInvalidDesignFiles as i, captureLocale as n, getScreenshots as r, captureExtensionScreenshots as t };
package/dist/cli.mjs ADDED
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env node
2
+ import { styleText } from "node:util";
3
+
4
+ //#region src/cli.ts
5
+ const DEFAULT_DIR = "store";
6
+ const [command, ...args] = process.argv.slice(2);
7
+ const showHelp = process.argv.includes("--help") || process.argv.includes("-h");
8
+ function getFlag(name) {
9
+ const prefix = `--${name}=`;
10
+ return args.find((a) => a.startsWith(prefix))?.slice(prefix.length);
11
+ }
12
+ switch (command) {
13
+ case "dev": {
14
+ const { DEV_HELP_MESSAGE } = await import("./help-messages-B_-FA6OE.mjs");
15
+ if (showHelp) {
16
+ console.log(DEV_HELP_MESSAGE);
17
+ process.exit(0);
18
+ }
19
+ const { createServer } = await import("./create-server-DwNGwEEV.mjs");
20
+ const server = await createServer(args[0] ?? DEFAULT_DIR);
21
+ await server.listen();
22
+ server.printUrls();
23
+ break;
24
+ }
25
+ case "export": {
26
+ const { EXPORT_HELP_MESSAGE } = await import("./help-messages-B_-FA6OE.mjs");
27
+ if (showHelp) {
28
+ console.log(EXPORT_HELP_MESSAGE);
29
+ process.exit(0);
30
+ }
31
+ const dir = args[0] ?? DEFAULT_DIR;
32
+ const { resolveConfig } = await import("./config-Cs-Dm27M.mjs");
33
+ const config = await resolveConfig(dir);
34
+ config.chromePath = getFlag("chromePath") ?? config.chromePath;
35
+ const { exportScreenshots } = await import("./export-screenshots-B77VFVWT.mjs");
36
+ await exportScreenshots(config);
37
+ process.exit(0);
38
+ }
39
+ case "popup": {
40
+ const { POPUP_HELP_MESSAGE } = await import("./help-messages-B_-FA6OE.mjs");
41
+ if (showHelp) {
42
+ console.log(POPUP_HELP_MESSAGE);
43
+ process.exit(0);
44
+ }
45
+ const { exportPopupScreenshot } = await import("./export-popup-screenshot-Dp7G2Ylf.mjs");
46
+ await exportPopupScreenshot({
47
+ extensionPath: getFlag("extension-path") ?? ".output/chrome-mv3",
48
+ chromePath: getFlag("chromePath"),
49
+ outputDir: getFlag("output"),
50
+ waitSelector: getFlag("wait-selector"),
51
+ settleDelay: getFlag("settle-delay") ? Number(getFlag("settle-delay")) : void 0
52
+ });
53
+ process.exit(0);
54
+ }
55
+ case "init": {
56
+ const { INIT_HELP_MESSAGE } = await import("./help-messages-B_-FA6OE.mjs");
57
+ if (showHelp) {
58
+ console.log(INIT_HELP_MESSAGE);
59
+ process.exit(0);
60
+ }
61
+ const { init } = await import("./init-BUrgR61N.mjs");
62
+ await init(args[0] ?? DEFAULT_DIR);
63
+ process.exit(0);
64
+ }
65
+ default: {
66
+ const { GENERAL_HELP_MESSAGE } = await import("./help-messages-B_-FA6OE.mjs");
67
+ if (showHelp) {
68
+ console.log(GENERAL_HELP_MESSAGE);
69
+ process.exit(0);
70
+ }
71
+ console.log(`${GENERAL_HELP_MESSAGE}\n\nUnknown command: ${styleText("yellow", command || "<none>")}`);
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ //#endregion
77
+ export { };