@aklinker1/viteshot 0.1.0 → 0.2.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/README.md +67 -8
- package/assets/dashboard.ts +164 -52
- package/dist/cli.mjs +2 -1
- package/dist/{generate-screenshots-bbolMzf5.mjs → generate-screenshots-W20DkMBq.mjs} +43 -18
- package/dist/index.d.mts +21 -6
- package/dist/index.mjs +1 -1
- package/package.json +13 -7
package/README.md
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
# ViteShot
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Generate store screenshots and promo images with code, powered by Vite.
|
|
6
|
+
|
|
7
|
+
<img width="1241" height="1243" alt="image" src="https://github.com/user-attachments/assets/c6eb4360-f367-4c37-8132-c675c7afe212" />
|
|
6
8
|
|
|
7
9
|
</div>
|
|
8
10
|
|
|
@@ -10,7 +12,7 @@ Build and generate store screenshots and promo images with code, powered by Vite
|
|
|
10
12
|
|
|
11
13
|
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
14
|
|
|
13
|
-
However, AI is really good at building
|
|
15
|
+
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
16
|
|
|
15
17
|
## Get Started
|
|
16
18
|
|
|
@@ -47,25 +49,82 @@ Your screenshot designs go in `store/designs/{name}@{width}x{height}.{ext}`.
|
|
|
47
49
|
- `{width}x{height}`: The size your screenshot should be rendered at (ex: "1280x600", "640x400").
|
|
48
50
|
- `{ext}`: ViteShot supports a variety of file extensions, so you can build your designs using your preferred frontend framework!
|
|
49
51
|
|
|
50
|
-
### HTML
|
|
52
|
+
### HTML
|
|
51
53
|
|
|
52
|
-
|
|
54
|
+
1. Define an HTML fragment, this will be added to the `body` element automatically.
|
|
55
|
+
2. `<link>` to your CSS for styles or use an inline `<style>` block
|
|
56
|
+
3. Translations can be accessed using the handlebars syntax
|
|
53
57
|
|
|
54
58
|
```html
|
|
55
|
-
|
|
59
|
+
<!-- designs/example@640x400.html-->
|
|
60
|
+
<link rel="stylesheet" href="assets/tailwind.css" />
|
|
61
|
+
<div>{{path.to.translation}}</div>
|
|
56
62
|
```
|
|
57
63
|
|
|
64
|
+
> [!NOTE]
|
|
65
|
+
>
|
|
66
|
+
> You don't need a frontend framework to build a simple static layout. Using HTML design files is recommended.
|
|
67
|
+
|
|
58
68
|
### Vue
|
|
59
69
|
|
|
60
|
-
|
|
70
|
+
1. Add `@vitejs/plugin-vue` to `store/viteshot.config.ts`
|
|
71
|
+
2. Define your screenshot in a `.vue` file
|
|
72
|
+
3. Import any styles, or use Vue's `<style>` block
|
|
73
|
+
4. Use the `t` prop to access translations for the current locale
|
|
74
|
+
|
|
75
|
+
```vue
|
|
76
|
+
<!-- store/designs/example@640x480.vue -->
|
|
77
|
+
<script lang="ts" setup>
|
|
78
|
+
import "../assets/tailwind.css";
|
|
79
|
+
|
|
80
|
+
defineProps<{
|
|
81
|
+
t: Record<string, any>;
|
|
82
|
+
}>();
|
|
83
|
+
</script>
|
|
84
|
+
|
|
85
|
+
<template>
|
|
86
|
+
<p>{{ t.path.to.message }}</p>
|
|
87
|
+
</template>
|
|
88
|
+
```
|
|
61
89
|
|
|
62
90
|
### Svelte
|
|
63
91
|
|
|
64
|
-
|
|
92
|
+
> [!WARN]
|
|
93
|
+
>
|
|
94
|
+
> Experimental, not 100% working yet.
|
|
95
|
+
|
|
96
|
+
1. Add `@sveltejs/vite-plugin-svelte` to `store/viteshot.config.ts`
|
|
97
|
+
2. Export your component as the default module from your `.svelte` file
|
|
98
|
+
3. Import any styles or use an inline `<style>` block
|
|
99
|
+
4. Use the `t` prop to access translations for the current locale
|
|
100
|
+
|
|
101
|
+
```svelte
|
|
102
|
+
<!-- store/designs/example@640x480.svelte -->
|
|
103
|
+
<script lang="ts">
|
|
104
|
+
import "../assets/tailwind.css"
|
|
105
|
+
|
|
106
|
+
export let t: Record<string, any>
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<p>{t.path.to.translation}</p>
|
|
110
|
+
```
|
|
65
111
|
|
|
66
112
|
### React
|
|
67
113
|
|
|
68
|
-
|
|
114
|
+
1. Add `@vitejs/plugin-react` to `store/viteshot.config.ts`
|
|
115
|
+
2. Export your component as the default module from your `.tsx` or `.jsx` file
|
|
116
|
+
3. Import any styles
|
|
117
|
+
4. Use the `t` prop to access translations for the current locale
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
// store/designs/example@640x400.tsx
|
|
121
|
+
import React from "react";
|
|
122
|
+
import "../assets/tailwind.css";
|
|
123
|
+
|
|
124
|
+
export default function (props: { t: Record<string, any> }) {
|
|
125
|
+
return <p>{t.path.to.translation}</p>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
69
128
|
|
|
70
129
|
## Styling
|
|
71
130
|
|
package/assets/dashboard.ts
CHANGED
|
@@ -4,82 +4,194 @@ import locales from "viteshot-virtual/locales";
|
|
|
4
4
|
|
|
5
5
|
declare const app: HTMLDivElement;
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
const
|
|
7
|
+
// Icons
|
|
8
|
+
|
|
9
|
+
const LOCALE_FLAGS: Record<string, string> = {
|
|
10
|
+
be: "🇧🇪",
|
|
11
|
+
br: "🇧🇷",
|
|
12
|
+
de: "🇩🇪",
|
|
13
|
+
en: "🇺🇸",
|
|
14
|
+
en_gb: "🇬🇧",
|
|
15
|
+
en_us: "🇺🇸",
|
|
16
|
+
es: "🇪🇸",
|
|
17
|
+
es_mx: "🇲🇽",
|
|
18
|
+
fr: "🇫🇷",
|
|
19
|
+
it: "🇮🇹",
|
|
20
|
+
ja: "🇯🇵",
|
|
21
|
+
pt: "🇧🇷",
|
|
22
|
+
pt_br: "🇧🇷",
|
|
23
|
+
ru: "🇷🇺",
|
|
24
|
+
zh: "🇨🇳",
|
|
25
|
+
zh_tw: "🇹🇼",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const HEROICONS_ARROW_UP_RIGHT_16_SOLID = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="currentColor" fill-rule="evenodd" d="M4.22 11.78a.75.75 0 0 1 0-1.06L9.44 5.5H5.75a.75.75 0 0 1 0-1.5h5.5a.75.75 0 0 1 .75.75v5.5a.75.75 0 0 1-1.5 0V6.56l-5.22 5.22a.75.75 0 0 1-1.06 0" clip-rule="evenodd"/></svg>`;
|
|
29
|
+
const HEROICONS_SUN = `<svg class="light" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 1 1-7.5 0a3.75 3.75 0 0 1 7.5 0"/></svg>`;
|
|
30
|
+
const HEROICONS_MOON = `<svg class="dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><!-- Icon from HeroIcons by Refactoring UI Inc - https://github.com/tailwindlabs/heroicons/blob/master/LICENSE --><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21.752 15.002A9.7 9.7 0 0 1 18 15.75A9.75 9.75 0 0 1 8.25 6c0-1.33.266-2.597.748-3.752A9.75 9.75 0 0 0 3 11.25A9.75 9.75 0 0 0 12.75 21a9.75 9.75 0 0 0 9.002-5.998"/></svg>`;
|
|
10
31
|
|
|
11
|
-
|
|
32
|
+
// Language management
|
|
12
33
|
|
|
13
|
-
|
|
14
|
-
const oldId = localStorage.getItem(CURRENT_LANGUAGE_STORAGE_KEY);
|
|
15
|
-
if (!oldId) return;
|
|
34
|
+
const LANGUAGE_STORAGE_KEY = "viteshot:language";
|
|
16
35
|
|
|
17
|
-
|
|
18
|
-
|
|
36
|
+
let currentLanguageId: string | undefined =
|
|
37
|
+
getStoredLanguage() ?? locales[0]?.id;
|
|
38
|
+
function getStoredLanguage() {
|
|
39
|
+
const prevId = localStorage.getItem(LANGUAGE_STORAGE_KEY);
|
|
40
|
+
if (!prevId || !locales.some((l) => l.id === prevId)) return;
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
return prevId;
|
|
21
43
|
}
|
|
22
|
-
|
|
23
44
|
function setLanguage(languageId: string): void {
|
|
24
45
|
currentLanguageId = languageId;
|
|
25
|
-
localStorage.setItem(
|
|
46
|
+
localStorage.setItem(LANGUAGE_STORAGE_KEY, languageId);
|
|
26
47
|
}
|
|
27
48
|
|
|
28
|
-
//
|
|
49
|
+
// Theme
|
|
29
50
|
|
|
30
|
-
|
|
31
|
-
app.innerHTML = "";
|
|
51
|
+
const THEME_STORAGE_KEY = "viteshot:theme";
|
|
32
52
|
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
header.className = "header";
|
|
53
|
+
let currentTheme: string = getStoredTheme() ?? "dark";
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
55
|
+
function getStoredTheme() {
|
|
56
|
+
const prevTheme = localStorage.getItem(THEME_STORAGE_KEY);
|
|
57
|
+
if (prevTheme !== "light" && prevTheme !== "dark") return;
|
|
40
58
|
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
return prevTheme;
|
|
60
|
+
}
|
|
61
|
+
function toggleTheme(): void {
|
|
62
|
+
const theme = currentTheme === "dark" ? "light" : "dark";
|
|
43
63
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
currentTheme = theme;
|
|
65
|
+
updateTheme();
|
|
66
|
+
localStorage.setItem(THEME_STORAGE_KEY, theme);
|
|
67
|
+
}
|
|
68
|
+
function updateTheme() {
|
|
69
|
+
document.documentElement.setAttribute("data-theme", currentTheme);
|
|
70
|
+
}
|
|
50
71
|
|
|
51
|
-
|
|
72
|
+
// UI Rendering
|
|
52
73
|
|
|
53
|
-
|
|
54
|
-
setLanguage(select.value);
|
|
55
|
-
renderScreenshots();
|
|
56
|
-
});
|
|
74
|
+
type Child = string | HTMLElement | SVGElement | false | undefined | null;
|
|
57
75
|
|
|
58
|
-
|
|
76
|
+
interface ElementTagMap extends HTMLElementTagNameMap {
|
|
77
|
+
svg: SVGElement;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function h<TTag extends keyof ElementTagMap>(
|
|
81
|
+
tag: TTag,
|
|
82
|
+
children: Child[],
|
|
83
|
+
): ElementTagMap[TTag];
|
|
84
|
+
function h<TTag extends keyof ElementTagMap>(
|
|
85
|
+
tag: TTag,
|
|
86
|
+
props: Partial<ElementTagMap[TTag]>,
|
|
87
|
+
children?: Child[],
|
|
88
|
+
): ElementTagMap[TTag];
|
|
89
|
+
function h<TTag extends keyof ElementTagMap>(
|
|
90
|
+
tag: TTag,
|
|
91
|
+
arg1?: any,
|
|
92
|
+
arg2?: any,
|
|
93
|
+
): ElementTagMap[TTag] {
|
|
94
|
+
const hasProps =
|
|
95
|
+
typeof arg1 === "object" &&
|
|
96
|
+
!Array.isArray(arg1) &&
|
|
97
|
+
!(arg1 instanceof HTMLElement);
|
|
98
|
+
const props: Record<string, any> = hasProps ? arg1 : undefined;
|
|
99
|
+
const children: Child[] = hasProps ? arg2 : arg1;
|
|
100
|
+
|
|
101
|
+
const el = document.createElement(tag) as ElementTagMap[TTag];
|
|
102
|
+
|
|
103
|
+
if (props) {
|
|
104
|
+
for (const [key, value] of Object.entries(props)) {
|
|
105
|
+
if (key in el) (el as any)[key] = value;
|
|
106
|
+
else el.setAttribute(key, value);
|
|
59
107
|
}
|
|
60
108
|
}
|
|
61
|
-
app.append(header);
|
|
62
109
|
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
110
|
+
if (children) {
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
if (child != null && child !== false) el.append(child);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
67
115
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
li.append(p);
|
|
116
|
+
return el;
|
|
117
|
+
}
|
|
71
118
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
119
|
+
function svg(outerHtml: string): SVGElement {
|
|
120
|
+
const temp = document.createElement("div");
|
|
121
|
+
temp.innerHTML = outerHtml;
|
|
122
|
+
return temp.firstElementChild as SVGElement;
|
|
123
|
+
}
|
|
77
124
|
|
|
78
|
-
|
|
79
|
-
|
|
125
|
+
function renderHeader() {
|
|
126
|
+
app.append(
|
|
127
|
+
h("div", { className: "header" }, [
|
|
128
|
+
h("div", { className: "left" }, [
|
|
129
|
+
h("h1", "ViteShot"),
|
|
130
|
+
h(
|
|
131
|
+
"a",
|
|
132
|
+
{ href: "https://github.com/aklinker1/viteshot", target: "_blank" },
|
|
133
|
+
[h("span", ["Docs"]), svg(HEROICONS_ARROW_UP_RIGHT_16_SOLID)],
|
|
134
|
+
),
|
|
135
|
+
]),
|
|
136
|
+
locales.length > 0 &&
|
|
137
|
+
h(
|
|
138
|
+
"select",
|
|
139
|
+
{
|
|
140
|
+
className: "language-select",
|
|
141
|
+
onchange: (e) => {
|
|
142
|
+
setLanguage((e.target as HTMLSelectElement).value);
|
|
143
|
+
renderScreenshots();
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
locales.map((l) =>
|
|
147
|
+
h("option", { value: l.id, selected: currentLanguageId === l.id }, [
|
|
148
|
+
`${LOCALE_FLAGS[l.language.replaceAll("-", "_").toLowerCase()] || "🌐"} ${l.language}`,
|
|
149
|
+
]),
|
|
150
|
+
),
|
|
151
|
+
),
|
|
152
|
+
h("button", { className: "theme-toggle", onclick: toggleTheme }, [
|
|
153
|
+
svg(HEROICONS_SUN),
|
|
154
|
+
svg(HEROICONS_MOON),
|
|
155
|
+
]),
|
|
156
|
+
]),
|
|
157
|
+
);
|
|
158
|
+
}
|
|
80
159
|
|
|
81
|
-
|
|
160
|
+
function renderScreenshots() {
|
|
161
|
+
const existingUl = app.querySelector("& > ul");
|
|
162
|
+
|
|
163
|
+
const newUl = h(
|
|
164
|
+
"ul",
|
|
165
|
+
screenshots.map((ss) =>
|
|
166
|
+
h("li", { id: ss.id, className: "list-item" }, [
|
|
167
|
+
h("h2", { className: "title-row" }, [
|
|
168
|
+
h("a", { className: "name", href: `#${ss.id}` }, [ss.name]),
|
|
169
|
+
ss.size && h("span", " "),
|
|
170
|
+
ss.size && h("span", { className: "size" }, [ss.size]),
|
|
171
|
+
]),
|
|
172
|
+
|
|
173
|
+
h("div", { className: "iframe-wrapper" }, [
|
|
174
|
+
h(
|
|
175
|
+
"iframe",
|
|
176
|
+
{
|
|
177
|
+
width: String(ss.width),
|
|
178
|
+
height: String(ss.height),
|
|
179
|
+
src: `/screenshot/${currentLanguageId ? encodeURIComponent(currentLanguageId) : "null"}/${encodeURIComponent(ss.id)}`,
|
|
180
|
+
},
|
|
181
|
+
[],
|
|
182
|
+
),
|
|
183
|
+
]),
|
|
184
|
+
]),
|
|
185
|
+
),
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (existingUl) {
|
|
189
|
+
existingUl.replaceWith(newUl);
|
|
190
|
+
} else {
|
|
191
|
+
app.append(newUl);
|
|
192
|
+
}
|
|
82
193
|
}
|
|
83
194
|
|
|
84
|
-
|
|
195
|
+
updateTheme();
|
|
196
|
+
renderHeader();
|
|
85
197
|
renderScreenshots();
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as createServer, t as generateScreenshots } from "./generate-screenshots-W20DkMBq.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/cli.ts
|
|
4
5
|
const [command, ..._args] = process.argv.slice(2);
|
|
@@ -12,15 +12,27 @@ var favicon_default = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 1
|
|
|
12
12
|
|
|
13
13
|
//#endregion
|
|
14
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:
|
|
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 :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 }\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";
|
|
16
16
|
|
|
17
17
|
//#endregion
|
|
18
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";
|
|
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 console.log(\"TEST\", location.pathname);\n import { renderScreenshot } from \"/viteshot-virtual/render-screenshot/{{screenshot.id}}.js\";\n import messages from \"/viteshot-virtual/messages/{{locale.id}}\";\n console.log({ messages, renderScreenshot });\n\n renderScreenshot(app, messages);\n <\/script>\n </body>\n</html>\n";
|
|
20
20
|
|
|
21
21
|
//#endregion
|
|
22
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])
|
|
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]) =>\n text.replace(new RegExp(`\\\\{\\\\{\\\\s*?${key}\\\\s*?\\\\}\\\\}`, \"g\"), value),\n HTML,\n );\n}\n";
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
//#region src/templates/render-vue-screenshot.js?raw
|
|
27
|
+
var render_vue_screenshot_default = "import Component from \"/@fs/{{path}}\";\nimport { createApp } from \"vue\";\n\nexport function renderScreenshot(container, messages) {\n const app = createApp(Component, { t: messages });\n app.mount(container);\n}\n";
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region src/templates/render-react-screenshot.js?raw
|
|
31
|
+
var render_react_screenshot_default = "import Component from \"/@fs/{{path}}\";\nimport { createElement } from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nexport function renderScreenshot(container, messages) {\n const root = createRoot(container);\n root.render(createElement(Component, { t: messages }));\n}\n";
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/templates/render-svelte-screenshot.js?raw
|
|
35
|
+
var render_svelte_screenshot_default = "import Component from \"/@fs/{{path}}\";\nimport { mount } from \"svelte\";\n\nexport function renderScreenshot(container, messages) {\n console.log(\"svelte\", container);\n\n const app = mount(Component, {\n target: container,\n props: { t: messages },\n });\n console.log(app);\n}\n";
|
|
24
36
|
|
|
25
37
|
//#endregion
|
|
26
38
|
//#region src/utils.ts
|
|
@@ -86,7 +98,13 @@ const VIRTUAL_LOCALES_FILTER = { id: [/viteshot-virtual\/locales/] };
|
|
|
86
98
|
function applyTemplateVars(template, vars) {
|
|
87
99
|
return Object.entries(vars).reduce((template, [key, value]) => template.replaceAll(`{{${key}}}`, value), template);
|
|
88
100
|
}
|
|
89
|
-
const RENDER_SCREENSHOT_JS_TEMPLATES = {
|
|
101
|
+
const RENDER_SCREENSHOT_JS_TEMPLATES = {
|
|
102
|
+
".html": render_html_screenshot_default,
|
|
103
|
+
".vue": render_vue_screenshot_default,
|
|
104
|
+
".tsx": render_react_screenshot_default,
|
|
105
|
+
".jsx": render_react_screenshot_default,
|
|
106
|
+
".svelte": render_svelte_screenshot_default
|
|
107
|
+
};
|
|
90
108
|
const resolverPlugin = (config) => [
|
|
91
109
|
{
|
|
92
110
|
name: "viteshot:resolve-favicon",
|
|
@@ -164,7 +182,7 @@ const resolverPlugin = (config) => [
|
|
|
164
182
|
load: {
|
|
165
183
|
filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
|
|
166
184
|
handler: (id) => {
|
|
167
|
-
const screenshotId = decodeURIComponent(id.slice(36));
|
|
185
|
+
const screenshotId = decodeURIComponent(id.slice(36, -3));
|
|
168
186
|
if (!screenshotId) throw Error(`Required query param "id" not provided for ${id}`);
|
|
169
187
|
const ext = extname(screenshotId);
|
|
170
188
|
const path = join(config.designsDir, screenshotId);
|
|
@@ -193,7 +211,7 @@ function defineConfig(config) {
|
|
|
193
211
|
return config;
|
|
194
212
|
}
|
|
195
213
|
async function importConfig(root) {
|
|
196
|
-
const configFileUrl = pathToFileURL(join(root, "viteshot.config")).href;
|
|
214
|
+
const configFileUrl = pathToFileURL(join(root, "viteshot.config.ts")).href;
|
|
197
215
|
try {
|
|
198
216
|
return (await import(configFileUrl)).default ?? {};
|
|
199
217
|
} catch (err) {
|
|
@@ -203,15 +221,15 @@ async function importConfig(root) {
|
|
|
203
221
|
}
|
|
204
222
|
async function resolveConfig(dir = process.cwd()) {
|
|
205
223
|
const root = resolve(dir);
|
|
206
|
-
const {
|
|
207
|
-
const designsDir =
|
|
208
|
-
const screenshotsDir =
|
|
224
|
+
const { screenshots: _screenshots, ...vite } = await importConfig(root);
|
|
225
|
+
const designsDir = _screenshots?.designsDir ? resolve(root, _screenshots.designsDir) : join(root, "designs");
|
|
226
|
+
const screenshotsDir = _screenshots?.screenshotsDir ? resolve(root, _screenshots.screenshotsDir) : join(root, "screenshots");
|
|
209
227
|
const config = {
|
|
210
228
|
root,
|
|
211
|
-
localesDir:
|
|
229
|
+
localesDir: _screenshots?.localesDir ? resolve(root, _screenshots.localesDir) : join(root, "locales"),
|
|
212
230
|
designsDir,
|
|
213
231
|
screenshotsDir,
|
|
214
|
-
|
|
232
|
+
renderConcurrency: _screenshots?.renderConcurrency || 4,
|
|
215
233
|
vite: {
|
|
216
234
|
...vite,
|
|
217
235
|
configFile: false
|
|
@@ -251,16 +269,22 @@ async function generateScreenshots(dir) {
|
|
|
251
269
|
server = await createServer(config.vite);
|
|
252
270
|
server.listen();
|
|
253
271
|
const { port } = server.config.server;
|
|
254
|
-
browser = await puppeteer.launch({
|
|
272
|
+
browser = await puppeteer.launch({
|
|
273
|
+
executablePath: process.env.VITESHOT_CHROME_PATH,
|
|
274
|
+
...config.puppeteer?.launchOptions
|
|
275
|
+
});
|
|
255
276
|
const screenshotMutex = new Mutex();
|
|
256
277
|
await pMap(screenshots.flatMap((screenshot) => locales.map((locale) => ({
|
|
257
278
|
screenshot,
|
|
258
279
|
locale
|
|
259
280
|
}))), async ({ screenshot, locale }) => {
|
|
260
|
-
const outputId = (locale ? `${locale.language}/` : "") + screenshot.
|
|
281
|
+
const outputId = (locale ? `${locale.language}/` : "") + screenshot.name + ".webp";
|
|
261
282
|
const outputPath = join(config.screenshotsDir, outputId);
|
|
262
283
|
await mkdir(dirname(outputPath), { recursive: true });
|
|
263
|
-
const page = await browser.newPage({
|
|
284
|
+
const page = await browser.newPage({
|
|
285
|
+
background: true,
|
|
286
|
+
...config.puppeteer?.newPageOptions
|
|
287
|
+
});
|
|
264
288
|
await page.goto(`http://localhost:${port}/screenshot/${locale?.id ?? "null"}/${screenshot.id}`, {
|
|
265
289
|
waitUntil: "networkidle0",
|
|
266
290
|
timeout: 5e3
|
|
@@ -269,21 +293,22 @@ async function generateScreenshots(dir) {
|
|
|
269
293
|
await page.bringToFront();
|
|
270
294
|
await page.screenshot({
|
|
271
295
|
captureBeyondViewport: true,
|
|
296
|
+
type: "webp",
|
|
297
|
+
quality: 100,
|
|
298
|
+
...config.puppeteer?.screenshotOptions,
|
|
272
299
|
clip: {
|
|
273
300
|
x: 0,
|
|
274
301
|
y: 0,
|
|
275
302
|
width: screenshot.width,
|
|
276
303
|
height: screenshot.height
|
|
277
304
|
},
|
|
278
|
-
type: "webp",
|
|
279
|
-
quality: 100,
|
|
280
305
|
path: outputPath
|
|
281
306
|
});
|
|
282
307
|
});
|
|
283
308
|
console.log(` ✅ \x1b[2m./${relative(cwd, config.screenshotsDir)}/\x1b[0m\x1b[36m${outputId}\x1b[0m`);
|
|
284
|
-
await page.close();
|
|
309
|
+
await page.close({ runBeforeUnload: false });
|
|
285
310
|
}, {
|
|
286
|
-
concurrency: config.
|
|
311
|
+
concurrency: config.renderConcurrency,
|
|
287
312
|
stopOnError: true
|
|
288
313
|
});
|
|
289
314
|
} catch (err) {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
1
1
|
import { InlineConfig as InlineConfig$1, UserConfig as UserConfig$1, ViteDevServer } from "vite";
|
|
2
|
+
import { CreatePageOptions, LaunchOptions, ScreenshotOptions } from "puppeteer-core";
|
|
2
3
|
|
|
3
4
|
//#region src/core/config.d.ts
|
|
5
|
+
type PuppeteerOptions = {
|
|
6
|
+
launchOptions?: LaunchOptions;
|
|
7
|
+
newPageOptions?: CreatePageOptions;
|
|
8
|
+
screenshotOptions?: Omit<ScreenshotOptions, "clip" | "path">;
|
|
9
|
+
};
|
|
4
10
|
type UserConfig = UserConfig$1 & {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
screenshots?: {
|
|
12
|
+
/** @default "locales" */localesDir?: string; /** @default "designs" */
|
|
13
|
+
designsDir?: string; /** @default "screenshots" */
|
|
14
|
+
screenshotsDir?: string;
|
|
15
|
+
/**
|
|
16
|
+
* How many screenshots can be generated concurrently.
|
|
17
|
+
*
|
|
18
|
+
* @default 4
|
|
19
|
+
*/
|
|
20
|
+
renderConcurrency?: number; /** Override the options passed into puppeteer. */
|
|
21
|
+
puppeteer?: PuppeteerOptions;
|
|
22
|
+
};
|
|
9
23
|
};
|
|
10
24
|
type InlineConfig = UserConfig & {
|
|
11
25
|
root: string;
|
|
@@ -15,7 +29,8 @@ type ResolvedConfig = {
|
|
|
15
29
|
localesDir: string;
|
|
16
30
|
designsDir: string;
|
|
17
31
|
screenshotsDir: string;
|
|
18
|
-
|
|
32
|
+
renderConcurrency: number;
|
|
33
|
+
puppeteer?: PuppeteerOptions;
|
|
19
34
|
vite: InlineConfig$1;
|
|
20
35
|
};
|
|
21
36
|
declare function defineConfig(config: UserConfig): UserConfig;
|
|
@@ -40,4 +55,4 @@ type Screenshot = {
|
|
|
40
55
|
};
|
|
41
56
|
declare function getScreenshots(designsDir: string): Promise<Screenshot[]>;
|
|
42
57
|
//#endregion
|
|
43
|
-
export { InlineConfig, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
|
|
58
|
+
export { InlineConfig, PuppeteerOptions, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as getScreenshots, i as resolveConfig, n as createServer, r as defineConfig, t as generateScreenshots } from "./generate-screenshots-
|
|
1
|
+
import { a as getScreenshots, i as resolveConfig, n as createServer, r as defineConfig, t as generateScreenshots } from "./generate-screenshots-W20DkMBq.mjs";
|
|
2
2
|
|
|
3
3
|
export { createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aklinker1/viteshot",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.
|
|
3
|
+
"description": "Generate store screenshots and promo images with code, powered by Vite",
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "bun@1.3.9",
|
|
7
7
|
"scripts": {
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"build": "tsdown src/index.ts src/cli.ts"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@vitejs/plugin-react": "^6.0.1",
|
|
15
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
14
16
|
"async-mutex": "^0.5.0",
|
|
15
17
|
"natural-compare-lite": "^1.4.0",
|
|
16
18
|
"p-map": "^7.0.4",
|
|
@@ -18,12 +20,18 @@
|
|
|
18
20
|
"puppeteer-core": "^24.37.5"
|
|
19
21
|
},
|
|
20
22
|
"peerDependencies": {
|
|
21
|
-
"vite": "
|
|
23
|
+
"vite": ">=5",
|
|
24
|
+
"vue": ">=3",
|
|
25
|
+
"react": ">=19",
|
|
26
|
+
"react-dom": ">=19",
|
|
27
|
+
"svelte": ">=5"
|
|
22
28
|
},
|
|
23
29
|
"devDependencies": {
|
|
24
30
|
"@aklinker1/check": "^2.2.0",
|
|
31
|
+
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
25
32
|
"@types/bun": "latest",
|
|
26
33
|
"@types/natural-compare-lite": "^1.4.2",
|
|
34
|
+
"@types/react": "^19.2.14",
|
|
27
35
|
"@typescript/native-preview": "^7.0.0-dev.20260223.1",
|
|
28
36
|
"oxlint": "^1.50.0",
|
|
29
37
|
"prettier": "^3.8.1",
|
|
@@ -31,8 +39,7 @@
|
|
|
31
39
|
"publint": "^0.3.17",
|
|
32
40
|
"puppeteer": "^24.37.5",
|
|
33
41
|
"tsdown": "^0.20.3",
|
|
34
|
-
"typescript": "^5"
|
|
35
|
-
"vite": "^7.3.1"
|
|
42
|
+
"typescript": "^5"
|
|
36
43
|
},
|
|
37
44
|
"types": "dist/index.d.mts",
|
|
38
45
|
"module": "dist/index.mjs",
|
|
@@ -42,8 +49,7 @@
|
|
|
42
49
|
"exports": {
|
|
43
50
|
".": {
|
|
44
51
|
"types": "./dist/index.d.mts",
|
|
45
|
-
"
|
|
46
|
-
"require": "./dist/index.cjs"
|
|
52
|
+
"default": "./dist/index.mjs"
|
|
47
53
|
}
|
|
48
54
|
},
|
|
49
55
|
"license": "MIT",
|