@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 +21 -0
- package/README.md +12 -0
- package/assets/dashboard.ts +197 -0
- package/dist/capture-extension-screenshots-DQWqq69W.mjs +180 -0
- package/dist/cli.mjs +77 -0
- package/dist/config-BvHGePNd.mjs +264 -0
- package/dist/config-Cs-Dm27M.mjs +4 -0
- package/dist/create-server-CpPbF7ms.mjs +14 -0
- package/dist/create-server-DwNGwEEV.mjs +5 -0
- package/dist/export-popup-screenshot-Dp7G2Ylf.mjs +3 -0
- package/dist/export-popup-screenshot-UhxUtKCP.mjs +145 -0
- package/dist/export-screenshots-B6IGCV4J.mjs +92 -0
- package/dist/export-screenshots-B77VFVWT.mjs +4 -0
- package/dist/get-locales-DgKRepYE.mjs +26 -0
- package/dist/help-messages-B_-FA6OE.mjs +73 -0
- package/dist/index.d.mts +112 -0
- package/dist/index.mjs +7 -0
- package/dist/init-BUrgR61N.mjs +63 -0
- package/package.json +62 -0
|
@@ -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,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,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,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 };
|