@open-assets/open-assets 0.3.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 +49 -14
- 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/public/viewer.html +190 -2
- package/skills/open-assets/SKILL.md +35 -13
- package/public/img/banner/1280/banner.png +0 -0
package/lib/render.mjs
CHANGED
|
@@ -1,24 +1,34 @@
|
|
|
1
|
-
import { resolve, join } from "path";
|
|
2
|
-
import { mkdirSync,
|
|
1
|
+
import { resolve, join, relative } from "path";
|
|
2
|
+
import { mkdirSync, copyFileSync, existsSync } from "fs";
|
|
3
|
+
import { writeFile } from "fs/promises";
|
|
4
|
+
import { cpus } from "os";
|
|
3
5
|
import { filterCollections, filterTemplates, resolveRenderSizes, flatOutputs } from "./manifest.mjs";
|
|
4
6
|
import { computeExportChecksum } from "./lockfile.mjs";
|
|
5
7
|
import { loadLocalizations, resolveStrings, getLocales, getDirection } from "./localization.mjs";
|
|
6
8
|
|
|
9
|
+
// Renders overlap because per-image time is dominated by page load waits,
|
|
10
|
+
// not CPU. Each in-flight unit holds one Puppeteer page.
|
|
11
|
+
const CONCURRENCY = Math.max(1, Math.min(4, cpus().length - 1));
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* @typedef {object} RenderDeps
|
|
9
|
-
* @property {(projectDir: string, htmlFile: string,
|
|
15
|
+
* @property {(projectDir: string, htmlFile: string, sourceW: number, sourceH: number, variants: {width: number, height: number, format?: string}[], options?: object) => Promise<Buffer[]>} renderVariants
|
|
10
16
|
* @property {(projectDir: string, collection: object, outputConfig: object) => Promise<string>} runXcodeOutput
|
|
11
17
|
* @property {() => Promise<void>} closeBrowser
|
|
12
18
|
* @property {(projectDir: string) => object} readLockfile
|
|
13
19
|
* @property {(projectDir: string, data: object) => void} writeLockfile
|
|
14
20
|
* @property {(filePath: string) => string|null} computeChecksum
|
|
15
|
-
* @property {(lockData: object, assetKey: string, variantKey: string, checksum: string, outputPath: string, exportChecksum?: string) => boolean} isUpToDate
|
|
21
|
+
* @property {(lockData: object, assetKey: string, variantKey: string, checksum: string, outputPath: string, exportChecksum?: string, projectDir?: string) => boolean} isUpToDate
|
|
16
22
|
* @property {(lockData: object, assetKey: string, variantKey: string, checksum: string, outputPath: string, exportChecksum?: string) => void} recordExport
|
|
17
23
|
*/
|
|
18
24
|
|
|
19
25
|
/**
|
|
20
26
|
* Render assets according to the manifest and options.
|
|
21
27
|
*
|
|
28
|
+
* Work is grouped into units of one (template, locale) pair: each unit
|
|
29
|
+
* navigates its page once and screenshots every export size from that single
|
|
30
|
+
* load. Units render concurrently on a small internal pool.
|
|
31
|
+
*
|
|
22
32
|
* @param {string} projectDir - Absolute path to the project directory
|
|
23
33
|
* @param {object} manifest - Parsed manifest object
|
|
24
34
|
* @param {object} options - Render options from CLI
|
|
@@ -30,8 +40,9 @@ import { loadLocalizations, resolveStrings, getLocales, getDirection } from "./l
|
|
|
30
40
|
* @param {string} [options.format] - Output format override (png or jpg)
|
|
31
41
|
* @param {string|number} [options.width] - Custom width
|
|
32
42
|
* @param {string|number} [options.height] - Custom height
|
|
33
|
-
* @param {boolean} [options.force] -
|
|
43
|
+
* @param {boolean} [options.force] - Re-render all assets, ignoring the incremental cache
|
|
34
44
|
* @param {string} [options.output] - Output directory path
|
|
45
|
+
* @param {string|number} [options.renderTimeout] - Puppeteer render timeout in ms
|
|
35
46
|
* @param {boolean} [options.quiet] - Suppress logs
|
|
36
47
|
* @param {RenderDeps} deps - Injected dependencies
|
|
37
48
|
* @param {(msg: string) => void} [log] - Optional log function
|
|
@@ -54,216 +65,330 @@ export async function renderAssets(projectDir, manifest, options, deps, log) {
|
|
|
54
65
|
const outputDir = resolve(options.output || "./exports");
|
|
55
66
|
mkdirSync(outputDir, { recursive: true });
|
|
56
67
|
|
|
68
|
+
const renderTimeout = options.renderTimeout
|
|
69
|
+
? parseInt(options.renderTimeout, 10)
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
57
72
|
const lockData = deps.readLockfile(projectDir);
|
|
58
73
|
const results = [];
|
|
59
74
|
let skipped = 0;
|
|
60
75
|
const startTime = Date.now();
|
|
61
76
|
|
|
62
|
-
for
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const { sizes: sizesToRender, warning } = resolveRenderSizes(col, {
|
|
69
|
-
width: options.width,
|
|
70
|
-
height: options.height,
|
|
71
|
-
size: options.size,
|
|
72
|
-
force: options.force,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
if (warning) {
|
|
76
|
-
_log(` Warning: ${warning}`);
|
|
77
|
-
continue;
|
|
77
|
+
// Source checksums are memoized for the run: a template is hashed once no
|
|
78
|
+
// matter how many sizes and locales it renders to.
|
|
79
|
+
const checksumCache = new Map();
|
|
80
|
+
const checksumOf = (srcPath) => {
|
|
81
|
+
if (!checksumCache.has(srcPath)) {
|
|
82
|
+
checksumCache.set(srcPath, deps.computeChecksum(srcPath));
|
|
78
83
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
return checksumCache.get(srcPath);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// The lockfile and browser are handled in finally so a mid-run failure
|
|
88
|
+
// still persists completed work and never leaks a Chromium process.
|
|
89
|
+
try {
|
|
90
|
+
for (const col of collections) {
|
|
91
|
+
const sourceW = col.sourceSize.width;
|
|
92
|
+
const sourceH = col.sourceSize.height;
|
|
93
|
+
|
|
94
|
+
const templates = filterTemplates(col.templates, options.template);
|
|
95
|
+
|
|
96
|
+
const { sizes: sizesToRender, warning } = resolveRenderSizes(col, {
|
|
97
|
+
width: options.width,
|
|
98
|
+
height: options.height,
|
|
99
|
+
size: options.size,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (warning) {
|
|
103
|
+
_log(` Warning: ${warning}`);
|
|
104
|
+
continue;
|
|
98
105
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const singleTemplate = templates.length === 1;
|
|
102
|
-
const multipleSizes = sizesToRender.length > 1 || options.force;
|
|
103
|
-
// Single-template collections use flat layout with entry.name as filename
|
|
104
|
-
// Multi-template collections use subdirs per size
|
|
105
|
-
const useSubDirs = multipleSizes && !singleTemplate;
|
|
106
|
-
const hasLocales = locales.length > 0 && locales[0] !== null;
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const localization = locale ? { strings, locale, direction } : undefined;
|
|
112
|
-
|
|
113
|
-
if (locale) {
|
|
114
|
-
_log(`\n Locale: ${locale} (${direction})`);
|
|
107
|
+
if (sizesToRender.length === 0) {
|
|
108
|
+
_log(` No export sizes found for collection "${col.id}"`);
|
|
109
|
+
continue;
|
|
115
110
|
}
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
: join(outputDir, col.id);
|
|
112
|
+
// Resolve locales for this collection
|
|
113
|
+
let locales = [null]; // null = no localization
|
|
114
|
+
let localizations = null;
|
|
115
|
+
|
|
116
|
+
if (col.localizations) {
|
|
117
|
+
localizations = loadLocalizations(projectDir, col.localizations);
|
|
118
|
+
locales = getLocales(localizations, col.locales);
|
|
119
|
+
if (options.locale) {
|
|
120
|
+
locales = locales.filter((l) => l === options.locale);
|
|
121
|
+
if (locales.length === 0) {
|
|
122
|
+
_log(` Warning: locale "${options.locale}" not found in collection "${col.id}", skipping`);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
131
125
|
}
|
|
132
|
-
|
|
126
|
+
}
|
|
133
127
|
|
|
134
|
-
|
|
135
|
-
|
|
128
|
+
// Layout is decided by the collection's full template list, not the
|
|
129
|
+
// filtered one, so --template renders land in the same paths as full runs
|
|
130
|
+
const singleTemplate = col.templates.length === 1;
|
|
131
|
+
// Single-template collections use flat layout with entry.name as filename
|
|
132
|
+
// Multi-template collections use subdirs per size
|
|
133
|
+
const useSubDirs = !singleTemplate;
|
|
134
|
+
const hasLocales = locales.length > 0 && locales[0] !== null;
|
|
135
|
+
|
|
136
|
+
// Enumerate render units: one per (locale, template), each carrying
|
|
137
|
+
// its non-skipped size variants. Skip checks run up front so units
|
|
138
|
+
// whose variants are all cached never open a page.
|
|
139
|
+
const units = [];
|
|
140
|
+
|
|
141
|
+
for (const locale of locales) {
|
|
142
|
+
const strings = locale ? resolveStrings(localizations, locale) : null;
|
|
143
|
+
const direction = locale ? getDirection(locale) : null;
|
|
144
|
+
const localization = locale ? { strings, locale, direction } : undefined;
|
|
145
|
+
|
|
146
|
+
if (locale) {
|
|
147
|
+
_log(`\n Locale: ${locale} (${direction})`);
|
|
136
148
|
}
|
|
137
149
|
|
|
138
150
|
for (const template of templates) {
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
? entry.outFile.split(".").pop().toLowerCase()
|
|
142
|
-
: "png";
|
|
143
|
-
const format = outExt === "jpg" || outExt === "jpeg" ? "jpeg" : "png";
|
|
144
|
-
const defaultExt = format === "jpeg" ? "jpg" : "png";
|
|
145
|
-
|
|
146
|
-
// Single-template: use entry.name as filename (e.g. logo-1024.png)
|
|
147
|
-
// Multi-template: use template.name as filename (e.g. 01-hero.png)
|
|
148
|
-
const fileName = singleTemplate && multipleSizes
|
|
149
|
-
? `${entry.name}.${defaultExt}`
|
|
150
|
-
: `${template.name}.${defaultExt}`;
|
|
151
|
-
|
|
152
|
-
let outPath;
|
|
153
|
-
if (entry.outFile) {
|
|
154
|
-
let resolved = entry.outFile.replace(/\{template\}/g, template.name);
|
|
155
|
-
if (locale) {
|
|
156
|
-
resolved = resolved.replace(/\{locale\}/g, locale);
|
|
157
|
-
}
|
|
158
|
-
outPath = resolve(projectDir, resolved);
|
|
159
|
-
} else {
|
|
160
|
-
outPath = join(sizeDir, fileName);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Asset key and variant key include locale for cache isolation
|
|
151
|
+
const srcPath = resolve(projectDir, template.src);
|
|
152
|
+
const checksum = checksumOf(srcPath);
|
|
164
153
|
const assetKey = locale
|
|
165
154
|
? `${col.id}/${locale}/${template.name}`
|
|
166
155
|
: `${col.id}/${template.name}`;
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
156
|
+
|
|
157
|
+
const variants = [];
|
|
158
|
+
|
|
159
|
+
for (const entry of sizesToRender) {
|
|
160
|
+
const w = entry.size.width;
|
|
161
|
+
const h = entry.size.height;
|
|
162
|
+
|
|
163
|
+
// Build output directory: insert locale level when localizations are active
|
|
164
|
+
let sizeDir;
|
|
165
|
+
if (hasLocales) {
|
|
166
|
+
sizeDir = useSubDirs
|
|
167
|
+
? join(outputDir, col.id, locale, entry.name)
|
|
168
|
+
: join(outputDir, col.id, locale);
|
|
169
|
+
} else {
|
|
170
|
+
sizeDir = useSubDirs
|
|
171
|
+
? join(outputDir, col.id, entry.name)
|
|
172
|
+
: join(outputDir, col.id);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Determine output format from outFile extension or default to png
|
|
176
|
+
const outExt = entry.outFile
|
|
177
|
+
? entry.outFile.split(".").pop().toLowerCase()
|
|
178
|
+
: "png";
|
|
179
|
+
const format = outExt === "jpg" || outExt === "jpeg" ? "jpeg" : "png";
|
|
180
|
+
const defaultExt = format === "jpeg" ? "jpg" : "png";
|
|
181
|
+
|
|
182
|
+
// Single-template: use entry.name as filename (e.g. logo/1024.png)
|
|
183
|
+
// Multi-template: use template.name as filename (e.g. 01-hero.png)
|
|
184
|
+
const fileName = singleTemplate
|
|
185
|
+
? `${entry.name}.${defaultExt}`
|
|
186
|
+
: `${template.name}.${defaultExt}`;
|
|
187
|
+
|
|
188
|
+
let outPath;
|
|
189
|
+
if (entry.outFile) {
|
|
190
|
+
let resolved = entry.outFile.replace(/\{template\}/g, template.name);
|
|
191
|
+
if (locale) {
|
|
192
|
+
resolved = resolved.replace(/\{locale\}/g, locale);
|
|
193
|
+
}
|
|
194
|
+
outPath = resolve(projectDir, resolved);
|
|
195
|
+
// Create the destination directory now, before any render or
|
|
196
|
+
// SVG source copy targets it.
|
|
197
|
+
mkdirSync(resolve(projectDir, join(outPath, "..")), { recursive: true });
|
|
198
|
+
} else {
|
|
199
|
+
mkdirSync(sizeDir, { recursive: true });
|
|
200
|
+
outPath = join(sizeDir, fileName);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const exportCksum = computeExportChecksum(entry, locale);
|
|
204
|
+
// Lockfile paths are stored relative to the project so the cache
|
|
205
|
+
// is portable across machines. The variant is keyed by output path,
|
|
206
|
+
// so the same template rendered to different destinations gets its
|
|
207
|
+
// own cache entry per destination.
|
|
208
|
+
const lockOutPath = relative(projectDir, outPath);
|
|
209
|
+
const variantKey = lockOutPath;
|
|
210
|
+
|
|
211
|
+
// Skip if unchanged
|
|
212
|
+
if (
|
|
213
|
+
!options.force &&
|
|
214
|
+
checksum &&
|
|
215
|
+
deps.isUpToDate(lockData, assetKey, variantKey, checksum, lockOutPath, exportCksum, projectDir)
|
|
216
|
+
) {
|
|
217
|
+
_log(
|
|
218
|
+
` Skipping ${template.name}${locale ? ` [${locale}]` : ""} at ${w}x${h} (unchanged)`
|
|
219
|
+
);
|
|
220
|
+
skipped++;
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
variants.push({
|
|
225
|
+
w,
|
|
226
|
+
h,
|
|
227
|
+
format,
|
|
228
|
+
defaultExt,
|
|
229
|
+
outPath,
|
|
230
|
+
lockOutPath,
|
|
231
|
+
exportCksum,
|
|
232
|
+
});
|
|
183
233
|
}
|
|
184
234
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
235
|
+
if (variants.length > 0) {
|
|
236
|
+
units.push({
|
|
237
|
+
template,
|
|
238
|
+
locale,
|
|
239
|
+
localization,
|
|
240
|
+
srcPath,
|
|
241
|
+
checksum,
|
|
242
|
+
assetKey,
|
|
243
|
+
variants,
|
|
244
|
+
logs: [],
|
|
245
|
+
results: [],
|
|
246
|
+
completed: false,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
188
251
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
252
|
+
// Drain units through a small worker pool. Logs are buffered per unit
|
|
253
|
+
// and flushed in enumeration order so output stays deterministic.
|
|
254
|
+
let nextUnit = 0;
|
|
255
|
+
let nextFlush = 0;
|
|
256
|
+
const errors = [];
|
|
257
|
+
|
|
258
|
+
const flushLogs = () => {
|
|
259
|
+
while (nextFlush < units.length && units[nextFlush].completed) {
|
|
260
|
+
for (const line of units[nextFlush].logs) _log(line);
|
|
261
|
+
nextFlush++;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const runUnit = async (unit) => {
|
|
266
|
+
const isSvg = unit.template.src.endsWith(".svg");
|
|
267
|
+
unit.logs.push(
|
|
268
|
+
` Rendering ${unit.template.name}${unit.locale ? ` [${unit.locale}]` : ""} (${unit.variants.length} size${unit.variants.length === 1 ? "" : "s"})...`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const buffers = await deps.renderVariants(
|
|
272
|
+
projectDir,
|
|
273
|
+
unit.template.src,
|
|
274
|
+
sourceW,
|
|
275
|
+
sourceH,
|
|
276
|
+
unit.variants.map((v) => ({ width: v.w, height: v.h, format: v.format })),
|
|
277
|
+
{
|
|
278
|
+
background: col.background,
|
|
279
|
+
localization: unit.localization,
|
|
280
|
+
timeout: renderTimeout,
|
|
192
281
|
}
|
|
282
|
+
);
|
|
193
283
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
284
|
+
for (let i = 0; i < unit.variants.length; i++) {
|
|
285
|
+
const v = unit.variants[i];
|
|
286
|
+
const buffer = buffers[i];
|
|
287
|
+
|
|
288
|
+
if (isSvg && v.w === sourceW && v.h === sourceH) {
|
|
289
|
+
copyFileSync(unit.srcPath, v.outPath.replace(`.${v.defaultExt}`, ".svg"));
|
|
197
290
|
}
|
|
198
291
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
sourceW,
|
|
205
|
-
sourceH,
|
|
206
|
-
{ format, background: col.background, localization }
|
|
207
|
-
);
|
|
208
|
-
writeFileSync(outPath, buffer);
|
|
209
|
-
results.push({
|
|
292
|
+
await writeFile(v.outPath, buffer);
|
|
293
|
+
// The output write above must complete before the variant is
|
|
294
|
+
// marked rendered; recordExport keys off this flag.
|
|
295
|
+
v.rendered = true;
|
|
296
|
+
unit.results.push({
|
|
210
297
|
collection: col.id,
|
|
211
|
-
template: template.name,
|
|
212
|
-
locale: locale || undefined,
|
|
213
|
-
path: outPath,
|
|
214
|
-
width: w,
|
|
215
|
-
height: h,
|
|
298
|
+
template: unit.template.name,
|
|
299
|
+
locale: unit.locale || undefined,
|
|
300
|
+
path: v.outPath,
|
|
301
|
+
width: v.w,
|
|
302
|
+
height: v.h,
|
|
216
303
|
size: buffer.length,
|
|
217
304
|
});
|
|
218
|
-
|
|
219
|
-
|
|
305
|
+
unit.logs.push(` → ${v.outPath} (${v.w}x${v.h})`);
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const workers = Array.from(
|
|
310
|
+
{ length: Math.min(CONCURRENCY, units.length) },
|
|
311
|
+
async () => {
|
|
312
|
+
while (nextUnit < units.length && errors.length === 0) {
|
|
313
|
+
const unit = units[nextUnit++];
|
|
314
|
+
try {
|
|
315
|
+
await runUnit(unit);
|
|
316
|
+
} catch (err) {
|
|
317
|
+
unit.logs.push(` Error rendering ${unit.template.name}${unit.locale ? ` [${unit.locale}]` : ""}: ${err.message}`);
|
|
318
|
+
errors.push(err);
|
|
319
|
+
}
|
|
320
|
+
unit.completed = true;
|
|
321
|
+
flushLogs();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
await Promise.all(workers);
|
|
326
|
+
flushLogs();
|
|
327
|
+
|
|
328
|
+
// Collect results and lockfile records in enumeration order so the
|
|
329
|
+
// output arrays and lockfile bytes are stable across runs.
|
|
330
|
+
for (const unit of units) {
|
|
331
|
+
results.push(...unit.results);
|
|
332
|
+
if (!unit.checksum) continue;
|
|
333
|
+
for (const v of unit.variants) {
|
|
334
|
+
if (v.rendered) {
|
|
335
|
+
deps.recordExport(lockData, unit.assetKey, v.lockOutPath, unit.checksum, v.lockOutPath, v.exportCksum);
|
|
220
336
|
}
|
|
221
|
-
_log(` → ${outPath}`);
|
|
222
337
|
}
|
|
223
338
|
}
|
|
224
|
-
}
|
|
225
339
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
340
|
+
if (errors.length > 0) {
|
|
341
|
+
throw errors[0];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Handle outputs (xcode, copy-source, etc.)
|
|
345
|
+
const outputs = flatOutputs(col);
|
|
346
|
+
if (outputs.length > 0) {
|
|
347
|
+
for (const output of outputs) {
|
|
348
|
+
if (output.type === "xcode") {
|
|
349
|
+
_log(`\n Exporting to Xcode: ${output.path}`);
|
|
350
|
+
const path = await deps.runXcodeOutput(projectDir, col, output);
|
|
351
|
+
results.push({ collection: col.id, type: "xcode", path });
|
|
352
|
+
} else if (output.type === "copy-source") {
|
|
353
|
+
const format = output.format || "svg";
|
|
354
|
+
const copyDir = join(outputDir, col.id);
|
|
355
|
+
mkdirSync(copyDir, { recursive: true });
|
|
356
|
+
const matchingTemplates = templates.filter((t) => t.src.endsWith(`.${format}`));
|
|
357
|
+
// Naming follows the collection's full match count so --template
|
|
358
|
+
// runs produce the same filenames as full runs
|
|
359
|
+
const collectionMatches = col.templates.filter((t) => t.src.endsWith(`.${format}`));
|
|
360
|
+
for (const template of matchingTemplates) {
|
|
361
|
+
const srcPath = resolve(projectDir, template.src);
|
|
362
|
+
// Use entry name when single match, template name when multiple
|
|
363
|
+
const baseName = output.name && collectionMatches.length === 1
|
|
364
|
+
? output.name
|
|
365
|
+
: template.name;
|
|
366
|
+
const destPath = join(copyDir, `${baseName}.${format}`);
|
|
367
|
+
if (existsSync(srcPath)) {
|
|
368
|
+
copyFileSync(srcPath, destPath);
|
|
369
|
+
results.push({
|
|
370
|
+
collection: col.id,
|
|
371
|
+
template: template.name,
|
|
372
|
+
type: `copy-${format}`,
|
|
373
|
+
path: destPath,
|
|
374
|
+
});
|
|
375
|
+
_log(` → ${destPath}`);
|
|
376
|
+
}
|
|
255
377
|
}
|
|
256
378
|
}
|
|
257
379
|
}
|
|
258
380
|
}
|
|
259
381
|
}
|
|
382
|
+
} finally {
|
|
383
|
+
// Persist whatever completed, even when a render failed mid-run
|
|
384
|
+
deps.writeLockfile(projectDir, lockData);
|
|
385
|
+
try {
|
|
386
|
+
await deps.closeBrowser();
|
|
387
|
+
} catch {
|
|
388
|
+
// A browser-close failure must not mask the original render error
|
|
389
|
+
}
|
|
260
390
|
}
|
|
261
391
|
|
|
262
|
-
// Persist lockfile
|
|
263
|
-
deps.writeLockfile(projectDir, lockData);
|
|
264
|
-
|
|
265
|
-
await deps.closeBrowser();
|
|
266
|
-
|
|
267
392
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
268
393
|
return { results, skipped, elapsed: `${elapsed}s` };
|
|
269
394
|
}
|