@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 CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  # ViteShot
4
4
 
5
- Build and generate store screenshots and promo images with code, powered by Vite.
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 *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.
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 (Recommended)
52
+ ### HTML
51
53
 
52
- You don't need a frontend framework to build a simple static layout.
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
- <div>My screenshot design</div>
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
- TODO
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
- TODO
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
- TODO
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
 
@@ -4,82 +4,194 @@ import locales from "viteshot-virtual/locales";
4
4
 
5
5
  declare const app: HTMLDivElement;
6
6
 
7
- // Language management
8
-
9
- const CURRENT_LANGUAGE_STORAGE_KEY = "viteshot:current-language";
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
- let currentLanguageId: string | undefined = locales[0]?.language;
32
+ // Language management
12
33
 
13
- function restoreLanguage() {
14
- const oldId = localStorage.getItem(CURRENT_LANGUAGE_STORAGE_KEY);
15
- if (!oldId) return;
34
+ const LANGUAGE_STORAGE_KEY = "viteshot:language";
16
35
 
17
- const oldLocale = locales.find((l) => l.id === oldId);
18
- if (!oldLocale) return;
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
- currentLanguageId = oldId;
42
+ return prevId;
21
43
  }
22
-
23
44
  function setLanguage(languageId: string): void {
24
45
  currentLanguageId = languageId;
25
- localStorage.setItem(CURRENT_LANGUAGE_STORAGE_KEY, languageId);
46
+ localStorage.setItem(LANGUAGE_STORAGE_KEY, languageId);
26
47
  }
27
48
 
28
- // UI Rendering
49
+ // Theme
29
50
 
30
- function renderScreenshots() {
31
- app.innerHTML = "";
51
+ const THEME_STORAGE_KEY = "viteshot:theme";
32
52
 
33
- const header = document.createElement("div");
34
- {
35
- header.className = "header";
53
+ let currentTheme: string = getStoredTheme() ?? "dark";
36
54
 
37
- const h1 = document.createElement("h1");
38
- h1.textContent = "Dashboard";
39
- header.append(h1);
55
+ function getStoredTheme() {
56
+ const prevTheme = localStorage.getItem(THEME_STORAGE_KEY);
57
+ if (prevTheme !== "light" && prevTheme !== "dark") return;
40
58
 
41
- if (locales.length > 0) {
42
- const select = document.createElement("select");
59
+ return prevTheme;
60
+ }
61
+ function toggleTheme(): void {
62
+ const theme = currentTheme === "dark" ? "light" : "dark";
43
63
 
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
- }
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
- if (currentLanguageId) select.value = currentLanguageId;
72
+ // UI Rendering
52
73
 
53
- select.addEventListener("change", () => {
54
- setLanguage(select.value);
55
- renderScreenshots();
56
- });
74
+ type Child = string | HTMLElement | SVGElement | false | undefined | null;
57
75
 
58
- header.append(select);
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
- const listItems = screenshots.map((screenshot) => {
64
- const li = document.createElement("li");
65
- li.id = screenshot.id;
66
- li.className = "list-item";
110
+ if (children) {
111
+ for (const child of children) {
112
+ if (child != null && child !== false) el.append(child);
113
+ }
114
+ }
67
115
 
68
- const p = document.createElement("p");
69
- p.textContent = screenshot.name;
70
- li.append(p);
116
+ return el;
117
+ }
71
118
 
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);
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
- return li;
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
- app.append(...listItems);
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
- restoreLanguage();
195
+ updateTheme();
196
+ renderHeader();
85
197
  renderScreenshots();
package/dist/cli.mjs CHANGED
@@ -1,4 +1,5 @@
1
- import { n as createServer, t as generateScreenshots } from "./generate-screenshots-bbolMzf5.mjs";
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: 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";
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]) => text.replaceAll(`{{${key}}}`, value),\n HTML,\n );\n}\n";
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 = { ".html": render_html_screenshot_default };
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 { 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");
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: _localesDir ? resolve(root, _localesDir) : join(root, "locales"),
229
+ localesDir: _screenshots?.localesDir ? resolve(root, _screenshots.localesDir) : join(root, "locales"),
212
230
  designsDir,
213
231
  screenshotsDir,
214
- screenshotsConcurrency: _screenshotsConcurrency || 1,
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({ executablePath: process.env.VITESHOT_CHROME_PATH });
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.id.slice(0, -screenshot.ext.length) + ".webp";
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({ background: true });
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.screenshotsConcurrency,
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
- localesDir?: string;
6
- designsDir?: string;
7
- screenshotsDir?: string;
8
- screenshotsConcurrency?: number;
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
- screenshotsConcurrency: number;
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-bbolMzf5.mjs";
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": "Build and generate store screenshots and promo images with code, powered by Vite",
4
- "version": "0.1.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": "^5 || ^6 || ^7"
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
- "import": "./dist/index.mjs",
46
- "require": "./dist/index.cjs"
52
+ "default": "./dist/index.mjs"
47
53
  }
48
54
  },
49
55
  "license": "MIT",