@open-assets/open-assets 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +141 -25
- package/bin/open-assets.mjs +2 -3
- package/lib/init.mjs +1 -1
- package/lib/lockfile.mjs +6 -3
- package/lib/manifest.mjs +5 -17
- package/lib/render.mjs +301 -176
- package/lib/renderer.mjs +214 -88
- package/lib/server.mjs +20 -12
- package/package.json +3 -3
- package/public/img/banner/1280.png +0 -0
- package/public/img/examples/tax-days-icon.png +0 -0
- package/public/img/examples/tax-days.png +0 -0
- package/public/img/examples/thoughtful-icon.png +0 -0
- package/public/img/examples/thoughtful.png +0 -0
- package/skills/open-assets/SKILL.md +46 -36
- package/public/img/banner/1280/banner.png +0 -0
package/lib/renderer.mjs
CHANGED
|
@@ -2,128 +2,254 @@ import { launch } from "puppeteer";
|
|
|
2
2
|
import { resolve } from "path";
|
|
3
3
|
import { writeFileSync, mkdirSync, readdirSync, unlinkSync } from "fs";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
// The browser is launched once per process and shared. Storing the launch
|
|
6
|
+
// promise (not the instance) makes concurrent callers await the same launch
|
|
7
|
+
// instead of racing to spawn duplicate Chromium processes.
|
|
8
|
+
let browserPromise = null;
|
|
9
|
+
|
|
10
|
+
// Pages are pooled and reused across renders: page.goto fully replaces the
|
|
11
|
+
// document, and newPage()/close() cost 100-300ms per image in headless mode.
|
|
12
|
+
const pagePool = [];
|
|
6
13
|
|
|
7
14
|
async function getBrowser() {
|
|
8
|
-
|
|
9
|
-
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (!browserPromise) {
|
|
17
|
+
browserPromise = launch({
|
|
18
|
+
headless: true,
|
|
19
|
+
// Pages render concurrently; without these flags Chromium throttles
|
|
20
|
+
// timers and frames in non-active tabs, stalling background renders.
|
|
21
|
+
args: [
|
|
22
|
+
"--disable-background-timer-throttling",
|
|
23
|
+
"--disable-backgrounding-occluded-windows",
|
|
24
|
+
"--disable-renderer-backgrounding",
|
|
25
|
+
],
|
|
26
|
+
}).catch((err) => {
|
|
27
|
+
browserPromise = null;
|
|
28
|
+
throw err;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
const current = browserPromise;
|
|
32
|
+
const browser = await current;
|
|
33
|
+
if (browser.connected) return browser;
|
|
34
|
+
// Browser died; clear the cached promise (unless someone already
|
|
35
|
+
// replaced it) and relaunch.
|
|
36
|
+
if (browserPromise === current) browserPromise = null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Pre-launch the shared browser so the first render doesn't pay the ~2s
|
|
42
|
+
* launch cost interactively.
|
|
43
|
+
*/
|
|
44
|
+
export function warmupBrowser() {
|
|
45
|
+
return getBrowser();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function acquirePage() {
|
|
49
|
+
const browser = await getBrowser();
|
|
50
|
+
while (pagePool.length > 0) {
|
|
51
|
+
const page = pagePool.pop();
|
|
52
|
+
// Drop pages tied to a closed or replaced browser instance
|
|
53
|
+
if (!page.isClosed() && page.browser() === browser) return page;
|
|
10
54
|
}
|
|
11
|
-
return browser;
|
|
55
|
+
return browser.newPage();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function releasePage(page) {
|
|
59
|
+
if (page && !page.isClosed()) pagePool.push(page);
|
|
12
60
|
}
|
|
13
61
|
|
|
14
62
|
export async function closeBrowser() {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
63
|
+
pagePool.length = 0;
|
|
64
|
+
if (browserPromise) {
|
|
65
|
+
const pending = browserPromise;
|
|
66
|
+
browserPromise = null;
|
|
67
|
+
try {
|
|
68
|
+
const browser = await pending;
|
|
69
|
+
await browser.close();
|
|
70
|
+
} catch {
|
|
71
|
+
// Launch already failed; nothing to close.
|
|
72
|
+
}
|
|
18
73
|
}
|
|
19
74
|
}
|
|
20
75
|
|
|
21
76
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
77
|
+
* Deterministic readiness barrier, replacing fixed sleeps: yields a macrotask
|
|
78
|
+
* (so MutationObserver-driven DOM updates like locale image swaps have run),
|
|
79
|
+
* forces a layout (so font loads triggered by first layout have started),
|
|
80
|
+
* then waits for fonts and image decodes. No frame waits here: rAF can stall
|
|
81
|
+
* indefinitely in a non-active tab, and the screenshot capture itself forces
|
|
82
|
+
* a fresh frame that flushes pending style and layout.
|
|
83
|
+
*/
|
|
84
|
+
async function settle(page) {
|
|
85
|
+
await page.evaluate(async () => {
|
|
86
|
+
await new Promise((r) => setTimeout(r, 0));
|
|
87
|
+
document.documentElement.getBoundingClientRect();
|
|
88
|
+
await document.fonts.ready;
|
|
89
|
+
const images = document.images ? Array.from(document.images) : [];
|
|
90
|
+
await Promise.all(images.map((img) => img.decode().catch(() => {})));
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render an HTML file at multiple sizes from a single navigation.
|
|
96
|
+
* The page is loaded (and localized) once; each variant re-applies
|
|
97
|
+
* viewport + CSS zoom and takes its own screenshot.
|
|
98
|
+
*
|
|
24
99
|
* @param {string} projectDir
|
|
25
100
|
* @param {string} htmlFile
|
|
26
|
-
* @param {number} width
|
|
27
|
-
* @param {number} height
|
|
28
101
|
* @param {number} sourceW
|
|
29
102
|
* @param {number} sourceH
|
|
30
|
-
* @param {{ format?: "png"|"jpeg", quality?: number
|
|
103
|
+
* @param {{ width: number, height: number, format?: "png"|"jpeg", quality?: number }[]} variants
|
|
104
|
+
* @param {{ background?: string, localization?: { strings: object, locale: string, direction: string }, timeout?: number }} [options]
|
|
105
|
+
* @returns {Promise<Buffer[]>} Screenshot buffers in variant order
|
|
31
106
|
*/
|
|
32
|
-
export async function
|
|
107
|
+
export async function renderVariants(
|
|
33
108
|
projectDir,
|
|
34
109
|
htmlFile,
|
|
35
|
-
width,
|
|
36
|
-
height,
|
|
37
110
|
sourceW,
|
|
38
111
|
sourceH,
|
|
112
|
+
variants,
|
|
39
113
|
options = {}
|
|
40
114
|
) {
|
|
41
|
-
const
|
|
42
|
-
|
|
115
|
+
const page = await acquirePage();
|
|
116
|
+
try {
|
|
117
|
+
const first = variants[0];
|
|
118
|
+
await page.setViewport({
|
|
119
|
+
width: first.width,
|
|
120
|
+
height: first.height,
|
|
121
|
+
deviceScaleFactor: 1,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const filePath = resolve(projectDir, htmlFile);
|
|
125
|
+
await page.goto(`file://${filePath}`, {
|
|
126
|
+
waitUntil: "load",
|
|
127
|
+
timeout: options.timeout || 30000,
|
|
128
|
+
});
|
|
43
129
|
|
|
44
|
-
|
|
130
|
+
// Inject localized strings and set lang before rendering
|
|
131
|
+
if (options.localization) {
|
|
132
|
+
const { strings, locale, direction } = options.localization;
|
|
133
|
+
await page.evaluate((strings, locale, dir) => {
|
|
134
|
+
// Set lang on root for font selection; do NOT set dir on root
|
|
135
|
+
// because it flips the entire CSS layout (flexbox, margins, etc.)
|
|
136
|
+
// which breaks fixed-pixel marketing layouts.
|
|
137
|
+
document.documentElement.lang = locale;
|
|
45
138
|
|
|
46
|
-
|
|
139
|
+
const formatter = new Intl.NumberFormat(locale);
|
|
47
140
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
141
|
+
function processValue(value) {
|
|
142
|
+
return value.replace(/\{\{n:(\d+(?:\.\d+)?)\}\}/g, (_, num) => {
|
|
143
|
+
return formatter.format(parseFloat(num));
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const walker = document.createTreeWalker(
|
|
148
|
+
document.documentElement,
|
|
149
|
+
NodeFilter.SHOW_TEXT
|
|
150
|
+
);
|
|
151
|
+
while (walker.nextNode()) {
|
|
152
|
+
const node = walker.currentNode;
|
|
153
|
+
let text = node.textContent;
|
|
154
|
+
let changed = false;
|
|
155
|
+
text = text.replace(/\{\{([a-zA-Z_][\w.]*)\}\}/g, (match, key) => {
|
|
156
|
+
if (strings[key] !== undefined) {
|
|
157
|
+
changed = true;
|
|
158
|
+
return processValue(strings[key]);
|
|
159
|
+
}
|
|
160
|
+
return match;
|
|
161
|
+
});
|
|
162
|
+
if (changed) {
|
|
163
|
+
node.textContent = text;
|
|
164
|
+
// Apply text direction on the parent element so RTL text
|
|
165
|
+
// renders correctly without flipping the overall page layout.
|
|
166
|
+
if (dir === "rtl") {
|
|
167
|
+
node.parentElement.dir = dir;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}, strings, locale, direction);
|
|
172
|
+
}
|
|
51
173
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// which breaks fixed-pixel marketing layouts.
|
|
59
|
-
document.documentElement.lang = locale;
|
|
174
|
+
// Handle background: transparent by default for PNG, or use specified color
|
|
175
|
+
if (options.background) {
|
|
176
|
+
await page.evaluate((color) => {
|
|
177
|
+
document.documentElement.style.backgroundColor = color;
|
|
178
|
+
}, options.background);
|
|
179
|
+
}
|
|
60
180
|
|
|
61
|
-
|
|
181
|
+
await settle(page);
|
|
62
182
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
183
|
+
const buffers = [];
|
|
184
|
+
for (const variant of variants) {
|
|
185
|
+
const { width, height } = variant;
|
|
186
|
+
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
|
68
187
|
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
return match;
|
|
83
|
-
});
|
|
84
|
-
if (changed) {
|
|
85
|
-
node.textContent = text;
|
|
86
|
-
// Apply text direction on the parent element so RTL text
|
|
87
|
-
// renders correctly without flipping the overall page layout.
|
|
88
|
-
if (dir === "rtl") {
|
|
89
|
-
node.parentElement.dir = dir;
|
|
188
|
+
const scale = Math.max(width / sourceW, height / sourceH);
|
|
189
|
+
// Always assign zoom (clearing it at scale 1) so a previous variant's
|
|
190
|
+
// zoom can't leak into this one, and freeze animations at t=0 so the
|
|
191
|
+
// capture is deterministic. The screenshot's forced frame applies both.
|
|
192
|
+
await page.evaluate((zoom) => {
|
|
193
|
+
document.documentElement.style.zoom = zoom;
|
|
194
|
+
for (const anim of document.getAnimations()) {
|
|
195
|
+
try {
|
|
196
|
+
anim.pause();
|
|
197
|
+
anim.currentTime = 0;
|
|
198
|
+
} catch {
|
|
199
|
+
// Some animations (e.g. infinite transitions) can reject; skip.
|
|
90
200
|
}
|
|
91
201
|
}
|
|
92
|
-
}
|
|
93
|
-
}, strings, locale, direction);
|
|
94
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (scale !== 1) {
|
|
98
|
-
await page.evaluate((z) => {
|
|
99
|
-
document.documentElement.style.zoom = z;
|
|
100
|
-
}, scale);
|
|
101
|
-
await new Promise((r) => setTimeout(r, 200));
|
|
102
|
-
}
|
|
202
|
+
}, scale === 1 ? "" : String(scale));
|
|
103
203
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
204
|
+
const format = variant.format || "png";
|
|
205
|
+
const screenshotOpts = {
|
|
206
|
+
type: format,
|
|
207
|
+
clip: { x: 0, y: 0, width, height },
|
|
208
|
+
};
|
|
209
|
+
if (format === "png" && !options.background) {
|
|
210
|
+
screenshotOpts.omitBackground = true;
|
|
211
|
+
}
|
|
212
|
+
if (format === "jpeg" && variant.quality) {
|
|
213
|
+
screenshotOpts.quality = variant.quality;
|
|
214
|
+
}
|
|
215
|
+
buffers.push(await page.screenshot(screenshotOpts));
|
|
216
|
+
}
|
|
111
217
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
clip: { x: 0, y: 0, width, height },
|
|
116
|
-
};
|
|
117
|
-
if (format === "png" && !bg) {
|
|
118
|
-
screenshotOpts.omitBackground = true;
|
|
119
|
-
}
|
|
120
|
-
if (format === "jpeg" && options.quality) {
|
|
121
|
-
screenshotOpts.quality = options.quality;
|
|
218
|
+
return buffers;
|
|
219
|
+
} finally {
|
|
220
|
+
releasePage(page);
|
|
122
221
|
}
|
|
222
|
+
}
|
|
123
223
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
224
|
+
/**
|
|
225
|
+
* Render an HTML file to an image buffer at the given dimensions.
|
|
226
|
+
* Scales from sourceSize to target size via CSS zoom.
|
|
227
|
+
* Single-variant wrapper around renderVariants.
|
|
228
|
+
* @param {string} projectDir
|
|
229
|
+
* @param {string} htmlFile
|
|
230
|
+
* @param {number} width
|
|
231
|
+
* @param {number} height
|
|
232
|
+
* @param {number} sourceW
|
|
233
|
+
* @param {number} sourceH
|
|
234
|
+
* @param {{ format?: "png"|"jpeg", quality?: number, background?: string, localization?: { strings: object, locale: string, direction: string }, timeout?: number }} [options]
|
|
235
|
+
*/
|
|
236
|
+
export async function renderScreenshot(
|
|
237
|
+
projectDir,
|
|
238
|
+
htmlFile,
|
|
239
|
+
width,
|
|
240
|
+
height,
|
|
241
|
+
sourceW,
|
|
242
|
+
sourceH,
|
|
243
|
+
options = {}
|
|
244
|
+
) {
|
|
245
|
+
const [buffer] = await renderVariants(
|
|
246
|
+
projectDir,
|
|
247
|
+
htmlFile,
|
|
248
|
+
sourceW,
|
|
249
|
+
sourceH,
|
|
250
|
+
[{ width, height, format: options.format, quality: options.quality }],
|
|
251
|
+
options
|
|
252
|
+
);
|
|
127
253
|
return buffer;
|
|
128
254
|
}
|
|
129
255
|
|
package/lib/server.mjs
CHANGED
|
@@ -62,7 +62,7 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
62
62
|
if (!renderer) {
|
|
63
63
|
renderer = await import("./renderer.mjs");
|
|
64
64
|
}
|
|
65
|
-
const { renderScreenshot, runXcodeOutput, closeBrowser } = renderer;
|
|
65
|
+
const { renderScreenshot, runXcodeOutput, closeBrowser, warmupBrowser } = renderer;
|
|
66
66
|
|
|
67
67
|
const port = options.port || 3200;
|
|
68
68
|
const host = options.host || "localhost";
|
|
@@ -159,7 +159,11 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
159
159
|
const sw = sourceW || width;
|
|
160
160
|
const sh = sourceH || height;
|
|
161
161
|
log(`Rendering ${file} at ${width}x${height} (source ${sw}x${sh})${localization ? ` [${localization.locale}]` : ''}...`);
|
|
162
|
-
const buffer = await renderScreenshot(projectDir, file, width, height, sw, sh, {
|
|
162
|
+
const buffer = await renderScreenshot(projectDir, file, width, height, sw, sh, {
|
|
163
|
+
background,
|
|
164
|
+
localization,
|
|
165
|
+
timeout: options.renderTimeout,
|
|
166
|
+
});
|
|
163
167
|
res.writeHead(200, {
|
|
164
168
|
"Content-Type": "image/png",
|
|
165
169
|
"Content-Length": buffer.length,
|
|
@@ -315,27 +319,31 @@ export async function startServer(projectDir, options = {}, renderer) {
|
|
|
315
319
|
tryListen();
|
|
316
320
|
});
|
|
317
321
|
|
|
318
|
-
return { port: actualPort };
|
|
319
|
-
|
|
320
322
|
function cleanup() {
|
|
321
323
|
for (const w of watchers) w.close();
|
|
322
324
|
for (const client of sseClients) client.end();
|
|
323
325
|
sseClients.clear();
|
|
324
326
|
}
|
|
325
327
|
|
|
326
|
-
|
|
328
|
+
const shutdown = async () => {
|
|
327
329
|
cleanup();
|
|
328
330
|
await closeBrowser();
|
|
329
331
|
server.close();
|
|
330
332
|
process.exit(0);
|
|
331
|
-
}
|
|
333
|
+
};
|
|
332
334
|
|
|
333
|
-
process.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
process.
|
|
335
|
+
process.once("SIGINT", shutdown);
|
|
336
|
+
process.once("SIGTERM", shutdown);
|
|
337
|
+
server.on("close", () => {
|
|
338
|
+
process.removeListener("SIGINT", shutdown);
|
|
339
|
+
process.removeListener("SIGTERM", shutdown);
|
|
338
340
|
});
|
|
339
341
|
|
|
340
|
-
|
|
342
|
+
// Pre-launch the shared browser in the background so the first interactive
|
|
343
|
+
// render doesn't pay the ~2s launch cost. Failures surface on first render.
|
|
344
|
+
if (typeof warmupBrowser === "function") {
|
|
345
|
+
Promise.resolve(warmupBrowser()).catch(() => {});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return { port: actualPort, server };
|
|
341
349
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-assets/open-assets",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Screenshots, icons, and marketing assets, made a breeze in your git repo. Design in HTML/CSS/SVG, render at every size, export anywhere. Like Storybook for marketing assets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"open-assets": "bin/open-assets.mjs"
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"scripts": {
|
|
42
42
|
"assets": "node bin/open-assets.mjs render --force -o ./public/img",
|
|
43
|
-
"test": "node --experimental-vm-modules node_modules
|
|
43
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
44
44
|
"release": "./scripts/release.sh"
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://openassets.dev",
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -17,11 +17,12 @@ open-assets dev
|
|
|
17
17
|
# Or with Tailwind
|
|
18
18
|
concurrently "npx @tailwindcss/cli -i assets/styles.css -o dist/styles.css --watch" "open-assets dev"
|
|
19
19
|
|
|
20
|
-
# Headless render
|
|
20
|
+
# Headless render (incremental: unchanged templates are skipped via assets.lock)
|
|
21
|
+
open-assets render # everything, at every export size
|
|
22
|
+
open-assets render --template log-out # one template only
|
|
23
|
+
open-assets render --collection ui-icons # one collection
|
|
21
24
|
open-assets render --collection screenshots --size iphone-6.9
|
|
22
|
-
open-assets render --
|
|
23
|
-
open-assets render --force
|
|
24
|
-
open-assets render --template 01-hero --force
|
|
25
|
+
open-assets render --force # re-render everything, ignoring the cache
|
|
25
26
|
```
|
|
26
27
|
|
|
27
28
|
## Concepts
|
|
@@ -43,7 +44,7 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
43
44
|
"version": 1,
|
|
44
45
|
"name": "App Display Name",
|
|
45
46
|
"publicDir": "public",
|
|
46
|
-
"command": "npx open-assets render
|
|
47
|
+
"command": "npx open-assets render",
|
|
47
48
|
"collections": [
|
|
48
49
|
{
|
|
49
50
|
"id": "unique-id",
|
|
@@ -76,7 +77,7 @@ The `assets.json` at the project root defines all asset collections. All collect
|
|
|
76
77
|
|
|
77
78
|
### Output Types
|
|
78
79
|
|
|
79
|
-
Export entries with a `type` field are post-render actions
|
|
80
|
+
Export entries with a `type` field are post-render actions that run after every render:
|
|
80
81
|
|
|
81
82
|
| Type | Config | Description |
|
|
82
83
|
|------|--------|-------------|
|
|
@@ -195,16 +196,33 @@ Export entries with a `type` field are post-render actions (run with `--force`):
|
|
|
195
196
|
| `--template <name>` | Render only the template with this name |
|
|
196
197
|
| `--size <name>` | Use a named export size from config |
|
|
197
198
|
| `--locale <code>` | Render only this locale (e.g. `en`, `ar`, `ja`) |
|
|
198
|
-
| `-f, --force` |
|
|
199
|
+
| `-f, --force` | Re-render everything, ignoring the incremental cache |
|
|
199
200
|
| `-o, --output <dir>` | Output directory (default: `./exports`) |
|
|
200
201
|
| `--json` | Output results as JSON |
|
|
201
202
|
| `-q, --quiet` | Suppress progress logs |
|
|
202
203
|
|
|
203
204
|
Flags compose naturally:
|
|
204
205
|
- `--collection screenshots --template 01-hero --size iphone-6.9` → single template, single size
|
|
205
|
-
- `--collection screenshots --template 01-hero
|
|
206
|
+
- `--collection screenshots --template 01-hero` → one template, all sizes
|
|
206
207
|
- `--collection screenshots --size iphone-6.9` → all templates, one size
|
|
207
208
|
|
|
209
|
+
#### Performance: prefer targeted renders when iterating
|
|
210
|
+
|
|
211
|
+
A plain `render` is incremental: unchanged templates are skipped via `assets.lock`, so it is usually fast. The renderer also loads each template once per locale and screenshots every export size from that single page load, with several templates rendering in parallel. Still, `--force` re-renders **every collection** in `assets.json` (icons, splash, badges, screenshots, etc.), so when you're iterating on a single asset, scope the render:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# Iterating on one icon (fastest)
|
|
215
|
+
open-assets render --template log-out
|
|
216
|
+
|
|
217
|
+
# Iterating on one collection
|
|
218
|
+
open-assets render --collection ui-icons
|
|
219
|
+
|
|
220
|
+
# Re-render everything, ignoring the cache
|
|
221
|
+
open-assets render --force
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Rule of thumb: plain `render` is the default; it only re-renders what changed. Add `--template <name>` or `--collection <id>` to scope further. Reserve `--force` for when the cache must be bypassed: changed shared CSS, changed images in `publicDir`, or a rare global rebuild.
|
|
225
|
+
|
|
208
226
|
### add commands
|
|
209
227
|
|
|
210
228
|
Interactive commands for managing the config without editing JSON by hand. All support `--config <path>` for custom config filenames.
|
|
@@ -330,6 +348,10 @@ Alternatively, add a new object to the `collections` array in `assets.json` manu
|
|
|
330
348
|
| Web | OG Image | 1200 | 630 |
|
|
331
349
|
| Mac App Store | Retina | 2880 | 1800 |
|
|
332
350
|
|
|
351
|
+
## Version Control
|
|
352
|
+
|
|
353
|
+
**Commit `assets.lock` and `exports/` to the repository. DO NOT gitignore them.** The lockfile enables incremental builds, and the exports are the final rendered assets that other parts of the project (Xcode asset catalogs, web builds, etc.) depend on. Only `dist/` (compiled Tailwind) should be gitignored.
|
|
354
|
+
|
|
333
355
|
## Incremental Builds (assets.lock)
|
|
334
356
|
|
|
335
357
|
The `render` command maintains a `assets.lock` file with SHA256 checksums of source files. On subsequent renders, unchanged assets are skipped automatically. Use `--force` to re-render everything.
|
|
@@ -371,41 +393,29 @@ Create a JSON file (iOS `.xcstrings`-inspired format) that maps string keys to p
|
|
|
371
393
|
|
|
372
394
|
### Wiring localizations to a collection
|
|
373
395
|
|
|
374
|
-
Add `localizations` (path to the JSON file) and optionally `locales` (filter to specific locales) to a collection in `assets.json
|
|
396
|
+
Add `localizations` (path to the JSON file) and optionally `locales` (filter to specific locales) to a collection in `assets.json`:
|
|
375
397
|
|
|
376
398
|
```json
|
|
377
399
|
{
|
|
378
|
-
"
|
|
379
|
-
"
|
|
380
|
-
"
|
|
381
|
-
"
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
"templates": [
|
|
389
|
-
{ "src": "assets/screenshots/01-hero.html", "name": "01-hero", "label": "Hero" }
|
|
390
|
-
],
|
|
391
|
-
"export": [
|
|
392
|
-
{ "name": "iphone-6.9", "label": "iPhone 6.9\"", "size": { "width": 1320, "height": 2868 } }
|
|
393
|
-
]
|
|
394
|
-
}
|
|
400
|
+
"id": "screenshots",
|
|
401
|
+
"label": "App Store Screenshots",
|
|
402
|
+
"sourceSize": { "width": 440, "height": 956 },
|
|
403
|
+
"localizations": "localizations.json",
|
|
404
|
+
"locales": ["en", "ar", "ja", "de"],
|
|
405
|
+
"templates": [
|
|
406
|
+
{ "src": "assets/screenshots/01-hero.html", "name": "01-hero", "label": "Hero" }
|
|
407
|
+
],
|
|
408
|
+
"export": [
|
|
409
|
+
{ "name": "iphone-6.9", "label": "iPhone 6.9\"", "size": { "width": 1320, "height": 2868 } }
|
|
395
410
|
]
|
|
396
411
|
}
|
|
397
412
|
```
|
|
398
413
|
|
|
399
414
|
| Field | Type | Description |
|
|
400
415
|
|-------|------|-------------|
|
|
401
|
-
| `defaultLocalization` | string | Root-level. Default locale for the dev server locale picker. Falls back to `sourceLanguage` then `"en"`. |
|
|
402
416
|
| `localizations` | string | Path to the localizations JSON file (relative to project root) |
|
|
403
417
|
| `locales` | string[] | Optional filter — render only these locales. Omit to render all locales in the file. |
|
|
404
418
|
|
|
405
|
-
### Dev server locale picker
|
|
406
|
-
|
|
407
|
-
When a collection has `localizations` configured, the dev server shows a locale picker dropdown in the toolbar. Selecting a locale injects the localized strings into all template iframes in real time. The selected locale is also persisted in the URL (`?locale=ar`) and used for exports — exported PNGs will include the localized text for the active locale.
|
|
408
|
-
|
|
409
419
|
### Using placeholders in templates
|
|
410
420
|
|
|
411
421
|
Templates use `{{key}}` mustache-style placeholders that get replaced with localized values before rendering:
|
|
@@ -458,13 +468,13 @@ With `outFile`, use the `{locale}` template variable:
|
|
|
458
468
|
|
|
459
469
|
```bash
|
|
460
470
|
# Render all locales for a collection
|
|
461
|
-
open-assets render --collection screenshots
|
|
471
|
+
open-assets render --collection screenshots
|
|
462
472
|
|
|
463
473
|
# Render only Arabic
|
|
464
|
-
open-assets render --collection screenshots --
|
|
474
|
+
open-assets render --collection screenshots --locale ar
|
|
465
475
|
|
|
466
476
|
# Render a single template in Japanese
|
|
467
|
-
open-assets render --collection screenshots --template 01-hero --locale ja
|
|
477
|
+
open-assets render --collection screenshots --template 01-hero --locale ja
|
|
468
478
|
```
|
|
469
479
|
|
|
470
480
|
### Locale fallback order
|
|
@@ -479,7 +489,7 @@ When resolving a string for a locale:
|
|
|
479
489
|
```
|
|
480
490
|
project/
|
|
481
491
|
assets.json
|
|
482
|
-
assets.lock # Auto-generated cache
|
|
492
|
+
assets.lock # Auto-generated cache — commit this
|
|
483
493
|
public/ # Shared assets (images, logos, icons, photos)
|
|
484
494
|
logo-round.png
|
|
485
495
|
social/
|
|
@@ -500,7 +510,7 @@ project/
|
|
|
500
510
|
default.html
|
|
501
511
|
dist/
|
|
502
512
|
styles.css # Compiled Tailwind
|
|
503
|
-
exports/ # Rendered output
|
|
513
|
+
exports/ # Rendered output — commit this
|
|
504
514
|
```
|
|
505
515
|
|
|
506
516
|
---
|
|
Binary file
|