@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.
@@ -0,0 +1,264 @@
1
+ import { t as getLocales } from "./get-locales-DgKRepYE.mjs";
2
+ import { n as captureLocale, r as getScreenshots } from "./capture-extension-screenshots-DQWqq69W.mjs";
3
+ import { dirname, extname, join, resolve } from "node:path";
4
+ import { homedir } from "node:os";
5
+ import { fileURLToPath, pathToFileURL } from "node:url";
6
+
7
+ //#region src/templates/favicon.svg?raw
8
+ var favicon_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 100 100\" width=\"200\" height=\"200\">\n <polygon points=\"50,15 61,40 88,40 66,57 74,82 50,65 26,82 34,57 12,40 39,40\" fill=\"yellow\" stroke=\"black\" stroke-width=\"1\"/>\n</svg>\n";
9
+
10
+ //#endregion
11
+ //#region src/templates/dashboard.html?raw
12
+ var dashboard_default = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Viteshot</title>\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n <style>\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n min-width: 0;\n min-height: 0;\n list-style: none;\n }\n\n :root {\n --color-base: #000000;\n --color-base-content: #ffffff;\n --color-accent: #2ce4f4;\n --spacing: 0.25rem;\n color-scheme: dark;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n }\n [data-theme=\"light\"] {\n --color-base: #f8f8f0;\n --color-base-content: #000000;\n --color-accent: #008996;\n color-scheme: light;\n }\n body {\n background-color: var(--color-base);\n color: var(--color-base-content);\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n \"Segoe UI\",\n Roboto,\n \"Helvetica Neue\",\n Arial,\n sans-serif;\n }\n a {\n color: var(--color-accent);\n font-weight: 500;\n text-decoration: underline;\n }\n a:hover {\n color: color-mix(\n in srgb,\n var(--color-accent) 70%,\n var(--color-base-content)\n );\n }\n\n .app {\n display: flex;\n flex-direction: column;\n gap: calc(4 * var(--spacing));\n }\n .header {\n display: flex;\n align-items: center;\n gap: calc(2 * var(--spacing));\n padding: calc(4 * var(--spacing)) calc(4 * var(--spacing)) 0\n calc(4 * var(--spacing));\n }\n .header .left {\n flex: 1;\n display: flex;\n align-items: center;\n gap: calc(4 * var(--spacing));\n }\n .header .left h1 {\n font-size: calc(6 * var(--spacing));\n }\n .header .left a {\n font-size: calc(4 * var(--spacing));\n display: flex;\n align-items: center;\n }\n .header .left a svg {\n width: calc(4 * var(--spacing));\n height: calc(4 * var(--spacing));\n }\n .header .language-select {\n flex-shrink: 0;\n height: calc(8 * var(--spacing));\n padding: 0 calc(1 * var(--spacing)) 0 calc(2 * var(--spacing));\n }\n\n .theme-toggle {\n display: block;\n padding: var(--spacing);\n width: calc(8 * var(--spacing));\n height: calc(8 * var(--spacing));\n }\n .theme-toggle > .light {\n display: none;\n }\n *[data-theme=\"light\"] .theme-toggle > .dark {\n display: none;\n }\n *[data-theme=\"light\"] .theme-toggle > .light {\n display: block;\n }\n\n .list-item {\n display: flex;\n flex-direction: column;\n gap: calc(2 * var(--spacing));\n border-top: calc(0.5 * var(--spacing)) solid\n color-mix(in srgb, var(--color-base-content) 50%, transparent);\n }\n .list-item .title-row {\n padding: calc(4 * var(--spacing)) calc(4 * var(--spacing)) 0\n calc(4 * var(--spacing));\n font-size: calc(4 * var(--spacing));\n font-weight: normal;\n }\n .list-item .title-row .name {\n font-size: calc(5 * var(--spacing));\n position: relative;\n }\n .list-item .title-row .name:before {\n content: \"# \";\n position: absolute;\n left: calc(-4 * var(--spacing));\n opacity: 0%;\n transition: 0.1s;\n }\n .list-item .title-row .name:hover:before {\n opacity: 70%;\n }\n .list-item .title-row .size {\n color: color-mix(in srgb, var(--color-base-content) 50%, transparent);\n }\n .list-item .iframe-wrapper {\n overflow-x: auto;\n width: 100%;\n padding: 0 calc(4 * var(--spacing)) calc(4 * var(--spacing))\n calc(4 * var(--spacing));\n }\n .list-item iframe {\n border: calc(0.5 * var(--spacing)) solid\n color-mix(in srgb, var(--color-base-content) 50%, transparent);\n }\n </style>\n </head>\n <body>\n <div id=\"app\" class=\"app\"></div>\n <script type=\"module\" src=\"/viteshot-assets/dashboard.ts\"><\/script>\n </body>\n</html>\n";
13
+
14
+ //#endregion
15
+ //#region src/templates/screenshot.html?raw
16
+ var screenshot_default = "<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n <title>Screenshot</title>\n <link rel=\"icon\" type=\"image/svg+xml\" href=\"/favicon.svg\" />\n {{css}}\n </head>\n <body>\n <div id=\"app\" class=\"app\"></div>\n <script type=\"module\">\n import { renderScreenshot } from \"/viteshot-virtual/render-screenshot/{{screenshot.id}}.js\";\n import messages from \"/viteshot-virtual/messages/{{locale.id}}\";\n renderScreenshot(app, messages, \"{{locale.language}}\");\n <\/script>\n </body>\n</html>\n";
17
+
18
+ //#endregion
19
+ //#region src/templates/render-html-screenshot.js?raw
20
+ var render_html_screenshot_default = "import HTML from \"/@fs/{{path}}?raw\";\n\nexport function renderScreenshot(container, messages, localeLanguage) {\n const captureBase = localeLanguage || \"_default\";\n const captureVars = {\n \"captures.popup\": `/viteshot-virtual/captures/${captureBase}/popup.png`,\n \"captures.sidebar\": `/viteshot-virtual/captures/${captureBase}/sidebar.png`,\n \"captures.options\": `/viteshot-virtual/captures/${captureBase}/options.png`,\n };\n const allVars = { ...messages, ...captureVars };\n container.innerHTML = Object.entries(allVars).reduce(\n (text, [key, value]) =>\n text.replace(new RegExp(`\\\\{\\\\{\\\\s*?${key}\\\\s*?\\\\}\\\\}`, \"g\"), value),\n HTML,\n );\n}\n";
21
+
22
+ //#endregion
23
+ //#region src/templates/render-svelte-screenshot.js?raw
24
+ var render_svelte_screenshot_default = "import Component from \"/@fs/{{path}}\";\nimport { mount } from \"svelte\";\n\n/**\n * @typedef {Object} Props\n * @property {Record<string, string>} t\n * @property {Object} captures\n * @property {string} captures.options\n * @property {string} captures.sidebar\n * @property {string} captures.popup\n */\n\nexport function renderScreenshot(container, messages, localeLanguage) {\n const captureBase = localeLanguage || \"_default\";\n /** @type {Props} */\n const props = {\n t: messages,\n captures: {\n popup: `/viteshot-virtual/captures/${captureBase}/popup.png`,\n sidebar: `/viteshot-virtual/captures/${captureBase}/sidebar.png`,\n options: `/viteshot-virtual/captures/${captureBase}/options.png`,\n },\n };\n\n mount(Component, {\n target: container,\n props,\n });\n}\n";
25
+
26
+ //#endregion
27
+ //#region src/utils.ts
28
+ async function getViteshotAssetsDir() {
29
+ return resolve(dirname(fileURLToPath(import.meta.resolve("@kernocal/viteshot"))), "../assets");
30
+ }
31
+
32
+ //#endregion
33
+ //#region src/core/resolver-plugin.ts
34
+ function resolveTemplate(options) {
35
+ return (server) => () => server.middlewares.use(async (req, res, next) => {
36
+ if (!req.originalUrl) return;
37
+ const url = new URL(req.originalUrl, "http://localhost");
38
+ if (!options.match(url)) return next();
39
+ const text = options.vars == null ? options.template : applyTemplateVars(options.template, options.vars(url));
40
+ return res.end(options.transform ? await server.transformIndexHtml(req.originalUrl, text) : text);
41
+ });
42
+ }
43
+ const VIRTUAL_SCREENSHOTS_FILTER = { id: [/viteshot-virtual\/screenshots/] };
44
+ const VIRTUAL_LOCALES_FILTER = { id: [/viteshot-virtual\/locales/] };
45
+ function applyTemplateVars(template, vars) {
46
+ return Object.entries(vars).reduce((template, [key, value]) => template.replaceAll(`{{${key}}}`, value), template);
47
+ }
48
+ const RENDER_SCREENSHOT_JS_TEMPLATES = {
49
+ ".html": render_html_screenshot_default,
50
+ ".svelte": render_svelte_screenshot_default
51
+ };
52
+ const resolverPlugin = (config) => [
53
+ {
54
+ name: "viteshot:resolve-favicon",
55
+ configureServer: resolveTemplate({
56
+ match: (url) => url.pathname === "/favicon.svg",
57
+ template: favicon_default
58
+ })
59
+ },
60
+ {
61
+ name: "viteshot:resolve-dashboard-html",
62
+ configureServer: resolveTemplate({
63
+ match: (url) => url.pathname === "/",
64
+ template: dashboard_default,
65
+ transform: true
66
+ })
67
+ },
68
+ {
69
+ name: "viteshot:resolve-screenshot-html",
70
+ configureServer: resolveTemplate({
71
+ match: (url) => /\/screenshot\/.*?\/.*?/.test(url.pathname),
72
+ template: screenshot_default,
73
+ transform: true,
74
+ vars: (url) => {
75
+ const [localeId, screenshotId] = url.pathname.slice(12, -5).split("/");
76
+ const language = localeId === "null" ? "" : localeId.replace(/\.json$/, "");
77
+ const links = config.css.map((file) => `<link rel="stylesheet" href="/@fs${join(config.root, file).replaceAll("\\", "/")}" />`);
78
+ return {
79
+ "screenshot.id": screenshotId,
80
+ "locale.id": localeId,
81
+ "locale.language": language,
82
+ css: links.join("")
83
+ };
84
+ }
85
+ })
86
+ },
87
+ {
88
+ name: "viteshot:resolve-assets",
89
+ resolveId: {
90
+ filter: { id: [/\/viteshot-assets\//] },
91
+ handler: async (id) => {
92
+ return join(await getViteshotAssetsDir(), id.slice(17));
93
+ }
94
+ }
95
+ },
96
+ {
97
+ name: "viteshot:resolve-virtual:screenshots",
98
+ resolveId: {
99
+ filter: VIRTUAL_SCREENSHOTS_FILTER,
100
+ handler: (id) => id
101
+ },
102
+ load: {
103
+ filter: VIRTUAL_SCREENSHOTS_FILTER,
104
+ handler: async () => {
105
+ const screenshots = await getScreenshots(config.designsDir);
106
+ return `export default ${JSON.stringify(screenshots)}`;
107
+ }
108
+ }
109
+ },
110
+ {
111
+ name: "viteshot:resolve-virtual:locales",
112
+ resolveId: {
113
+ filter: VIRTUAL_LOCALES_FILTER,
114
+ handler: (id) => id
115
+ },
116
+ load: {
117
+ filter: VIRTUAL_LOCALES_FILTER,
118
+ handler: async () => {
119
+ const locales = await getLocales(config.localesDir);
120
+ return `export default ${JSON.stringify(locales)}`;
121
+ }
122
+ }
123
+ },
124
+ {
125
+ name: "viteshot:resolve-virtual:render-screenshot",
126
+ resolveId: {
127
+ filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
128
+ handler: (id) => id
129
+ },
130
+ load: {
131
+ filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
132
+ handler: (id) => {
133
+ const screenshotId = decodeURIComponent(id.slice(36, -3));
134
+ if (!screenshotId) throw Error(`Required query param "id" not provided for ${id}`);
135
+ const ext = extname(screenshotId);
136
+ const path = join(config.designsDir, screenshotId).replaceAll("\\", "/");
137
+ const template = RENDER_SCREENSHOT_JS_TEMPLATES[ext];
138
+ if (!template) throw Error(`Unsupported screenshot file type (${ext}). Must be one of ${Object.keys(RENDER_SCREENSHOT_JS_TEMPLATES).join(", ")}`);
139
+ return applyTemplateVars(template, { path });
140
+ }
141
+ }
142
+ },
143
+ {
144
+ name: "viteshot:resolve-virtual:messages",
145
+ resolveId: {
146
+ filter: { id: [/\/viteshot-virtual\/messages/] },
147
+ handler: (id) => {
148
+ const localeId = id.slice(27);
149
+ if (!localeId || localeId === "null") return "\0viteshot-virtual/messages-empty";
150
+ return join(config.localesDir, localeId);
151
+ }
152
+ },
153
+ load: {
154
+ filter: { id: [/\0viteshot-virtual\/messages-empty/] },
155
+ handler: () => "export default {}"
156
+ }
157
+ },
158
+ {
159
+ name: "viteshot:serve-captures",
160
+ configureServer: (server) => {
161
+ const pending = /* @__PURE__ */ new Map();
162
+ return () => server.middlewares.use(async (req, res, next) => {
163
+ const match = req.originalUrl?.match(/^\/viteshot-virtual\/captures\/([^/]+)\/(popup|sidebar|options)\.png$/);
164
+ if (!match) return next();
165
+ const [, lang, type] = match;
166
+ if (config.extensionPath && config.captures && !config.captures.has(lang)) {
167
+ if (!pending.has(lang)) {
168
+ const promise = captureLocale({
169
+ extensionPath: config.extensionPath,
170
+ chromePath: config.chromePath,
171
+ language: lang
172
+ }).then((captured) => {
173
+ config.captures.set(lang, captured);
174
+ pending.delete(lang);
175
+ return captured;
176
+ });
177
+ pending.set(lang, promise);
178
+ }
179
+ await pending.get(lang);
180
+ }
181
+ const buf = config.captures?.get(lang)?.[type];
182
+ if (!buf) {
183
+ res.statusCode = 404;
184
+ return res.end();
185
+ }
186
+ res.setHeader("Content-Type", "image/png");
187
+ res.setHeader("Cache-Control", "no-store");
188
+ res.end(buf);
189
+ });
190
+ }
191
+ },
192
+ {
193
+ name: "viteshot:resolve-virtual:captures",
194
+ resolveId: {
195
+ filter: { id: [/viteshot-virtual\/captures$/] },
196
+ handler: (id) => id
197
+ },
198
+ load: {
199
+ filter: { id: [/viteshot-virtual\/captures$/] },
200
+ handler: () => {
201
+ const manifest = {};
202
+ for (const [lang, images] of config.captures ?? []) manifest[lang] = Object.keys(images).filter((k) => images[k] != null);
203
+ return `export default ${JSON.stringify(manifest)}`;
204
+ }
205
+ }
206
+ }
207
+ ];
208
+
209
+ //#endregion
210
+ //#region src/core/config.ts
211
+ function defineConfig(config) {
212
+ return config;
213
+ }
214
+ async function importConfig(root) {
215
+ const configFileUrl = pathToFileURL(join(root, "viteshot.config.ts")).href;
216
+ try {
217
+ return (await import(configFileUrl)).default ?? {};
218
+ } catch (err) {
219
+ if (err?.message?.includes?.("Cannot find module")) return {};
220
+ throw err;
221
+ }
222
+ }
223
+ async function resolveConfig(dir = process.cwd()) {
224
+ const root = resolve(dir);
225
+ const { screenshots: _screenshots, ...vite } = await importConfig(root);
226
+ const localesDir = _screenshots?.localesDir ? resolve(root, _screenshots.localesDir) : join(root, "locales");
227
+ const designsDir = _screenshots?.designsDir ? resolve(root, _screenshots.designsDir) : join(root, "designs");
228
+ const exportsDir = _screenshots?.exportsDir ? resolve(root, _screenshots.exportsDir) : join(root, "exports");
229
+ const renderConcurrency = _screenshots?.renderConcurrency || 4;
230
+ const extensionPath = resolve(_screenshots?.extensionPath ?? ".output/chrome-mv3");
231
+ const defaultChromePath = join(homedir(), "AppData/Local/Chromium/Application/chrome.exe");
232
+ const chromePath = _screenshots?.chromePath ?? process.env.VITESHOT_CHROME_PATH ?? defaultChromePath;
233
+ const config = {
234
+ root,
235
+ localesDir,
236
+ designsDir,
237
+ exportsDir,
238
+ renderConcurrency,
239
+ puppeteer: _screenshots?.puppeteer,
240
+ css: _screenshots?.css ?? [],
241
+ extensionPath,
242
+ chromePath,
243
+ vite: {
244
+ ...vite,
245
+ root,
246
+ configFile: false
247
+ }
248
+ };
249
+ const assetsDir = await getViteshotAssetsDir();
250
+ config.vite.server ??= {};
251
+ config.vite.server.fs ??= {};
252
+ config.vite.server.fs.allow ??= [];
253
+ config.vite.server.fs.allow.push(assetsDir, root);
254
+ config.vite.plugins ??= [];
255
+ config.vite.plugins.push(resolverPlugin(config));
256
+ config.vite.resolve ??= {};
257
+ config.vite.resolve.external ??= [];
258
+ const external = config.vite.resolve.external;
259
+ if (Array.isArray(external)) external.push("viteshot-assets/dashboard.ts", "viteshot-assets/screenshot.ts", "viteshot-virtual/render-screenshot?id={{screenshot.id}}", "viteshot-virtual/locale?id={{locale.id}}");
260
+ return config;
261
+ }
262
+
263
+ //#endregion
264
+ export { resolveConfig as n, defineConfig as t };
@@ -0,0 +1,4 @@
1
+ import { n as resolveConfig, t as defineConfig } from "./config-BvHGePNd.mjs";
2
+ import "./capture-extension-screenshots-DQWqq69W.mjs";
3
+
4
+ export { resolveConfig };
@@ -0,0 +1,14 @@
1
+ import { n as resolveConfig } from "./config-BvHGePNd.mjs";
2
+ import { i as logInvalidDesignFiles } from "./capture-extension-screenshots-DQWqq69W.mjs";
3
+ import { createServer } from "vite";
4
+
5
+ //#region src/core/create-server.ts
6
+ async function createServer$1(dir) {
7
+ const config = await resolveConfig(dir);
8
+ await logInvalidDesignFiles(config.designsDir);
9
+ config.captures = /* @__PURE__ */ new Map();
10
+ return createServer(config.vite);
11
+ }
12
+
13
+ //#endregion
14
+ export { createServer$1 as t };
@@ -0,0 +1,5 @@
1
+ import "./config-BvHGePNd.mjs";
2
+ import "./capture-extension-screenshots-DQWqq69W.mjs";
3
+ import { t as createServer } from "./create-server-CpPbF7ms.mjs";
4
+
5
+ export { createServer };
@@ -0,0 +1,3 @@
1
+ import { t as exportPopupScreenshot } from "./export-popup-screenshot-UhxUtKCP.mjs";
2
+
3
+ export { exportPopupScreenshot };
@@ -0,0 +1,145 @@
1
+ import { t as getLocales } from "./get-locales-DgKRepYE.mjs";
2
+ import { basename, extname, join, relative, resolve } from "node:path";
3
+ import { access, mkdir } from "node:fs/promises";
4
+ import puppeteer from "puppeteer-core";
5
+
6
+ //#region src/core/export-popup-screenshot.ts
7
+ async function exists(path) {
8
+ return access(path).then(() => true, () => false);
9
+ }
10
+ async function uniquePath(dir, name) {
11
+ let outputPath = join(dir, name);
12
+ while (await exists(outputPath)) {
13
+ const ext = extname(name);
14
+ name = basename(name, ext) + "9" + ext;
15
+ outputPath = join(dir, name);
16
+ }
17
+ return outputPath;
18
+ }
19
+ async function exportPopupScreenshot(options) {
20
+ const cwd = process.cwd();
21
+ const extensionPath = resolve(cwd, options.extensionPath);
22
+ const baseOutputDir = resolve(cwd, options.outputDir ?? "exports");
23
+ const waitSelector = options.waitSelector ?? "body";
24
+ const settleDelay = options.settleDelay ?? 300;
25
+ const locales = options.localesDir ? await getLocales(resolve(cwd, options.localesDir)) : [];
26
+ const runs = locales.length > 0 ? locales.map((l) => ({
27
+ language: l.language,
28
+ outputDir: join(baseOutputDir, l.language)
29
+ })) : [{
30
+ language: void 0,
31
+ outputDir: baseOutputDir
32
+ }];
33
+ console.log(`\n\x1b[1mExporting popup screenshots${locales.length > 0 ? ` for ${locales.length} locales` : ""}...\x1b[0m\n`);
34
+ for (const run of runs) {
35
+ if (run.language) console.log(` \x1b[1m[${run.language}]\x1b[0m`);
36
+ let browser;
37
+ try {
38
+ await mkdir(run.outputDir, { recursive: true });
39
+ browser = await puppeteer.launch({
40
+ executablePath: options.chromePath,
41
+ headless: false,
42
+ args: [
43
+ `--disable-extensions-except=${extensionPath}`,
44
+ `--load-extension=${extensionPath}`,
45
+ "--window-size=1280,800",
46
+ ...run.language ? [`--lang=${run.language}`] : []
47
+ ]
48
+ });
49
+ const page = (await browser.pages())[0] ?? await browser.newPage();
50
+ await page.goto("data:text/html,<h1>viteshot</h1>");
51
+ const client = await page.createCDPSession();
52
+ const extensionId = await new Promise((resolve, reject) => {
53
+ const timeout = setTimeout(() => reject(/* @__PURE__ */ new Error("Timed out waiting for extension to load")), 2e4);
54
+ const poll = async () => {
55
+ const { targetInfos } = await client.send("Target.getTargets");
56
+ const ext = targetInfos.find((t) => t.url.startsWith("chrome-extension://"));
57
+ if (ext) {
58
+ clearTimeout(timeout);
59
+ resolve(new URL(ext.url).hostname);
60
+ } else setTimeout(poll, 200);
61
+ };
62
+ poll();
63
+ });
64
+ await client.detach();
65
+ console.log(` Extension ID: ${extensionId}`);
66
+ await page.goto("https://example.com", {
67
+ waitUntil: "domcontentloaded",
68
+ timeout: 2e4
69
+ });
70
+ await new Promise((r) => setTimeout(r, 1e3));
71
+ let hasPopup = false;
72
+ let hasSidebar = false;
73
+ try {
74
+ const worker = await (await browser.waitForTarget((target) => target.type() === "service_worker" && target.url().includes(`chrome-extension://${extensionId}`), { timeout: 1e4 })).worker();
75
+ if (!worker) throw new Error("Service worker not available");
76
+ await worker.evaluate("chrome.action.openPopup()");
77
+ const popupPage = await (await browser.waitForTarget((target) => target.type() === "page" && target.url().includes(`chrome-extension://${extensionId}`), { timeout: 5e3 })).asPage();
78
+ await popupPage.waitForSelector(waitSelector);
79
+ await new Promise((r) => setTimeout(r, settleDelay));
80
+ const outputPath = await uniquePath(run.outputDir, "popup.png");
81
+ await popupPage.screenshot({
82
+ type: "png",
83
+ fullPage: true,
84
+ path: outputPath
85
+ });
86
+ hasPopup = true;
87
+ console.log(` ✅ \x1b[2m./${relative(cwd, run.outputDir)}/\x1b[0m\x1b[36m${basename(outputPath)}\x1b[0m`);
88
+ } catch {
89
+ console.log(" ⚠ No popup action found");
90
+ }
91
+ try {
92
+ const sidebarPage = await browser.newPage();
93
+ const sidePanelUrl = `chrome-extension://${extensionId}/sidepanel.html`;
94
+ const response = await sidebarPage.goto(sidePanelUrl, {
95
+ waitUntil: "domcontentloaded",
96
+ timeout: 5e3
97
+ });
98
+ if (!response || !response.ok()) throw new Error("No sidebar");
99
+ await sidebarPage.waitForSelector(waitSelector);
100
+ await new Promise((r) => setTimeout(r, settleDelay));
101
+ const outputPath = await uniquePath(run.outputDir, "sidebar.png");
102
+ await sidebarPage.screenshot({
103
+ type: "png",
104
+ fullPage: true,
105
+ path: outputPath
106
+ });
107
+ hasSidebar = true;
108
+ console.log(` ✅ \x1b[2m./${relative(cwd, run.outputDir)}/\x1b[0m\x1b[36m${basename(outputPath)}\x1b[0m`);
109
+ } catch {
110
+ console.log(" ⚠ No sidebar found");
111
+ }
112
+ if (!hasPopup && !hasSidebar) console.warn("\n \x1B[33m⚠⚠⚠ WARNING: Extension has neither a popup nor a sidebar!\x1B[0m\n");
113
+ try {
114
+ const optionsPage = await browser.newPage();
115
+ const optionsUrl = `chrome-extension://${extensionId}/options.html`;
116
+ const response = await optionsPage.goto(optionsUrl, {
117
+ waitUntil: "domcontentloaded",
118
+ timeout: 5e3
119
+ });
120
+ if (response && response.ok()) {
121
+ await optionsPage.waitForSelector(waitSelector);
122
+ await new Promise((r) => setTimeout(r, settleDelay));
123
+ const optionsOutputPath = await uniquePath(run.outputDir, "options.png");
124
+ await optionsPage.screenshot({
125
+ type: "png",
126
+ fullPage: true,
127
+ path: optionsOutputPath
128
+ });
129
+ console.log(` ✅ \x1b[2m./${relative(cwd, run.outputDir)}/\x1b[0m\x1b[36m${basename(optionsOutputPath)}\x1b[0m`);
130
+ } else await optionsPage.close();
131
+ } catch {
132
+ console.log(" ⚠ No options found");
133
+ }
134
+ } catch (err) {
135
+ 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.");
136
+ else throw err;
137
+ } finally {
138
+ await browser?.close().catch(() => {});
139
+ }
140
+ }
141
+ console.log("");
142
+ }
143
+
144
+ //#endregion
145
+ export { exportPopupScreenshot as t };
@@ -0,0 +1,92 @@
1
+ import { t as getLocales } from "./get-locales-DgKRepYE.mjs";
2
+ import { i as logInvalidDesignFiles, r as getScreenshots, t as captureExtensionScreenshots } from "./capture-extension-screenshots-DQWqq69W.mjs";
3
+ import { dirname, join, relative } from "node:path";
4
+ import { mkdir } from "node:fs/promises";
5
+ import puppeteer from "puppeteer-core";
6
+ import { createServer } from "vite";
7
+ import pMap from "p-map";
8
+ import { Mutex } from "async-mutex";
9
+
10
+ //#region src/core/export-screenshots.ts
11
+ async function exportScreenshots(config) {
12
+ await logInvalidDesignFiles(config.designsDir);
13
+ if (config.extensionPath) config.captures = await captureExtensionScreenshots({
14
+ extensionPath: config.extensionPath,
15
+ chromePath: config.chromePath,
16
+ localesDir: config.localesDir
17
+ });
18
+ const cwd = process.cwd();
19
+ const screenshots = await getScreenshots(config.designsDir);
20
+ const locales = await getLocales(config.localesDir);
21
+ console.log(`\n\x1b[1mExporting ${screenshots.length * (locales.length || 1)} screenshots...\x1b[0m\n`);
22
+ let server;
23
+ let browser;
24
+ try {
25
+ await mkdir(config.exportsDir, { recursive: true });
26
+ server = await createServer(config.vite);
27
+ server.listen();
28
+ const { port } = server.config.server;
29
+ browser = await puppeteer.launch({
30
+ executablePath: config.chromePath,
31
+ ...config.puppeteer?.launchOptions
32
+ });
33
+ const screenshotMutex = new Mutex();
34
+ const nonLocalizablePrefix = /^(?:small-tile|large-tile)/;
35
+ await pMap(screenshots.flatMap((screenshot) => {
36
+ if (locales.length === 0) return [{
37
+ screenshot,
38
+ locale: void 0
39
+ }];
40
+ if (nonLocalizablePrefix.test(screenshot.name)) return [{
41
+ screenshot,
42
+ locale: locales.find((l) => l.language === "en")
43
+ }];
44
+ return locales.map((locale) => ({
45
+ screenshot,
46
+ locale
47
+ }));
48
+ }), async ({ screenshot, locale }) => {
49
+ const outputId = (locale ? `${locale.language}/` : "") + screenshot.name + ".png";
50
+ const outputPath = join(config.exportsDir, outputId);
51
+ await mkdir(dirname(outputPath), { recursive: true });
52
+ const page = await browser.newPage({
53
+ background: true,
54
+ ...config.puppeteer?.newPageOptions
55
+ });
56
+ await page.goto(`http://localhost:${port}/screenshot/${locale?.id ?? "null"}/${screenshot.id}.html`, {
57
+ waitUntil: "networkidle0",
58
+ timeout: 5e3
59
+ });
60
+ await screenshotMutex.runExclusive(async () => {
61
+ await page.bringToFront();
62
+ await page.screenshot({
63
+ captureBeyondViewport: true,
64
+ type: "png",
65
+ ...config.puppeteer?.screenshotOptions,
66
+ clip: {
67
+ x: 0,
68
+ y: 0,
69
+ width: screenshot.width,
70
+ height: screenshot.height
71
+ },
72
+ path: outputPath
73
+ });
74
+ });
75
+ console.log(` ✅ \x1b[2m./${relative(cwd, config.exportsDir)}/\x1b[0m\x1b[36m${outputId}\x1b[0m`);
76
+ await page.close({ runBeforeUnload: false });
77
+ }, {
78
+ concurrency: config.renderConcurrency,
79
+ stopOnError: true
80
+ });
81
+ } catch (err) {
82
+ if (err?.message === "An `executablePath` or `channel` must be specified for `puppeteer-core`") throw Error(`Chromium not detected. Set the VITESHOT_CHROME_PATH env var to your Chromium executable.`);
83
+ else throw err;
84
+ } finally {
85
+ await browser?.close().catch(() => {});
86
+ await server?.close().catch(() => {});
87
+ console.log("");
88
+ }
89
+ }
90
+
91
+ //#endregion
92
+ export { exportScreenshots as t };
@@ -0,0 +1,4 @@
1
+ import "./capture-extension-screenshots-DQWqq69W.mjs";
2
+ import { t as exportScreenshots } from "./export-screenshots-B6IGCV4J.mjs";
3
+
4
+ export { exportScreenshots };
@@ -0,0 +1,26 @@
1
+ import { extname, join } from "node:path";
2
+ import { readFile, readdir } from "node:fs/promises";
3
+
4
+ //#region src/core/get-locales.ts
5
+ async function getLocales(localesDir) {
6
+ let allFilenames;
7
+ try {
8
+ allFilenames = await readdir(localesDir, {});
9
+ } catch {
10
+ return [];
11
+ }
12
+ const jsonFilenames = allFilenames.filter((file) => file.endsWith(".json"));
13
+ return await Promise.all(jsonFilenames.map(async (file) => {
14
+ const ext = extname(file);
15
+ const text = await readFile(join(localesDir, file), "utf-8");
16
+ const messages = JSON.parse(text);
17
+ return {
18
+ id: file,
19
+ language: file.slice(0, -ext.length),
20
+ messages
21
+ };
22
+ }));
23
+ }
24
+
25
+ //#endregion
26
+ export { getLocales as t };
@@ -0,0 +1,73 @@
1
+ import { styleText } from "node:util";
2
+
3
+ //#region package.json
4
+ var version = "0.1.1";
5
+
6
+ //#endregion
7
+ //#region src/core/help-messages.ts
8
+ const GENERAL_HELP_MESSAGE = `${styleText(["bold", "cyan"], "ViteShot")} generates screenshots from code. ${styleText("dim", `(${version})`)}
9
+
10
+ ${styleText("bold", "Usage: viteshot <command>")} ${styleText(["bold", "blue"], "[...flags]")} ${styleText("bold", "[...args]")}
11
+
12
+ ${styleText("bold", "Commands")}
13
+
14
+ ${styleText(["bold", "magenta"], "dev")} ${styleText("dim", "store")} Preview and update your screenshots
15
+ ${styleText(["bold", "magenta"], "export")} ${styleText("dim", "store")} Export screenshots
16
+ ${styleText(["bold", "magenta"], "init")} ${styleText("dim", "store")} Initialize viteshot in your project
17
+ ${styleText(["bold", "magenta"], "popup")} ${styleText("dim", "--extension-path")} Screenshot a browser extension popup
18
+
19
+ ${styleText("dim", "<command>")} ${styleText(["bold", "blue"], "--help")} Print help text for a command
20
+ `;
21
+ const FOLDER_ARG = `${styleText("dim", "<folder>")} The folder containing your Viteshot project ${styleText("dim", "(default: ./store)")}`;
22
+ const DEV_HELP_MESSAGE = `${styleText("bold", "Usage:")} ${styleText(["bold", "green"], "viteshot dev")} ${styleText("bold", "[<folder>]")}
23
+ Spin up the Vite dev server for your project.
24
+
25
+ ${styleText("bold", "Args:")}
26
+
27
+ ${FOLDER_ARG}
28
+
29
+ ${styleText("bold", "Examples:")}
30
+ ${styleText(["bold", "green"], "viteshot init")}
31
+ ${styleText(["bold", "green"], "viteshot init")} promos
32
+ `;
33
+ const EXPORT_HELP_MESSAGE = `
34
+ ${styleText("bold", "Usage:")} ${styleText(["bold", "green"], "viteshot export")} ${styleText("bold", "[<folder>]")}
35
+ Export screenshots from your Viteshot project.
36
+
37
+ ${styleText("bold", "Args:")}
38
+
39
+ ${FOLDER_ARG}
40
+
41
+ ${styleText("bold", "Examples:")}
42
+ ${styleText(["bold", "green"], "viteshot export")}
43
+ ${styleText(["bold", "green"], "viteshot export")} promos
44
+ `;
45
+ const POPUP_HELP_MESSAGE = `${styleText("bold", "Usage:")} ${styleText(["bold", "green"], "viteshot popup")} ${styleText(["bold", "blue"], "--extension-path=<path>")} ${styleText("bold", "[...flags]")}
46
+ Take a screenshot of a browser extension's popup.
47
+
48
+ ${styleText("bold", "Flags:")}
49
+
50
+ ${styleText(["bold", "blue"], "--extension-path")} Path to the unpacked extension directory ${styleText("red", "(required)")}
51
+ ${styleText(["bold", "blue"], "--output")} Output directory for the screenshot ${styleText("dim", "(default: ./screenshots)")}
52
+ ${styleText(["bold", "blue"], "--wait-selector")} CSS selector to wait for before screenshotting ${styleText("dim", "(default: \"body\")")}
53
+ ${styleText(["bold", "blue"], "--settle-delay")} Milliseconds to wait after render ${styleText("dim", "(default: 300)")}
54
+
55
+ ${styleText("bold", "Examples:")}
56
+ ${styleText(["bold", "green"], "viteshot popup")} --extension-path=.output/chrome-mv3
57
+ ${styleText(["bold", "green"], "viteshot popup")} --extension-path=.output/chrome-mv3 --output=screenshots
58
+ `;
59
+ const INIT_HELP_MESSAGE = `
60
+ ${styleText("bold", "Usage:")} ${styleText(["bold", "green"], "viteshot init")} ${styleText("bold", "[<folder>]")}
61
+ Initialize viteshot in your project.
62
+
63
+ ${styleText("bold", "Args:")}
64
+
65
+ ${FOLDER_ARG}
66
+
67
+ ${styleText("bold", "Examples:")}
68
+ ${styleText(["bold", "green"], "viteshot init")}
69
+ ${styleText(["bold", "green"], "viteshot init")} promos
70
+ `;
71
+
72
+ //#endregion
73
+ export { DEV_HELP_MESSAGE, EXPORT_HELP_MESSAGE, GENERAL_HELP_MESSAGE, INIT_HELP_MESSAGE, POPUP_HELP_MESSAGE };