@aklinker1/viteshot 0.1.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/LICENSE +21 -0
- package/README.md +79 -0
- package/assets/dashboard.ts +85 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +18 -0
- package/dist/generate-screenshots-bbolMzf5.mjs +300 -0
- package/dist/index.d.mts +43 -0
- package/dist/index.mjs +3 -0
- package/package.json +73 -0
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,79 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# ViteShot
|
|
4
|
+
|
|
5
|
+
Build and generate store screenshots and promo images with code, powered by Vite.
|
|
6
|
+
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
## Why?
|
|
10
|
+
|
|
11
|
+
With AI, it's common for developers with less design experience to generate store screenshots and promo images. However, one shot image generation models aren't very good at this yet.
|
|
12
|
+
|
|
13
|
+
However, AI is really good at building *simple* UIs with HTML! ViteShot provides a simple way for agents in your preferred dev environment to create and generate images in a structured, easy-to-iterate way.
|
|
14
|
+
|
|
15
|
+
## Get Started
|
|
16
|
+
|
|
17
|
+
1. Add Vite and ViteShot as dev dependencies to your project:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
bun add -D vite @aklinker1/viteshot
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. Initialize the `./store` directory:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bun viteshot init ./store
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
3. Add the following scripts to your `package.json`:
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
{
|
|
33
|
+
"scripts": {
|
|
34
|
+
"store:dev": "viteshot dev ./store",
|
|
35
|
+
"store:generate": "viteshot generate ./store",
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Then generate your screenshots with `bun store:generate`! Screenshots will be output to `store/screenshots`.
|
|
41
|
+
|
|
42
|
+
## Design Files
|
|
43
|
+
|
|
44
|
+
Your screenshot designs go in `store/designs/{name}@{width}x{height}.{ext}`.
|
|
45
|
+
|
|
46
|
+
- `{name}`: The name of your screenshot, it can be anything (ex: "small-marquee", "screenshot-1", etc).
|
|
47
|
+
- `{width}x{height}`: The size your screenshot should be rendered at (ex: "1280x600", "640x400").
|
|
48
|
+
- `{ext}`: ViteShot supports a variety of file extensions, so you can build your designs using your preferred frontend framework!
|
|
49
|
+
|
|
50
|
+
### HTML (Recommended)
|
|
51
|
+
|
|
52
|
+
You don't need a frontend framework to build a simple static layout.
|
|
53
|
+
|
|
54
|
+
```html
|
|
55
|
+
<div>My screenshot design</div>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Vue
|
|
59
|
+
|
|
60
|
+
TODO
|
|
61
|
+
|
|
62
|
+
### Svelte
|
|
63
|
+
|
|
64
|
+
TODO
|
|
65
|
+
|
|
66
|
+
### React
|
|
67
|
+
|
|
68
|
+
TODO
|
|
69
|
+
|
|
70
|
+
## Styling
|
|
71
|
+
|
|
72
|
+
1. Setup your framework (like installing the TailwindCSS Vite plugin)
|
|
73
|
+
2. In your screenshots file, `<link>` to your CSS file, import your CSS file, or import your UI framework's components
|
|
74
|
+
|
|
75
|
+
And that's it!
|
|
76
|
+
|
|
77
|
+
## Assets
|
|
78
|
+
|
|
79
|
+
Create an `assets` directory and reference them via `assets/<filename>`. Vite will load them.
|
|
@@ -0,0 +1,85 @@
|
|
|
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
|
+
// Language management
|
|
8
|
+
|
|
9
|
+
const CURRENT_LANGUAGE_STORAGE_KEY = "viteshot:current-language";
|
|
10
|
+
|
|
11
|
+
let currentLanguageId: string | undefined = locales[0]?.language;
|
|
12
|
+
|
|
13
|
+
function restoreLanguage() {
|
|
14
|
+
const oldId = localStorage.getItem(CURRENT_LANGUAGE_STORAGE_KEY);
|
|
15
|
+
if (!oldId) return;
|
|
16
|
+
|
|
17
|
+
const oldLocale = locales.find((l) => l.id === oldId);
|
|
18
|
+
if (!oldLocale) return;
|
|
19
|
+
|
|
20
|
+
currentLanguageId = oldId;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function setLanguage(languageId: string): void {
|
|
24
|
+
currentLanguageId = languageId;
|
|
25
|
+
localStorage.setItem(CURRENT_LANGUAGE_STORAGE_KEY, languageId);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// UI Rendering
|
|
29
|
+
|
|
30
|
+
function renderScreenshots() {
|
|
31
|
+
app.innerHTML = "";
|
|
32
|
+
|
|
33
|
+
const header = document.createElement("div");
|
|
34
|
+
{
|
|
35
|
+
header.className = "header";
|
|
36
|
+
|
|
37
|
+
const h1 = document.createElement("h1");
|
|
38
|
+
h1.textContent = "Dashboard";
|
|
39
|
+
header.append(h1);
|
|
40
|
+
|
|
41
|
+
if (locales.length > 0) {
|
|
42
|
+
const select = document.createElement("select");
|
|
43
|
+
|
|
44
|
+
for (const locale of locales) {
|
|
45
|
+
const option = document.createElement("option");
|
|
46
|
+
option.value = locale.id;
|
|
47
|
+
option.textContent = locale.language;
|
|
48
|
+
select.append(option);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (currentLanguageId) select.value = currentLanguageId;
|
|
52
|
+
|
|
53
|
+
select.addEventListener("change", () => {
|
|
54
|
+
setLanguage(select.value);
|
|
55
|
+
renderScreenshots();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
header.append(select);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
app.append(header);
|
|
62
|
+
|
|
63
|
+
const listItems = screenshots.map((screenshot) => {
|
|
64
|
+
const li = document.createElement("li");
|
|
65
|
+
li.id = screenshot.id;
|
|
66
|
+
li.className = "list-item";
|
|
67
|
+
|
|
68
|
+
const p = document.createElement("p");
|
|
69
|
+
p.textContent = screenshot.name;
|
|
70
|
+
li.append(p);
|
|
71
|
+
|
|
72
|
+
const iframe = document.createElement("iframe");
|
|
73
|
+
iframe.src = `/screenshot/${currentLanguageId ? encodeURIComponent(currentLanguageId) : "null"}/${encodeURIComponent(screenshot.id)}`;
|
|
74
|
+
if (screenshot.width) iframe.width = String(screenshot.width);
|
|
75
|
+
if (screenshot.width) iframe.height = String(screenshot.height);
|
|
76
|
+
li.append(iframe);
|
|
77
|
+
|
|
78
|
+
return li;
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
app.append(...listItems);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
restoreLanguage();
|
|
85
|
+
renderScreenshots();
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { n as createServer, t as generateScreenshots } from "./generate-screenshots-bbolMzf5.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/cli.ts
|
|
4
|
+
const [command, ..._args] = process.argv.slice(2);
|
|
5
|
+
switch (command) {
|
|
6
|
+
case "dev":
|
|
7
|
+
const server = await createServer("example");
|
|
8
|
+
await server.listen();
|
|
9
|
+
server.printUrls();
|
|
10
|
+
break;
|
|
11
|
+
case "generate":
|
|
12
|
+
await generateScreenshots("example");
|
|
13
|
+
process.exit(0);
|
|
14
|
+
default: break;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
export { };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { dirname, extname, join, relative, resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { mkdir, readFile, readdir, rm } from "node:fs/promises";
|
|
4
|
+
import natsort from "natural-compare-lite";
|
|
5
|
+
import { createServer } from "vite";
|
|
6
|
+
import puppeteer from "puppeteer-core";
|
|
7
|
+
import pMap from "p-map";
|
|
8
|
+
import { Mutex } from "async-mutex";
|
|
9
|
+
|
|
10
|
+
//#region src/templates/favicon.svg?raw
|
|
11
|
+
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";
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/templates/dashboard.html?raw
|
|
15
|
+
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 .app {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding: 1rem;\n }\n .header {\n display: flex;\n align-items: center;\n gap: 1rem;\n }\n .header h1 {\n flex: 1;\n }\n .header select {\n flex-shrink: 0;\n padding-left: 0.25rem;\n }\n .list-item {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\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";
|
|
16
|
+
|
|
17
|
+
//#endregion
|
|
18
|
+
//#region src/templates/screenshot.html?raw
|
|
19
|
+
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 <style>\n body {\n margin: 0;\n padding: 0;\n }\n </style>\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}}\";\n import messages from \"/viteshot-virtual/messages/{{locale.id}}\";\n\n renderScreenshot(app, messages);\n <\/script>\n </body>\n</html>\n";
|
|
20
|
+
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/templates/render-html-screenshot.js?raw
|
|
23
|
+
var render_html_screenshot_default = "import HTML from \"/@fs/{{path}}?raw\";\n\nexport function renderScreenshot(container, messages) {\n container.innerHTML = Object.entries(messages).reduce(\n (text, [key, value]) => text.replaceAll(`{{${key}}}`, value),\n HTML,\n );\n}\n";
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/utils.ts
|
|
27
|
+
async function getViteshotAssetsDir() {
|
|
28
|
+
if (process.env.DEV) return resolve("assets");
|
|
29
|
+
else return import.meta.resolve("viteshot");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/core/get-locales.ts
|
|
34
|
+
async function getLocales(localesDir) {
|
|
35
|
+
const jsonFilenames = (await readdir(localesDir, {})).filter((file) => file.endsWith(".json"));
|
|
36
|
+
return await Promise.all(jsonFilenames.map(async (file) => {
|
|
37
|
+
const ext = extname(file);
|
|
38
|
+
const text = await readFile(join(localesDir, file), "utf-8");
|
|
39
|
+
const messages = JSON.parse(text);
|
|
40
|
+
return {
|
|
41
|
+
id: file,
|
|
42
|
+
language: file.slice(0, -ext.length),
|
|
43
|
+
messages
|
|
44
|
+
};
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/core/get-screenshots.ts
|
|
50
|
+
async function getScreenshots(designsDir) {
|
|
51
|
+
return (await readdir(designsDir, {
|
|
52
|
+
recursive: true,
|
|
53
|
+
withFileTypes: true
|
|
54
|
+
})).filter((file) => file.isFile()).map((file) => {
|
|
55
|
+
const path = join(file.parentPath, file.name);
|
|
56
|
+
const ext = extname(file.name);
|
|
57
|
+
const filenameNoExt = file.name.slice(0, -ext.length);
|
|
58
|
+
const [name, size] = filenameNoExt.split("@", 2);
|
|
59
|
+
const [width, height] = size ? size.split("x").map((value) => Number(value)) : [];
|
|
60
|
+
return {
|
|
61
|
+
id: relative(designsDir, path),
|
|
62
|
+
path,
|
|
63
|
+
ext,
|
|
64
|
+
filenameNoExt,
|
|
65
|
+
name,
|
|
66
|
+
size,
|
|
67
|
+
width,
|
|
68
|
+
height
|
|
69
|
+
};
|
|
70
|
+
}).toSorted((a, b) => natsort(a.id, b.id));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/core/resolver-plugin.ts
|
|
75
|
+
function resolveTemplate(options) {
|
|
76
|
+
return (server) => () => server.middlewares.use(async (req, res, next) => {
|
|
77
|
+
if (!req.originalUrl) return;
|
|
78
|
+
const url = new URL(req.originalUrl, "http://localhost");
|
|
79
|
+
if (!options.match(url)) return next();
|
|
80
|
+
const text = options.vars == null ? options.template : applyTemplateVars(options.template, options.vars(url));
|
|
81
|
+
return res.end(options.transform ? await server.transformIndexHtml(req.originalUrl, text) : text);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const VIRTUAL_SCREENSHOTS_FILTER = { id: [/viteshot-virtual\/screenshots/] };
|
|
85
|
+
const VIRTUAL_LOCALES_FILTER = { id: [/viteshot-virtual\/locales/] };
|
|
86
|
+
function applyTemplateVars(template, vars) {
|
|
87
|
+
return Object.entries(vars).reduce((template, [key, value]) => template.replaceAll(`{{${key}}}`, value), template);
|
|
88
|
+
}
|
|
89
|
+
const RENDER_SCREENSHOT_JS_TEMPLATES = { ".html": render_html_screenshot_default };
|
|
90
|
+
const resolverPlugin = (config) => [
|
|
91
|
+
{
|
|
92
|
+
name: "viteshot:resolve-favicon",
|
|
93
|
+
configureServer: resolveTemplate({
|
|
94
|
+
match: (url) => url.pathname === "/favicon.svg",
|
|
95
|
+
template: favicon_default
|
|
96
|
+
})
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "viteshot:resolve-dashboard-html",
|
|
100
|
+
configureServer: resolveTemplate({
|
|
101
|
+
match: (url) => url.pathname === "/",
|
|
102
|
+
template: dashboard_default,
|
|
103
|
+
transform: true
|
|
104
|
+
})
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
name: "viteshot:resolve-screenshot-html",
|
|
108
|
+
configureServer: resolveTemplate({
|
|
109
|
+
match: (url) => /\/screenshot\/.*?\/.*?/.test(url.pathname),
|
|
110
|
+
template: screenshot_default,
|
|
111
|
+
transform: true,
|
|
112
|
+
vars: (url) => {
|
|
113
|
+
const [localeId, screenshotId] = url.pathname.slice(12).split("/");
|
|
114
|
+
return {
|
|
115
|
+
"screenshot.id": screenshotId,
|
|
116
|
+
"locale.id": localeId
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
})
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: "viteshot:resolve-assets",
|
|
123
|
+
resolveId: {
|
|
124
|
+
filter: { id: [/\/viteshot-assets\//] },
|
|
125
|
+
handler: async (id) => {
|
|
126
|
+
return join(await getViteshotAssetsDir(), id.slice(17));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "viteshot:resolve-virtual:screenshots",
|
|
132
|
+
resolveId: {
|
|
133
|
+
filter: VIRTUAL_SCREENSHOTS_FILTER,
|
|
134
|
+
handler: (id) => id
|
|
135
|
+
},
|
|
136
|
+
load: {
|
|
137
|
+
filter: VIRTUAL_SCREENSHOTS_FILTER,
|
|
138
|
+
handler: async () => {
|
|
139
|
+
const screenshots = await getScreenshots(config.designsDir);
|
|
140
|
+
return `export default ${JSON.stringify(screenshots)}`;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: "viteshot:resolve-virtual:locales",
|
|
146
|
+
resolveId: {
|
|
147
|
+
filter: VIRTUAL_LOCALES_FILTER,
|
|
148
|
+
handler: (id) => id
|
|
149
|
+
},
|
|
150
|
+
load: {
|
|
151
|
+
filter: VIRTUAL_LOCALES_FILTER,
|
|
152
|
+
handler: async () => {
|
|
153
|
+
const locales = await getLocales(config.localesDir);
|
|
154
|
+
return `export default ${JSON.stringify(locales)}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "viteshot:resolve-virtual:render-screenshot",
|
|
160
|
+
resolveId: {
|
|
161
|
+
filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
|
|
162
|
+
handler: (id) => id
|
|
163
|
+
},
|
|
164
|
+
load: {
|
|
165
|
+
filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
|
|
166
|
+
handler: (id) => {
|
|
167
|
+
const screenshotId = decodeURIComponent(id.slice(36));
|
|
168
|
+
if (!screenshotId) throw Error(`Required query param "id" not provided for ${id}`);
|
|
169
|
+
const ext = extname(screenshotId);
|
|
170
|
+
const path = join(config.designsDir, screenshotId);
|
|
171
|
+
const template = RENDER_SCREENSHOT_JS_TEMPLATES[ext];
|
|
172
|
+
if (!template) throw Error(`Unsupported screenshot file type (${ext}). Must be one of ${Object.keys(RENDER_SCREENSHOT_JS_TEMPLATES).join(", ")}`);
|
|
173
|
+
return applyTemplateVars(template, { path });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "viteshot:resolve-virtual:messages",
|
|
179
|
+
resolveId: {
|
|
180
|
+
filter: { id: [/\/viteshot-virtual\/messages/] },
|
|
181
|
+
handler: (id) => {
|
|
182
|
+
const localeId = id.slice(27);
|
|
183
|
+
if (!localeId) throw Error(`Required query param "id" not provided for ${id}`);
|
|
184
|
+
return join(config.localesDir, localeId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
//#endregion
|
|
191
|
+
//#region src/core/config.ts
|
|
192
|
+
function defineConfig(config) {
|
|
193
|
+
return config;
|
|
194
|
+
}
|
|
195
|
+
async function importConfig(root) {
|
|
196
|
+
const configFileUrl = pathToFileURL(join(root, "viteshot.config")).href;
|
|
197
|
+
try {
|
|
198
|
+
return (await import(configFileUrl)).default ?? {};
|
|
199
|
+
} catch (err) {
|
|
200
|
+
if (err?.message?.includes?.("Cannot find module")) return {};
|
|
201
|
+
throw err;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
async function resolveConfig(dir = process.cwd()) {
|
|
205
|
+
const root = resolve(dir);
|
|
206
|
+
const { localesDir: _localesDir, designsDir: _designsDir, screenshotsDir: _screenshotsDir, screenshotsConcurrency: _screenshotsConcurrency, ...vite } = await importConfig(root);
|
|
207
|
+
const designsDir = _designsDir ? resolve(root, _designsDir) : join(root, "designs");
|
|
208
|
+
const screenshotsDir = _screenshotsDir ? resolve(root, _screenshotsDir) : join(root, "screenshots");
|
|
209
|
+
const config = {
|
|
210
|
+
root,
|
|
211
|
+
localesDir: _localesDir ? resolve(root, _localesDir) : join(root, "locales"),
|
|
212
|
+
designsDir,
|
|
213
|
+
screenshotsDir,
|
|
214
|
+
screenshotsConcurrency: _screenshotsConcurrency || 1,
|
|
215
|
+
vite: {
|
|
216
|
+
...vite,
|
|
217
|
+
configFile: false
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
config.vite.plugins ??= [];
|
|
221
|
+
config.vite.plugins.push(resolverPlugin(config));
|
|
222
|
+
config.vite.resolve ??= {};
|
|
223
|
+
config.vite.resolve.external ??= [];
|
|
224
|
+
const external = config.vite.resolve.external;
|
|
225
|
+
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}}");
|
|
226
|
+
return config;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
//#endregion
|
|
230
|
+
//#region src/core/create-server.ts
|
|
231
|
+
async function createServer$1(dir) {
|
|
232
|
+
return createServer((await resolveConfig(dir)).vite);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
//#endregion
|
|
236
|
+
//#region src/core/generate-screenshots.ts
|
|
237
|
+
async function generateScreenshots(dir) {
|
|
238
|
+
const config = await resolveConfig(dir);
|
|
239
|
+
const cwd = process.cwd();
|
|
240
|
+
const screenshots = await getScreenshots(config.designsDir);
|
|
241
|
+
const locales = await getLocales(config.localesDir);
|
|
242
|
+
console.log(`\n\x1b[1mGenerating ${screenshots.length * (locales.length || 1)} screenshots...\x1b[0m\n`);
|
|
243
|
+
let server;
|
|
244
|
+
let browser;
|
|
245
|
+
try {
|
|
246
|
+
await rm(config.screenshotsDir, {
|
|
247
|
+
recursive: true,
|
|
248
|
+
force: true
|
|
249
|
+
});
|
|
250
|
+
await mkdir(config.screenshotsDir, { recursive: true });
|
|
251
|
+
server = await createServer(config.vite);
|
|
252
|
+
server.listen();
|
|
253
|
+
const { port } = server.config.server;
|
|
254
|
+
browser = await puppeteer.launch({ executablePath: process.env.VITESHOT_CHROME_PATH });
|
|
255
|
+
const screenshotMutex = new Mutex();
|
|
256
|
+
await pMap(screenshots.flatMap((screenshot) => locales.map((locale) => ({
|
|
257
|
+
screenshot,
|
|
258
|
+
locale
|
|
259
|
+
}))), async ({ screenshot, locale }) => {
|
|
260
|
+
const outputId = (locale ? `${locale.language}/` : "") + screenshot.id.slice(0, -screenshot.ext.length) + ".webp";
|
|
261
|
+
const outputPath = join(config.screenshotsDir, outputId);
|
|
262
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
263
|
+
const page = await browser.newPage({ background: true });
|
|
264
|
+
await page.goto(`http://localhost:${port}/screenshot/${locale?.id ?? "null"}/${screenshot.id}`, {
|
|
265
|
+
waitUntil: "networkidle0",
|
|
266
|
+
timeout: 5e3
|
|
267
|
+
});
|
|
268
|
+
await screenshotMutex.runExclusive(async () => {
|
|
269
|
+
await page.bringToFront();
|
|
270
|
+
await page.screenshot({
|
|
271
|
+
captureBeyondViewport: true,
|
|
272
|
+
clip: {
|
|
273
|
+
x: 0,
|
|
274
|
+
y: 0,
|
|
275
|
+
width: screenshot.width,
|
|
276
|
+
height: screenshot.height
|
|
277
|
+
},
|
|
278
|
+
type: "webp",
|
|
279
|
+
quality: 100,
|
|
280
|
+
path: outputPath
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
console.log(` ✅ \x1b[2m./${relative(cwd, config.screenshotsDir)}/\x1b[0m\x1b[36m${outputId}\x1b[0m`);
|
|
284
|
+
await page.close();
|
|
285
|
+
}, {
|
|
286
|
+
concurrency: config.screenshotsConcurrency,
|
|
287
|
+
stopOnError: true
|
|
288
|
+
});
|
|
289
|
+
} catch (err) {
|
|
290
|
+
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.`);
|
|
291
|
+
else throw err;
|
|
292
|
+
} finally {
|
|
293
|
+
await browser?.close().catch(() => {});
|
|
294
|
+
await server?.close().catch(() => {});
|
|
295
|
+
console.log("");
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
export { getScreenshots as a, resolveConfig as i, createServer$1 as n, defineConfig as r, generateScreenshots as t };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { InlineConfig as InlineConfig$1, UserConfig as UserConfig$1, ViteDevServer } from "vite";
|
|
2
|
+
|
|
3
|
+
//#region src/core/config.d.ts
|
|
4
|
+
type UserConfig = UserConfig$1 & {
|
|
5
|
+
localesDir?: string;
|
|
6
|
+
designsDir?: string;
|
|
7
|
+
screenshotsDir?: string;
|
|
8
|
+
screenshotsConcurrency?: number;
|
|
9
|
+
};
|
|
10
|
+
type InlineConfig = UserConfig & {
|
|
11
|
+
root: string;
|
|
12
|
+
};
|
|
13
|
+
type ResolvedConfig = {
|
|
14
|
+
root: string;
|
|
15
|
+
localesDir: string;
|
|
16
|
+
designsDir: string;
|
|
17
|
+
screenshotsDir: string;
|
|
18
|
+
screenshotsConcurrency: number;
|
|
19
|
+
vite: InlineConfig$1;
|
|
20
|
+
};
|
|
21
|
+
declare function defineConfig(config: UserConfig): UserConfig;
|
|
22
|
+
declare function resolveConfig(dir?: string): Promise<ResolvedConfig>;
|
|
23
|
+
//#endregion
|
|
24
|
+
//#region src/core/create-server.d.ts
|
|
25
|
+
declare function createServer(dir?: string): Promise<ViteDevServer>;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/core/generate-screenshots.d.ts
|
|
28
|
+
declare function generateScreenshots(dir?: string): Promise<void>;
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/core/get-screenshots.d.ts
|
|
31
|
+
type Screenshot = {
|
|
32
|
+
id: string;
|
|
33
|
+
path: string;
|
|
34
|
+
ext: string;
|
|
35
|
+
filenameNoExt: string;
|
|
36
|
+
name: string;
|
|
37
|
+
size: string | undefined;
|
|
38
|
+
width: number | undefined;
|
|
39
|
+
height: number | undefined;
|
|
40
|
+
};
|
|
41
|
+
declare function getScreenshots(designsDir: string): Promise<Screenshot[]>;
|
|
42
|
+
//#endregion
|
|
43
|
+
export { InlineConfig, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
|
package/dist/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@aklinker1/viteshot",
|
|
3
|
+
"description": "Build and generate store screenshots and promo images with code, powered by Vite",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"packageManager": "bun@1.3.9",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"viteshot": "bun run -d process.env.DEV:\"true\" src/cli.ts",
|
|
9
|
+
"dev": "bun viteshot dev example",
|
|
10
|
+
"generate": "bun viteshot generate example",
|
|
11
|
+
"build": "tsdown src/index.ts src/cli.ts"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"async-mutex": "^0.5.0",
|
|
15
|
+
"natural-compare-lite": "^1.4.0",
|
|
16
|
+
"p-map": "^7.0.4",
|
|
17
|
+
"p-queue": "^9.1.0",
|
|
18
|
+
"puppeteer-core": "^24.37.5"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"vite": "^5 || ^6 || ^7"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@aklinker1/check": "^2.2.0",
|
|
25
|
+
"@types/bun": "latest",
|
|
26
|
+
"@types/natural-compare-lite": "^1.4.2",
|
|
27
|
+
"@typescript/native-preview": "^7.0.0-dev.20260223.1",
|
|
28
|
+
"oxlint": "^1.50.0",
|
|
29
|
+
"prettier": "^3.8.1",
|
|
30
|
+
"prettier-plugin-jsdoc": "^1.8.0",
|
|
31
|
+
"publint": "^0.3.17",
|
|
32
|
+
"puppeteer": "^24.37.5",
|
|
33
|
+
"tsdown": "^0.20.3",
|
|
34
|
+
"typescript": "^5",
|
|
35
|
+
"vite": "^7.3.1"
|
|
36
|
+
},
|
|
37
|
+
"types": "dist/index.d.mts",
|
|
38
|
+
"module": "dist/index.mjs",
|
|
39
|
+
"bin": {
|
|
40
|
+
"viteshot": "dist/cli.mjs"
|
|
41
|
+
},
|
|
42
|
+
"exports": {
|
|
43
|
+
".": {
|
|
44
|
+
"types": "./dist/index.d.mts",
|
|
45
|
+
"import": "./dist/index.mjs",
|
|
46
|
+
"require": "./dist/index.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"author": {
|
|
51
|
+
"email": "aaronklinker1@gmail.com",
|
|
52
|
+
"name": "Aaron Klinker"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://jsr.io/@aklinker1/viteshot",
|
|
55
|
+
"repository": {
|
|
56
|
+
"url": "https://github.com/aklinker1/viteshot"
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"assets"
|
|
61
|
+
],
|
|
62
|
+
"publishConfig": {
|
|
63
|
+
"access": "public"
|
|
64
|
+
},
|
|
65
|
+
"keywords": [
|
|
66
|
+
"vite",
|
|
67
|
+
"promo",
|
|
68
|
+
"images",
|
|
69
|
+
"screenshot",
|
|
70
|
+
"generate",
|
|
71
|
+
"store"
|
|
72
|
+
]
|
|
73
|
+
}
|