@aklinker1/viteshot 0.2.0 → 0.4.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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  Generate store screenshots and promo images with code, powered by Vite.
6
6
 
7
- <img width="1241" height="1243" alt="image" src="https://github.com/user-attachments/assets/c6eb4360-f367-4c37-8132-c675c7afe212" />
7
+ <img width="840" height="1243" alt="Example" src="https://github.com/user-attachments/assets/caf1cdd9-a9f6-4208-86ef-f3e78b27b28c" />
8
8
 
9
9
  </div>
10
10
 
@@ -12,20 +12,20 @@ Generate store screenshots and promo images with code, powered by Vite.
12
12
 
13
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.
14
14
 
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.
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 export images in a structured, easy-to-iterate way.
16
16
 
17
17
  ## Get Started
18
18
 
19
19
  1. Add Vite and ViteShot as dev dependencies to your project:
20
20
 
21
21
  ```sh
22
- bun add -D vite @aklinker1/viteshot
22
+ bun add -D @aklinker1/viteshot vite
23
23
  ```
24
24
 
25
25
  2. Initialize the `./store` directory:
26
26
 
27
27
  ```sh
28
- bun viteshot init ./store
28
+ bun viteshot init store
29
29
  ```
30
30
 
31
31
  3. Add the following scripts to your `package.json`:
@@ -33,13 +33,13 @@ However, AI is really good at building _simple_ UIs with HTML! ViteShot provides
33
33
  ```sh
34
34
  {
35
35
  "scripts": {
36
- "store:dev": "viteshot dev ./store",
37
- "store:generate": "viteshot generate ./store",
36
+ "viteshot:dev": "viteshot dev store",
37
+ "viteshot:export": "viteshot export store",
38
38
  }
39
39
  }
40
40
  ```
41
41
 
42
- Then generate your screenshots with `bun store:generate`! Screenshots will be output to `store/screenshots`.
42
+ Then export your screenshots with `bun viteshot:export`! Screenshots will be output to `store/exports`.
43
43
 
44
44
  ## Design Files
45
45
 
@@ -52,13 +52,11 @@ Your screenshot designs go in `store/designs/{name}@{width}x{height}.{ext}`.
52
52
  ### HTML
53
53
 
54
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
55
+ 2. Translations can be accessed using the handlebars syntax
57
56
 
58
57
  ```html
59
58
  <!-- designs/example@640x400.html-->
60
- <link rel="stylesheet" href="assets/tailwind.css" />
61
- <div>{{path.to.translation}}</div>
59
+ <p>{{path.to.message}}</p>
62
60
  ```
63
61
 
64
62
  > [!NOTE]
@@ -68,9 +66,7 @@ Your screenshot designs go in `store/designs/{name}@{width}x{height}.{ext}`.
68
66
  ### Vue
69
67
 
70
68
  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
69
+ 2. Use the `t` prop to access translations for the current locale
74
70
 
75
71
  ```vue
76
72
  <!-- store/designs/example@640x480.vue -->
@@ -89,32 +85,23 @@ defineProps<{
89
85
 
90
86
  ### Svelte
91
87
 
92
- > [!WARN]
93
- >
94
- > Experimental, not 100% working yet.
95
-
96
88
  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
89
+ 2. Use the `t` prop to access translations for the current locale
100
90
 
101
91
  ```svelte
102
92
  <!-- store/designs/example@640x480.svelte -->
103
93
  <script lang="ts">
104
- import "../assets/tailwind.css"
105
-
106
94
  export let t: Record<string, any>
107
95
  </script>
108
96
 
109
- <p>{t.path.to.translation}</p>
97
+ <p>{t.path.to.message}</p>
110
98
  ```
111
99
 
112
100
  ### React
113
101
 
114
102
  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
103
+ 2. You're component must be the default export
104
+ 3. Use the `t` prop to access translations for the current locale
118
105
 
119
106
  ```tsx
120
107
  // store/designs/example@640x400.tsx
@@ -122,17 +109,64 @@ import React from "react";
122
109
  import "../assets/tailwind.css";
123
110
 
124
111
  export default function (props: { t: Record<string, any> }) {
125
- return <p>{t.path.to.translation}</p>;
112
+ return <p>{t.path.to.message}</p>;
126
113
  }
