@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 +66 -32
- package/assets/dashboard.ts +1 -1
- package/dist/index.d.mts +17 -11
- package/dist/index.mjs +341 -2
- package/package.json +9 -4
- package/dist/cli.d.mts +0 -1
- package/dist/cli.mjs +0 -19
- package/dist/generate-screenshots-W20DkMBq.mjs +0 -325
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="
|
|
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
|
|
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
|
|
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
|
|
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
|
-
"
|
|
37
|
-
"
|
|
36
|
+
"viteshot:dev": "viteshot dev store",
|
|
37
|
+
"viteshot:export": "viteshot export store",
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
Then
|
|
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.
|
|
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
|
-
<
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
116
|
-
3.
|
|
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.
|
|
112
|
+
return <p>{t.path.to.message}</p>;
|
|
126
113
|
}
|
|
127
114
|
```
|
|
128
115
|
|
|
129
116
|
## Styling
|
|
130
117
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
package/assets/dashboard.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
+
exportsDir?: string;
|
|
15
15
|
/**
|
|
16
|
-
* How many screenshots can be
|
|
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
|
-
|
|
36
|
+
exportsDir: string;
|
|
32
37
|
renderConcurrency: number;
|
|
33
|
-
puppeteer
|
|
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/
|
|
43
|
-
declare function
|
|
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
|
|
53
|
-
width: number
|
|
54
|
-
height: number
|
|
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,
|
|
64
|
+
export { InlineConfig, PuppeteerOptions, ResolvedConfig, Screenshot, UserConfig, createServer, defineConfig, exportScreenshots, getScreenshots, logInvalidDesignFiles, resolveConfig };
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,342 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
11
|
-
"build": "tsdown
|
|
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 };
|