127
114
  ```
128
115
 
129
116
  ## Styling
130
117
 
131
- 1. Setup your framework (like installing the TailwindCSS Vite plugin)
132
- 2. In your screenshots file, `<link>` to your CSS file, import your CSS file, or import your UI framework's components
118
+ There are a few ways of adding CSS to your screenshots:
119
+
120
+ 1. Add global, shared CSS to your `viteshot.config.ts` file:
121
+
122
+ ```ts
123
+ import { defineConfig } from "@aklinker1/viteshot";
124
+
125
+ export default defineConfig({
126
+ screenshots: {
127
+ css: ["assets/tailwind.css"], // Supports SCSS, SASS, etc
128
+ },
129
+ });
130
+ ```
131
+
132
+ 2. Import your CSS files from inside your design file
133
+
134
+ ```tsx
135
+ import "../assets/tailwind.css";
136
+
137
+ export default function () {
138
+ return <div>...</div>;
139
+ }
140
+ ```
141
+
142
+ 3. Use inline `<style>` blocks
143
+
144
+ ```html
145
+ <style>
146
+ ...
147
+ </style>
148
+
149
+ <div>...</div>
150
+ ```
133
151
 
134
- And that's it!
152
+ | Method | HTML | Vue | Svelte | React |
153
+ | ---------------- | :--: | :-: | :----: | :---: |
154
+ | Add `css` config | ✅ | ✅ | ✅ | ✅ |
155
+ | Import CSS file | ❌ | ✅ | ✅ | ✅ |
156
+ | Inline `<style>` | ✅ | ✅ | ✅ | ❌ |
135
157
 
136
158
  ## Assets
137
159
 
138
- Create an `assets` directory and reference them via `assets/<filename>`. Vite will load them.
160
+ Vite allows you to put assets in two folders:
161
+
162
+ 1. `assets/`
163
+ 2. `public/`
164
+
165
+ For HTML designs, you need to put files in `public/`, but for the other supported frameworks, you can use `assets/` and import them into your design file.
166
+
167
+ | Method | HTML | Vue | Svelte | React |
168
+ | --------- | :--: | :-: | :----: | :---: |
169
+ | `assets/ | ❌ | ✅ | ✅ | ✅ |
170
+ | `public/` | ✅ | ✅ | ✅ | ✅ |
171
+
172
+ Review [Vite's docs](https://vite.dev/guide/assets) for how to import or use assets from each directory.
@@ -176,7 +176,7 @@ function renderScreenshots() {
176
176
  {
177
177
  width: String(ss.width),
178
178
  height: String(ss.height),
179
- src: `/screenshot/${currentLanguageId ? encodeURIComponent(currentLanguageId) : "null"}/${encodeURIComponent(ss.id)}`,
179
+ src: `/screenshot/${currentLanguageId ? encodeURIComponent(currentLanguageId) : "null"}/${encodeURIComponent(ss.id)}.html`,
180
180
  },
181
181
  [],
182
182
  ),
package/dist/index.d.mts CHANGED
@@ -11,14 +11,19 @@ type UserConfig = UserConfig$1 & {
11
11
  screenshots?: {
12
12
  /** @default "locales" */localesDir?: string; /** @default "designs" */
13
13
  designsDir?: string; /** @default "screenshots" */
14
- screenshotsDir?: string;
14
+ exportsDir?: string;
15
15
  /**
16
- * How many screenshots can be generated concurrently.
16
+ * How many screenshots can be exported concurrently.
17
17
  *
18
18
  * @default 4
19
19
  */
20
20
  renderConcurrency?: number; /** Override the options passed into puppeteer. */
21
21
  puppeteer?: PuppeteerOptions;
22
+ /**
23
+ * List of relative paths from your viteshot.config.ts file to CSS files to
24
+ * add to your screenshot's HTML file as links.
25
+ */
26
+ css?: string[];
22
27
  };
23
28
  };
24
29
  type InlineConfig = UserConfig & {
@@ -28,9 +33,10 @@ type ResolvedConfig = {
28
33
  root: string;
29
34
  localesDir: string;
30
35
  designsDir: string;
31
- screenshotsDir: string;
36
+ exportsDir: string;
32
37
  renderConcurrency: number;
33
- puppeteer?: PuppeteerOptions;
38
+ puppeteer: PuppeteerOptions | undefined;
39
+ css: string[];
34
40
  vite: InlineConfig$1;
35
41
  };
36
42
  declare function defineConfig(config: UserConfig): UserConfig;
@@ -39,20 +45,20 @@ declare function resolveConfig(dir?: string): Promise<ResolvedConfig>;
39
45
  //#region src/core/create-server.d.ts
40
46
  declare function createServer(dir?: string): Promise<ViteDevServer>;
41
47
  //#endregion
42
- //#region src/core/generate-screenshots.d.ts
43
- declare function generateScreenshots(dir?: string): Promise<void>;
48
+ //#region src/core/export-screenshots.d.ts
49
+ declare function exportScreenshots(dir?: string): Promise<void>;
44
50
  //#endregion
45
51
  //#region src/core/get-screenshots.d.ts
46
52
  type Screenshot = {
47
53
  id: string;
48
54
  path: string;
49
55
  ext: string;
50
- filenameNoExt: string;
51
56
  name: string;
52
- size: string | undefined;
53
- width: number | undefined;
54
- height: number | undefined;
57
+ size: string;
58
+ width: number;
59
+ height: number;
55
60
  };
56
61
  declare function getScreenshots(designsDir: string): Promise<Screenshot[]>;
62
+ declare function logInvalidDesignFiles(designsDir: string): Promise<void>;
57
63
  //#endregion
58
- export { InlineConfig, PuppeteerOptions, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
64
+ export { InlineConfig, PuppeteerOptions, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, exportScreenshots, getScreenshots, logInvalidDesignFiles, resolveConfig };
package/dist/index.mjs CHANGED
@@ -1,3 +1,342 @@
1
- import { a as getScreenshots, i as resolveConfig, n as createServer, r as defineConfig, t as generateScreenshots } from "./generate-screenshots-W20DkMBq.mjs";
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 { styleText } from "node:util";
6
+ import { createServer as createServer$1 } from "vite";
7
+ import puppeteer from "puppeteer-core";
8
+ import pMap from "p-map";
9
+ import { Mutex } from "async-mutex";
2
10
 
3
- export { createServer, defineConfig, generateScreenshots, getScreenshots, resolveConfig };
11
+ //#region src/templates/favicon.svg?raw
12
+ 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";
13
+
14
+ //#endregion
15
+ //#region src/templates/dashboard.html?raw
16
+ 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";
17
+
18
+ //#endregion
19
+ //#region src/templates/screenshot.html?raw
20
+ 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 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";
21
+
22
+ //#endregion
23
+ //#region src/templates/render-html-screenshot.js?raw
24
+ 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";
25
+
26
+ //#endregion
27
+ //#region src/templates/render-vue-screenshot.js?raw
28
+ 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";
29
+
30
+ //#endregion
31
+ //#region src/templates/render-react-screenshot.js?raw
32
+ 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";
33
+
34
+ //#endregion
35
+ //#region src/templates/render-svelte-screenshot.js?raw
36
+ 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";
37
+
38
+ //#endregion
39
+ //#region src/utils.ts
40
+ async function getViteshotAssetsDir() {
41
+ return import.meta.resolve("viteshot");
42
+ }
43
+
44
+ //#endregion
45
+ //#region src/core/get-locales.ts
46
+ async function getLocales(localesDir) {
47
+ const jsonFilenames = (await readdir(localesDir, {})).filter((file) => file.endsWith(".json"));
48
+ return await Promise.all(jsonFilenames.map(async (file) => {
49
+ const ext = extname(file);
50
+ const text = await readFile(join(localesDir, file), "utf-8");
51
+ const messages = JSON.parse(text);
52
+ return {
53
+ id: file,
54
+ language: file.slice(0, -ext.length),
55
+ messages
56
+ };
57
+ }));
58
+ }
59
+
60
+ //#endregion
61
+ //#region src/core/get-screenshots.ts
62
+ const FILENAME_REGEX = /^(?<name>.*?)@(?<size>(?<width>[0-9]+)x(?<height>[0-9]+)).(?<ext>.*)$/;
63
+ async function getScreenshots(designsDir) {
64
+ return (await readdir(designsDir, {
65
+ recursive: true,
66
+ withFileTypes: true
67
+ })).filter((file) => file.isFile()).map((file) => {
68
+ const match = FILENAME_REGEX.exec(file.name);
69
+ if (!match) return;
70
+ const path = join(file.parentPath, file.name);
71
+ const name = match.groups.name;
72
+ const size = match.groups.size;
73
+ const width = Number(match.groups.width);
74
+ const height = Number(match.groups.height);
75
+ const ext = match.groups.ext;
76
+ return {
77
+ id: relative(designsDir, path),
78
+ path,
79
+ ext,
80
+ name,
81
+ size,
82
+ width,
83
+ height
84
+ };
85
+ }).filter((file) => file != null).toSorted((a, b) => natsort(a.id, b.id));
86
+ }
87
+ async function logInvalidDesignFiles(designsDir) {
88
+ const invalid = (await readdir(designsDir, {
89
+ recursive: true,
90
+ withFileTypes: true
91
+ })).filter((file) => file.isFile()).map((file) => FILENAME_REGEX.exec(file.name) ? void 0 : file).filter((file) => file != null).map((file) => file.name);
92
+ if (invalid.length > 0) console.warn(`${styleText(["bold", "yellow"], "Invalid design file names:")}\n - ${invalid.join("\n - ")}`);
93
+ }
94
+
95
+ //#endregion
96
+ //#region src/core/resolver-plugin.ts
97
+ function resolveTemplate(options) {
98
+ return (server) => () => server.middlewares.use(async (req, res, next) => {
99
+ if (!req.originalUrl) return;
100
+ const url = new URL(req.originalUrl, "http://localhost");
101
+ if (!options.match(url)) return next();
102
+ const text = options.vars == null ? options.template : applyTemplateVars(options.template, options.vars(url));
103
+ return res.end(options.transform ? await server.transformIndexHtml(req.originalUrl, text) : text);
104
+ });
105
+ }
106
+ const VIRTUAL_SCREENSHOTS_FILTER = { id: [/viteshot-virtual\/screenshots/] };
107
+ const VIRTUAL_LOCALES_FILTER = { id: [/viteshot-virtual\/locales/] };
108
+ function applyTemplateVars(template, vars) {
109
+ return Object.entries(vars).reduce((template, [key, value]) => template.replaceAll(`{{${key}}}`, value), template);
110
+ }
111
+ const RENDER_SCREENSHOT_JS_TEMPLATES = {
112
+ ".html": render_html_screenshot_default,
113
+ ".vue": render_vue_screenshot_default,
114
+ ".tsx": render_react_screenshot_default,
115
+ ".jsx": render_react_screenshot_default,
116
+ ".svelte": render_svelte_screenshot_default
117
+ };
118
+ const resolverPlugin = (config) => [
119
+ {
120
+ name: "viteshot:resolve-favicon",
121
+ configureServer: resolveTemplate({
122
+ match: (url) => url.pathname === "/favicon.svg",
123
+ template: favicon_default
124
+ })
125
+ },
126
+ {
127
+ name: "viteshot:resolve-dashboard-html",
128
+ configureServer: resolveTemplate({
129
+ match: (url) => url.pathname === "/",
130
+ template: dashboard_default,
131
+ transform: true
132
+ })
133
+ },
134
+ {
135
+ name: "viteshot:resolve-screenshot-html",
136
+ configureServer: resolveTemplate({
137
+ match: (url) => /\/screenshot\/.*?\/.*?/.test(url.pathname),
138
+ template: screenshot_default,
139
+ transform: true,
140
+ vars: (url) => {
141
+ const [localeId, screenshotId] = url.pathname.slice(12, -5).split("/");
142
+ const links = config.css.map((file) => `<link rel="stylesheet" href="/@fs${join(config.root, file)}" />`);
143
+ return {
144
+ "screenshot.id": screenshotId,
145
+ "locale.id": localeId,
146
+ css: links.join("")
147
+ };
148
+ }
149
+ })
150
+ },
151
+ {
152
+ name: "viteshot:resolve-assets",
153
+ resolveId: {
154
+ filter: { id: [/\/viteshot-assets\//] },
155
+ handler: async (id) => {
156
+ return join(await getViteshotAssetsDir(), id.slice(17));
157
+ }
158
+ }
159
+ },
160
+ {
161
+ name: "viteshot:resolve-virtual:screenshots",
162
+ resolveId: {
163
+ filter: VIRTUAL_SCREENSHOTS_FILTER,
164
+ handler: (id) => id
165
+ },
166
+ load: {
167
+ filter: VIRTUAL_SCREENSHOTS_FILTER,
168
+ handler: async () => {
169
+ const screenshots = await getScreenshots(config.designsDir);
170
+ return `export default ${JSON.stringify(screenshots)}`;
171
+ }
172
+ }
173
+ },
174
+ {
175
+ name: "viteshot:resolve-virtual:locales",
176
+ resolveId: {
177
+ filter: VIRTUAL_LOCALES_FILTER,
178
+ handler: (id) => id
179
+ },
180
+ load: {
181
+ filter: VIRTUAL_LOCALES_FILTER,
182
+ handler: async () => {
183
+ const locales = await getLocales(config.localesDir);
184
+ return `export default ${JSON.stringify(locales)}`;
185
+ }
186
+ }
187
+ },
188
+ {
189
+ name: "viteshot:resolve-virtual:render-screenshot",
190
+ resolveId: {
191
+ filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
192
+ handler: (id) => id
193
+ },
194
+ load: {
195
+ filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
196
+ handler: (id) => {
197
+ const screenshotId = decodeURIComponent(id.slice(36, -3));
198
+ if (!screenshotId) throw Error(`Required query param "id" not provided for ${id}`);
199
+ const ext = extname(screenshotId);
200
+ const path = join(config.designsDir, screenshotId);
201
+ const template = RENDER_SCREENSHOT_JS_TEMPLATES[ext];
202
+ if (!template) throw Error(`Unsupported screenshot file type (${ext}). Must be one of ${Object.keys(RENDER_SCREENSHOT_JS_TEMPLATES).join(", ")}`);
203
+ return applyTemplateVars(template, { path });
204
+ }
205
+ }
206
+ },
207
+ {
208
+ name: "viteshot:resolve-virtual:messages",
209
+ resolveId: {
210
+ filter: { id: [/\/viteshot-virtual\/messages/] },
211
+ handler: (id) => {
212
+ const localeId = id.slice(27);
213
+ if (!localeId) throw Error(`Required query param "id" not provided for ${id}`);
214
+ return join(config.localesDir, localeId);
215
+ }
216
+ }
217
+ }
218
+ ];
219
+
220
+ //#endregion
221
+ //#region src/core/config.ts
222
+ function defineConfig(config) {
223
+ return config;
224
+ }
225
+ async function importConfig(root) {
226
+ const configFileUrl = pathToFileURL(join(root, "viteshot.config.ts")).href;
227
+ try {
228
+ return (await import(configFileUrl)).default ?? {};
229
+ } catch (err) {
230
+ if (err?.message?.includes?.("Cannot find module")) return {};
231
+ throw err;
232
+ }
233
+ }
234
+ async function resolveConfig(dir = process.cwd()) {
235
+ const root = resolve(dir);
236
+ const { screenshots: _screenshots, ...vite } = await importConfig(root);
237
+ const designsDir = _screenshots?.designsDir ? resolve(root, _screenshots.designsDir) : join(root, "designs");
238
+ const exportsDir = _screenshots?.exportsDir ? resolve(root, _screenshots.exportsDir) : join(root, "exports");
239
+ const config = {
240
+ root,
241
+ localesDir: _screenshots?.localesDir ? resolve(root, _screenshots.localesDir) : join(root, "locales"),
242
+ designsDir,
243
+ exportsDir,
244
+ renderConcurrency: _screenshots?.renderConcurrency || 4,
245
+ puppeteer: _screenshots?.puppeteer,
246
+ css: _screenshots?.css ?? [],
247
+ vite: {
248
+ ...vite,
249
+ configFile: false
250
+ }
251
+ };
252
+ config.vite.plugins ??= [];
253
+ config.vite.plugins.push(resolverPlugin(config));
254
+ config.vite.resolve ??= {};
255
+ config.vite.resolve.external ??= [];
256
+ const external = config.vite.resolve.external;
257
+ 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}}");
258
+ return config;
259
+ }
260
+
261
+ //#endregion
262
+ //#region src/core/create-server.ts
263
+ async function createServer(dir) {
264
+ const config = await resolveConfig(dir);
265
+ await logInvalidDesignFiles(config.designsDir);
266
+ return createServer$1(config.vite);
267
+ }
268
+
269
+ //#endregion
270
+ //#region src/core/export-screenshots.ts
271
+ async function exportScreenshots(dir) {
272
+ const config = await resolveConfig(dir);
273
+ await logInvalidDesignFiles(config.designsDir);
274
+ const cwd = process.cwd();
275
+ const screenshots = await getScreenshots(config.designsDir);
276
+ const locales = await getLocales(config.localesDir);
277
+ console.log(`\n\x1b[1mExporting ${screenshots.length * (locales.length || 1)} screenshots...\x1b[0m\n`);
278
+ let server;
279
+ let browser;
280
+ try {
281
+ await rm(config.exportsDir, {
282
+ recursive: true,
283
+ force: true
284
+ });
285
+ await mkdir(config.exportsDir, { recursive: true });
286
+ server = await createServer$1(config.vite);
287
+ server.listen();
288
+ const { port } = server.config.server;
289
+ browser = await puppeteer.launch({
290
+ executablePath: process.env.VITESHOT_CHROME_PATH,
291
+ ...config.puppeteer?.launchOptions
292
+ });
293
+ const screenshotMutex = new Mutex();
294
+ await pMap(screenshots.flatMap((screenshot) => locales.map((locale) => ({
295
+ screenshot,
296
+ locale
297
+ }))), async ({ screenshot, locale }) => {
298
+ const outputId = (locale ? `${locale.language}/` : "") + screenshot.name + ".webp";
299
+ const outputPath = join(config.exportsDir, outputId);
300
+ await mkdir(dirname(outputPath), { recursive: true });
301
+ const page = await browser.newPage({
302
+ background: true,
303
+ ...config.puppeteer?.newPageOptions
304
+ });
305
+ await page.goto(`http://localhost:${port}/screenshot/${locale?.id ?? "null"}/${screenshot.id}.html`, {
306
+ waitUntil: "networkidle0",
307
+ timeout: 5e3
308
+ });
309
+ await screenshotMutex.runExclusive(async () => {
310
+ await page.bringToFront();
311
+ await page.screenshot({
312
+ captureBeyondViewport: true,
313
+ type: "webp",
314
+ quality: 100,
315
+ ...config.puppeteer?.screenshotOptions,
316
+ clip: {
317
+ x: 0,
318
+ y: 0,
319
+ width: screenshot.width,
320
+ height: screenshot.height
321
+ },
322
+ path: outputPath
323
+ });
324
+ });
325
+ console.log(` ✅ \x1b[2m./${relative(cwd, config.exportsDir)}/\x1b[0m\x1b[36m${outputId}\x1b[0m`);
326
+ await page.close({ runBeforeUnload: false });
327
+ }, {
328
+ concurrency: config.renderConcurrency,
329
+ stopOnError: true
330
+ });
331
+ } catch (err) {
332
+ 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.`);
333
+ else throw err;
334
+ } finally {
335
+ await browser?.close().catch(() => {});
336
+ await server?.close().catch(() => {});
337
+ console.log("");
338
+ }
339
+ }
340
+
341
+ //#endregion
342
+ export { createServer, defineConfig, exportScreenshots, getScreenshots, logInvalidDesignFiles, resolveConfig };
package/package.json CHANGED
@@ -1,14 +1,16 @@
1
1
  {
2
2
  "name": "@aklinker1/viteshot",
3
3
  "description": "Generate store screenshots and promo images with code, powered by Vite",
4
- "version": "0.2.0",
4
+ "version": "0.4.1",
5
5
  "type": "module",
6
6
  "packageManager": "bun@1.3.9",
7
7
  "scripts": {
8
- "viteshot": "bun run -d process.env.DEV:\"true\" src/cli.ts",
8
+ "viteshot": "bun run -d process.env.DEV:\"true\" -d process.env.COMMIT_HASH:\"'DEV'\" src/cli.ts",
9
9
  "dev": "bun viteshot dev example",
10
- "generate": "bun viteshot generate example",
11
- "build": "tsdown src/index.ts src/cli.ts"
10
+ "export": "bun viteshot export example",
11
+ "build": "tsdown",
12
+ "build:hash": "git rev-parse --short=7 HEAD",
13
+ "prepack": "bun run build"
12
14
  },
13
15
  "dependencies": {
14
16
  "@vitejs/plugin-react": "^6.0.1",
@@ -29,6 +31,7 @@
29
31
  "devDependencies": {
30
32
  "@aklinker1/check": "^2.2.0",
31
33
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
34
+ "@tailwindcss/vite": "^4.2.2",
32
35
  "@types/bun": "latest",
33
36
  "@types/natural-compare-lite": "^1.4.2",
34
37
  "@types/react": "^19.2.14",
@@ -38,6 +41,7 @@
38
41
  "prettier-plugin-jsdoc": "^1.8.0",
39
42
  "publint": "^0.3.17",
40
43
  "puppeteer": "^24.37.5",
44
+ "tailwindcss": "^4.2.2",
41
45
  "tsdown": "^0.20.3",
42
46
  "typescript": "^5"
43
47
  },
@@ -74,6 +78,7 @@
74
78
  "images",
75
79
  "screenshot",
76
80
  "generate",
81
+ "export",
77
82
  "store"
78
83
  ]
79
84
  }
package/dist/cli.d.mts DELETED
@@ -1 +0,0 @@
1
- export { };
package/dist/cli.mjs DELETED
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as createServer, t as generateScreenshots } from "./generate-screenshots-W20DkMBq.mjs";
3
-
4
- //#region src/cli.ts
5
- const [command, ..._args] = process.argv.slice(2);
6
- switch (command) {
7
- case "dev":
8
- const server = await createServer("example");
9
- await server.listen();
10
- server.printUrls();
11
- break;
12
- case "generate":
13
- await generateScreenshots("example");
14
- process.exit(0);
15
- default: break;
16
- }
17
-
18
- //#endregion
19
- export { };
@@ -1,325 +0,0 @@
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 :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
-
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 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
-
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]) =>\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";
36
-
37
- //#endregion
38
- //#region src/utils.ts
39
- async function getViteshotAssetsDir() {
40
- if (process.env.DEV) return resolve("assets");
41
- else return import.meta.resolve("viteshot");
42
- }
43
-
44
- //#endregion
45
- //#region src/core/get-locales.ts
46
- async function getLocales(localesDir) {
47
- const jsonFilenames = (await readdir(localesDir, {})).filter((file) => file.endsWith(".json"));
48
- return await Promise.all(jsonFilenames.map(async (file) => {
49
- const ext = extname(file);
50
- const text = await readFile(join(localesDir, file), "utf-8");
51
- const messages = JSON.parse(text);
52
- return {
53
- id: file,
54
- language: file.slice(0, -ext.length),
55
- messages
56
- };
57
- }));
58
- }
59
-
60
- //#endregion
61
- //#region src/core/get-screenshots.ts
62
- async function getScreenshots(designsDir) {
63
- return (await readdir(designsDir, {
64
- recursive: true,
65
- withFileTypes: true
66
- })).filter((file) => file.isFile()).map((file) => {
67
- const path = join(file.parentPath, file.name);
68
- const ext = extname(file.name);
69
- const filenameNoExt = file.name.slice(0, -ext.length);
70
- const [name, size] = filenameNoExt.split("@", 2);
71
- const [width, height] = size ? size.split("x").map((value) => Number(value)) : [];
72
- return {
73
- id: relative(designsDir, path),
74
- path,
75
- ext,
76
- filenameNoExt,
77
- name,
78
- size,
79
- width,
80
- height
81
- };
82
- }).toSorted((a, b) => natsort(a.id, b.id));
83
- }
84
-
85
- //#endregion
86
- //#region src/core/resolver-plugin.ts
87
- function resolveTemplate(options) {
88
- return (server) => () => server.middlewares.use(async (req, res, next) => {
89
- if (!req.originalUrl) return;
90
- const url = new URL(req.originalUrl, "http://localhost");
91
- if (!options.match(url)) return next();
92
- const text = options.vars == null ? options.template : applyTemplateVars(options.template, options.vars(url));
93
- return res.end(options.transform ? await server.transformIndexHtml(req.originalUrl, text) : text);
94
- });
95
- }
96
- const VIRTUAL_SCREENSHOTS_FILTER = { id: [/viteshot-virtual\/screenshots/] };
97
- const VIRTUAL_LOCALES_FILTER = { id: [/viteshot-virtual\/locales/] };
98
- function applyTemplateVars(template, vars) {
99
- return Object.entries(vars).reduce((template, [key, value]) => template.replaceAll(`{{${key}}}`, value), template);
100
- }
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
- };
108
- const resolverPlugin = (config) => [
109
- {
110
- name: "viteshot:resolve-favicon",
111
- configureServer: resolveTemplate({
112
- match: (url) => url.pathname === "/favicon.svg",
113
- template: favicon_default
114
- })
115
- },
116
- {
117
- name: "viteshot:resolve-dashboard-html",
118
- configureServer: resolveTemplate({
119
- match: (url) => url.pathname === "/",
120
- template: dashboard_default,
121
- transform: true
122
- })
123
- },
124
- {
125
- name: "viteshot:resolve-screenshot-html",
126
- configureServer: resolveTemplate({
127
- match: (url) => /\/screenshot\/.*?\/.*?/.test(url.pathname),
128
- template: screenshot_default,
129
- transform: true,
130
- vars: (url) => {
131
- const [localeId, screenshotId] = url.pathname.slice(12).split("/");
132
- return {
133
- "screenshot.id": screenshotId,
134
- "locale.id": localeId
135
- };
136
- }
137
- })
138
- },
139
- {
140
- name: "viteshot:resolve-assets",
141
- resolveId: {
142
- filter: { id: [/\/viteshot-assets\//] },
143
- handler: async (id) => {
144
- return join(await getViteshotAssetsDir(), id.slice(17));
145
- }
146
- }
147
- },
148
- {
149
- name: "viteshot:resolve-virtual:screenshots",
150
- resolveId: {
151
- filter: VIRTUAL_SCREENSHOTS_FILTER,
152
- handler: (id) => id
153
- },
154
- load: {
155
- filter: VIRTUAL_SCREENSHOTS_FILTER,
156
- handler: async () => {
157
- const screenshots = await getScreenshots(config.designsDir);
158
- return `export default ${JSON.stringify(screenshots)}`;
159
- }
160
- }
161
- },
162
- {
163
- name: "viteshot:resolve-virtual:locales",
164
- resolveId: {
165
- filter: VIRTUAL_LOCALES_FILTER,
166
- handler: (id) => id
167
- },
168
- load: {
169
- filter: VIRTUAL_LOCALES_FILTER,
170
- handler: async () => {
171
- const locales = await getLocales(config.localesDir);
172
- return `export default ${JSON.stringify(locales)}`;
173
- }
174
- }
175
- },
176
- {
177
- name: "viteshot:resolve-virtual:render-screenshot",
178
- resolveId: {
179
- filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
180
- handler: (id) => id
181
- },
182
- load: {
183
- filter: { id: [/^\/viteshot-virtual\/render-screenshot/] },
184
- handler: (id) => {
185
- const screenshotId = decodeURIComponent(id.slice(36, -3));
186
- if (!screenshotId) throw Error(`Required query param "id" not provided for ${id}`);
187
- const ext = extname(screenshotId);
188
- const path = join(config.designsDir, screenshotId);
189
- const template = RENDER_SCREENSHOT_JS_TEMPLATES[ext];
190
- if (!template) throw Error(`Unsupported screenshot file type (${ext}). Must be one of ${Object.keys(RENDER_SCREENSHOT_JS_TEMPLATES).join(", ")}`);
191
- return applyTemplateVars(template, { path });
192
- }
193
- }
194
- },
195
- {
196
- name: "viteshot:resolve-virtual:messages",
197
- resolveId: {
198
- filter: { id: [/\/viteshot-virtual\/messages/] },
199
- handler: (id) => {
200
- const localeId = id.slice(27);
201
- if (!localeId) throw Error(`Required query param "id" not provided for ${id}`);
202
- return join(config.localesDir, localeId);
203
- }
204
- }
205
- }
206
- ];
207
-
208
- //#endregion
209
- //#region src/core/config.ts
210
- function defineConfig(config) {
211
- return config;
212
- }
213
- async function importConfig(root) {
214
- const configFileUrl = pathToFileURL(join(root, "viteshot.config.ts")).href;
215
- try {
216
- return (await import(configFileUrl)).default ?? {};
217
- } catch (err) {
218
- if (err?.message?.includes?.("Cannot find module")) return {};
219
- throw err;
220
- }
221
- }
222
- async function resolveConfig(dir = process.cwd()) {
223
- const root = resolve(dir);
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");
227
- const config = {
228
- root,
229
- localesDir: _screenshots?.localesDir ? resolve(root, _screenshots.localesDir) : join(root, "locales"),
230
- designsDir,
231
- screenshotsDir,
232
- renderConcurrency: _screenshots?.renderConcurrency || 4,
233
- vite: {
234
- ...vite,
235
- configFile: false
236
- }
237
- };
238
- config.vite.plugins ??= [];
239
- config.vite.plugins.push(resolverPlugin(config));
240
- config.vite.resolve ??= {};
241
- config.vite.resolve.external ??= [];
242
- const external = config.vite.resolve.external;
243
- 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}}");
244
- return config;
245
- }
246
-
247
- //#endregion
248
- //#region src/core/create-server.ts
249
- async function createServer$1(dir) {
250
- return createServer((await resolveConfig(dir)).vite);
251
- }
252
-
253
- //#endregion
254
- //#region src/core/generate-screenshots.ts
255
- async function generateScreenshots(dir) {
256
- const config = await resolveConfig(dir);
257
- const cwd = process.cwd();
258
- const screenshots = await getScreenshots(config.designsDir);
259
- const locales = await getLocales(config.localesDir);
260
- console.log(`\n\x1b[1mGenerating ${screenshots.length * (locales.length || 1)} screenshots...\x1b[0m\n`);
261
- let server;
262
- let browser;
263
- try {
264
- await rm(config.screenshotsDir, {
265
- recursive: true,
266
- force: true
267
- });
268
- await mkdir(config.screenshotsDir, { recursive: true });
269
- server = await createServer(config.vite);
270
- server.listen();
271
- const { port } = server.config.server;
272
- browser = await puppeteer.launch({
273
- executablePath: process.env.VITESHOT_CHROME_PATH,
274
- ...config.puppeteer?.launchOptions
275
- });
276
- const screenshotMutex = new Mutex();
277
- await pMap(screenshots.flatMap((screenshot) => locales.map((locale) => ({
278
- screenshot,
279
- locale
280
- }))), async ({ screenshot, locale }) => {
281
- const outputId = (locale ? `${locale.language}/` : "") + screenshot.name + ".webp";
282
- const outputPath = join(config.screenshotsDir, outputId);
283
- await mkdir(dirname(outputPath), { recursive: true });
284
- const page = await browser.newPage({
285
- background: true,
286
- ...config.puppeteer?.newPageOptions
287
- });
288
- await page.goto(`http://localhost:${port}/screenshot/${locale?.id ?? "null"}/${screenshot.id}`, {
289
- waitUntil: "networkidle0",
290
- timeout: 5e3
291
- });
292
- await screenshotMutex.runExclusive(async () => {
293
- await page.bringToFront();
294
- await page.screenshot({
295
- captureBeyondViewport: true,
296
- type: "webp",
297
- quality: 100,
298
- ...config.puppeteer?.screenshotOptions,
299
- clip: {
300
- x: 0,
301
- y: 0,
302
- width: screenshot.width,
303
- height: screenshot.height
304
- },
305
- path: outputPath
306
- });
307
- });
308
- console.log(` ✅ \x1b[2m./${relative(cwd, config.screenshotsDir)}/\x1b[0m\x1b[36m${outputId}\x1b[0m`);
309
- await page.close({ runBeforeUnload: false });
310
- }, {
311
- concurrency: config.renderConcurrency,
312
- stopOnError: true
313
- });
314
- } catch (err) {
315
- 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.`);
316
- else throw err;
317
- } finally {
318
- await browser?.close().catch(() => {});
319
- await server?.close().catch(() => {});
320
- console.log("");
321
- }
322
- }
323
-
324
- //#endregion
325
- export { getScreenshots as a, resolveConfig as i, createServer$1 as n, defineConfig as r, generateScreenshots as t };