@open-assets/open-assets 0.1.10 → 0.1.12
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 +113 -94
- package/bin/open-assets.mjs +133 -320
- package/lib/add.mjs +476 -0
- package/lib/init.mjs +215 -107
- package/lib/list.mjs +66 -0
- package/lib/lockfile.mjs +1 -1
- package/lib/manifest.mjs +125 -0
- package/lib/render.mjs +206 -0
- package/lib/renderer.mjs +19 -5
- package/lib/server.mjs +14 -4
- package/lib/validate.mjs +90 -0
- package/package.json +10 -1
- package/public/img/banner/1280/banner.png +0 -0
- package/public/img/icon/16/icon.png +0 -0
- package/public/img/icon/180/icon.png +0 -0
- package/public/img/icon/192/icon.png +0 -0
- package/public/img/icon/32/icon.png +0 -0
- package/public/img/icon/512/icon.png +0 -0
- package/public/viewer.html +28 -25
- package/skills/open-assets/SKILL.md +544 -0
package/lib/init.mjs
CHANGED
|
@@ -29,34 +29,172 @@ function askConfirm(rl, question, defaultYes = true) {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
function askCheckbox(rl, question, options) {
|
|
32
|
+
// Non-TTY fallback: numbered list, comma-separated input
|
|
33
|
+
if (!process.stdin.isTTY) {
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
console.log(`\n ${question}\n`);
|
|
36
|
+
options.forEach((opt, i) => {
|
|
37
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
38
|
+
});
|
|
39
|
+
console.log();
|
|
40
|
+
rl.question(" Enter numbers (comma-separated): ", (answer) => {
|
|
41
|
+
const indices = answer.trim().split(",").map((s) => parseInt(s.trim(), 10) - 1);
|
|
42
|
+
resolve(
|
|
43
|
+
options
|
|
44
|
+
.filter((_, i) => indices.includes(i))
|
|
45
|
+
.map((o) => o.value)
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
32
51
|
return new Promise((resolve) => {
|
|
52
|
+
const selected = new Set();
|
|
53
|
+
let cursor = 0;
|
|
54
|
+
|
|
55
|
+
function render() {
|
|
56
|
+
// Move cursor up to overwrite previous render (if not first render)
|
|
57
|
+
if (render._rendered) {
|
|
58
|
+
process.stdout.write(`\x1b[${options.length + 1}A`);
|
|
59
|
+
}
|
|
60
|
+
render._rendered = true;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i < options.length; i++) {
|
|
63
|
+
const check = selected.has(i) ? "\x1b[36m●\x1b[0m" : "○";
|
|
64
|
+
const label = i === cursor ? `\x1b[36m${options[i].label}\x1b[0m` : options[i].label;
|
|
65
|
+
const pointer = i === cursor ? "\x1b[36m❯\x1b[0m" : " ";
|
|
66
|
+
process.stdout.write(`\x1b[2K ${pointer} ${check} ${label}\n`);
|
|
67
|
+
}
|
|
68
|
+
process.stdout.write(`\x1b[2K \x1b[2m(↑/↓ navigate, space select, enter confirm)\x1b[0m`);
|
|
69
|
+
}
|
|
70
|
+
|
|
33
71
|
console.log(`\n ${question}\n`);
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
72
|
+
render();
|
|
73
|
+
|
|
74
|
+
const { stdin } = process;
|
|
75
|
+
const wasRaw = stdin.isRaw;
|
|
76
|
+
stdin.setRawMode(true);
|
|
77
|
+
stdin.resume();
|
|
78
|
+
|
|
79
|
+
function onData(key) {
|
|
80
|
+
// Ctrl+C
|
|
81
|
+
if (key[0] === 3) {
|
|
82
|
+
stdin.setRawMode(wasRaw);
|
|
83
|
+
stdin.removeListener("data", onData);
|
|
84
|
+
process.stdout.write("\n");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Enter
|
|
89
|
+
if (key[0] === 13) {
|
|
90
|
+
stdin.setRawMode(wasRaw);
|
|
91
|
+
stdin.removeListener("data", onData);
|
|
92
|
+
process.stdout.write("\n");
|
|
93
|
+
resolve(
|
|
94
|
+
options
|
|
95
|
+
.filter((_, i) => selected.has(i))
|
|
96
|
+
.map((o) => o.value)
|
|
97
|
+
);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Space – toggle
|
|
102
|
+
if (key[0] === 32) {
|
|
103
|
+
if (selected.has(cursor)) selected.delete(cursor);
|
|
104
|
+
else selected.add(cursor);
|
|
105
|
+
render();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Arrow keys (escape sequences: 27, 91, 65/66)
|
|
110
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
111
|
+
if (key[2] === 65) {
|
|
112
|
+
// Up
|
|
113
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
114
|
+
render();
|
|
115
|
+
} else if (key[2] === 66) {
|
|
116
|
+
// Down
|
|
117
|
+
cursor = (cursor + 1) % options.length;
|
|
118
|
+
render();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
stdin.on("data", onData);
|
|
45
124
|
});
|
|
46
125
|
}
|
|
47
126
|
|
|
48
127
|
function askSelect(rl, question, options) {
|
|
128
|
+
// Non-TTY fallback: numbered list
|
|
129
|
+
if (!process.stdin.isTTY) {
|
|
130
|
+
return new Promise((resolve) => {
|
|
131
|
+
console.log(`\n ${question}\n`);
|
|
132
|
+
options.forEach((opt, i) => {
|
|
133
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
134
|
+
});
|
|
135
|
+
console.log();
|
|
136
|
+
rl.question(" Enter number: ", (answer) => {
|
|
137
|
+
const idx = parseInt(answer.trim(), 10) - 1;
|
|
138
|
+
if (idx >= 0 && idx < options.length) resolve(options[idx].value);
|
|
139
|
+
else resolve(options[0].value);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
49
144
|
return new Promise((resolve) => {
|
|
145
|
+
let cursor = 0;
|
|
146
|
+
|
|
147
|
+
function render() {
|
|
148
|
+
if (render._rendered) {
|
|
149
|
+
process.stdout.write(`\x1b[${options.length + 1}A`);
|
|
150
|
+
}
|
|
151
|
+
render._rendered = true;
|
|
152
|
+
|
|
153
|
+
for (let i = 0; i < options.length; i++) {
|
|
154
|
+
const dot = i === cursor ? "\x1b[36m●\x1b[0m" : "○";
|
|
155
|
+
const label = i === cursor ? `\x1b[36m${options[i].label}\x1b[0m` : options[i].label;
|
|
156
|
+
const pointer = i === cursor ? "\x1b[36m❯\x1b[0m" : " ";
|
|
157
|
+
process.stdout.write(`\x1b[2K ${pointer} ${dot} ${label}\n`);
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write(`\x1b[2K \x1b[2m(↑/↓ navigate, enter confirm)\x1b[0m`);
|
|
160
|
+
}
|
|
161
|
+
|
|
50
162
|
console.log(`\n ${question}\n`);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
163
|
+
render();
|
|
164
|
+
|
|
165
|
+
const { stdin } = process;
|
|
166
|
+
const wasRaw = stdin.isRaw;
|
|
167
|
+
stdin.setRawMode(true);
|
|
168
|
+
stdin.resume();
|
|
169
|
+
|
|
170
|
+
function onData(key) {
|
|
171
|
+
if (key[0] === 3) {
|
|
172
|
+
stdin.setRawMode(wasRaw);
|
|
173
|
+
stdin.removeListener("data", onData);
|
|
174
|
+
process.stdout.write("\n");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (key[0] === 13) {
|
|
179
|
+
stdin.setRawMode(wasRaw);
|
|
180
|
+
stdin.removeListener("data", onData);
|
|
181
|
+
process.stdout.write("\n");
|
|
182
|
+
resolve(options[cursor].value);
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (key[0] === 27 && key[1] === 91) {
|
|
187
|
+
if (key[2] === 65) {
|
|
188
|
+
cursor = (cursor - 1 + options.length) % options.length;
|
|
189
|
+
render();
|
|
190
|
+
} else if (key[2] === 66) {
|
|
191
|
+
cursor = (cursor + 1) % options.length;
|
|
192
|
+
render();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
stdin.on("data", onData);
|
|
60
198
|
});
|
|
61
199
|
}
|
|
62
200
|
|
|
@@ -66,29 +204,23 @@ const ASSET_TYPES = {
|
|
|
66
204
|
screenshots: {
|
|
67
205
|
label: "App Store Screenshots",
|
|
68
206
|
collection: (platforms) => {
|
|
69
|
-
const
|
|
207
|
+
const exportList = [];
|
|
70
208
|
|
|
71
209
|
if (platforms.includes("ios")) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{ name: "ipad-13", label: 'iPad 13"', width: 2064, height: 2752 },
|
|
79
|
-
],
|
|
80
|
-
});
|
|
210
|
+
exportList.push(
|
|
211
|
+
{ name: "iphone-6.9", label: 'iPhone 6.9"', size: { width: 1320, height: 2868 } },
|
|
212
|
+
{ name: "iphone-6.7", label: 'iPhone 6.7"', size: { width: 1290, height: 2796 } },
|
|
213
|
+
{ name: "iphone-6.5", label: 'iPhone 6.5"', size: { width: 1284, height: 2778 } },
|
|
214
|
+
{ name: "ipad-13", label: 'iPad 13"', size: { width: 2064, height: 2752 } },
|
|
215
|
+
);
|
|
81
216
|
}
|
|
82
217
|
|
|
83
218
|
if (platforms.includes("android")) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{ name: "tablet-10", label: '10" Tablet', width: 1600, height: 2560 },
|
|
90
|
-
],
|
|
91
|
-
});
|
|
219
|
+
exportList.push(
|
|
220
|
+
{ name: "phone", label: "Phone", size: { width: 1080, height: 1920 } },
|
|
221
|
+
{ name: "tablet-7", label: '7" Tablet', size: { width: 1200, height: 1920 } },
|
|
222
|
+
{ name: "tablet-10", label: '10" Tablet', size: { width: 1600, height: 2560 } },
|
|
223
|
+
);
|
|
92
224
|
}
|
|
93
225
|
|
|
94
226
|
return {
|
|
@@ -100,7 +232,7 @@ const ASSET_TYPES = {
|
|
|
100
232
|
templates: [
|
|
101
233
|
{ src: "src/screenshots/01-hero.html", name: "01-hero", label: "Hero" },
|
|
102
234
|
],
|
|
103
|
-
|
|
235
|
+
export: exportList,
|
|
104
236
|
customExport: { defaultWidth: 1320, defaultHeight: 2868 },
|
|
105
237
|
};
|
|
106
238
|
},
|
|
@@ -146,48 +278,39 @@ const ASSET_TYPES = {
|
|
|
146
278
|
icon: {
|
|
147
279
|
label: "App Icon",
|
|
148
280
|
collection: (platforms) => {
|
|
149
|
-
const
|
|
281
|
+
const exportList = [];
|
|
150
282
|
|
|
151
283
|
if (platforms.includes("ios")) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
{ name: "40", label: "40px", width: 40, height: 40 },
|
|
162
|
-
],
|
|
163
|
-
});
|
|
284
|
+
exportList.push(
|
|
285
|
+
{ name: "1024", label: "1024px", size: { width: 1024, height: 1024 } },
|
|
286
|
+
{ name: "180", label: "180px", size: { width: 180, height: 180 } },
|
|
287
|
+
{ name: "120", label: "120px", size: { width: 120, height: 120 } },
|
|
288
|
+
{ name: "87", label: "87px", size: { width: 87, height: 87 } },
|
|
289
|
+
{ name: "80", label: "80px", size: { width: 80, height: 80 } },
|
|
290
|
+
{ name: "60", label: "60px", size: { width: 60, height: 60 } },
|
|
291
|
+
{ name: "40", label: "40px", size: { width: 40, height: 40 } },
|
|
292
|
+
);
|
|
164
293
|
}
|
|
165
294
|
|
|
166
295
|
if (platforms.includes("android")) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
{ name: "48", label: "mdpi", width: 48, height: 48 },
|
|
176
|
-
],
|
|
177
|
-
});
|
|
296
|
+
exportList.push(
|
|
297
|
+
{ name: "512", label: "Play Store", size: { width: 512, height: 512 } },
|
|
298
|
+
{ name: "192", label: "xxxhdpi", size: { width: 192, height: 192 } },
|
|
299
|
+
{ name: "144", label: "xxhdpi", size: { width: 144, height: 144 } },
|
|
300
|
+
{ name: "96", label: "xhdpi", size: { width: 96, height: 96 } },
|
|
301
|
+
{ name: "72", label: "hdpi", size: { width: 72, height: 72 } },
|
|
302
|
+
{ name: "48", label: "mdpi", size: { width: 48, height: 48 } },
|
|
303
|
+
);
|
|
178
304
|
}
|
|
179
305
|
|
|
180
306
|
if (platforms.includes("web")) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
{ name: "16", label: "Favicon 16", width: 16, height: 16 },
|
|
189
|
-
],
|
|
190
|
-
});
|
|
307
|
+
exportList.push(
|
|
308
|
+
{ name: "512", label: "512px", size: { width: 512, height: 512 } },
|
|
309
|
+
{ name: "192", label: "192px", size: { width: 192, height: 192 } },
|
|
310
|
+
{ name: "180", label: "Apple Touch", size: { width: 180, height: 180 } },
|
|
311
|
+
{ name: "32", label: "Favicon 32", size: { width: 32, height: 32 } },
|
|
312
|
+
{ name: "16", label: "Favicon 16", size: { width: 16, height: 16 } },
|
|
313
|
+
);
|
|
191
314
|
}
|
|
192
315
|
|
|
193
316
|
return {
|
|
@@ -199,7 +322,7 @@ const ASSET_TYPES = {
|
|
|
199
322
|
templates: [
|
|
200
323
|
{ src: "src/icon.html", name: "icon", label: "App Icon" },
|
|
201
324
|
],
|
|
202
|
-
|
|
325
|
+
export: exportList,
|
|
203
326
|
};
|
|
204
327
|
},
|
|
205
328
|
dirs: [],
|
|
@@ -245,16 +368,11 @@ const ASSET_TYPES = {
|
|
|
245
368
|
templates: [
|
|
246
369
|
{ src: "src/logo.html", name: "logo", label: "Logo" },
|
|
247
370
|
],
|
|
248
|
-
|
|
249
|
-
{
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
{ name: "400", label: "400px", width: 400, height: 400 },
|
|
254
|
-
{ name: "200", label: "200px", width: 200, height: 200 },
|
|
255
|
-
{ name: "64", label: "64px", width: 64, height: 64 },
|
|
256
|
-
],
|
|
257
|
-
},
|
|
371
|
+
export: [
|
|
372
|
+
{ name: "800", label: "800px", size: { width: 800, height: 800 } },
|
|
373
|
+
{ name: "400", label: "400px", size: { width: 400, height: 400 } },
|
|
374
|
+
{ name: "200", label: "200px", size: { width: 200, height: 200 } },
|
|
375
|
+
{ name: "64", label: "64px", size: { width: 64, height: 64 } },
|
|
258
376
|
],
|
|
259
377
|
}),
|
|
260
378
|
dirs: [],
|
|
@@ -311,15 +429,10 @@ const ASSET_TYPES = {
|
|
|
311
429
|
templates: [
|
|
312
430
|
{ src: "src/wordmark.html", name: "wordmark", label: "Wordmark" },
|
|
313
431
|
],
|
|
314
|
-
|
|
315
|
-
{
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
{ name: "1200x400", label: "Large", width: 1200, height: 400 },
|
|
319
|
-
{ name: "600x200", label: "Medium", width: 600, height: 200 },
|
|
320
|
-
{ name: "300x100", label: "Small", width: 300, height: 100 },
|
|
321
|
-
],
|
|
322
|
-
},
|
|
432
|
+
export: [
|
|
433
|
+
{ name: "1200x400", label: "Large", size: { width: 1200, height: 400 } },
|
|
434
|
+
{ name: "600x200", label: "Medium", size: { width: 600, height: 200 } },
|
|
435
|
+
{ name: "300x100", label: "Small", size: { width: 300, height: 100 } },
|
|
323
436
|
],
|
|
324
437
|
}),
|
|
325
438
|
dirs: [],
|
|
@@ -368,13 +481,8 @@ const ASSET_TYPES = {
|
|
|
368
481
|
templates: [
|
|
369
482
|
{ src: "src/feature-graphic.html", name: "feature-graphic", label: "Feature Graphic" },
|
|
370
483
|
],
|
|
371
|
-
|
|
372
|
-
{
|
|
373
|
-
platform: "Google Play",
|
|
374
|
-
sizes: [
|
|
375
|
-
{ name: "1024x500", label: "Feature Graphic", width: 1024, height: 500 },
|
|
376
|
-
],
|
|
377
|
-
},
|
|
484
|
+
export: [
|
|
485
|
+
{ name: "1024x500", label: "Feature Graphic", size: { width: 1024, height: 500 } },
|
|
378
486
|
],
|
|
379
487
|
}),
|
|
380
488
|
dirs: [],
|
|
@@ -415,17 +523,17 @@ const ASSET_TYPES = {
|
|
|
415
523
|
// ── Main interactive flow ───────────────────────────────────────────────────
|
|
416
524
|
|
|
417
525
|
export async function scaffoldProject(dir) {
|
|
418
|
-
const
|
|
526
|
+
const configPath = join(dir, "assets.json");
|
|
419
527
|
|
|
420
|
-
if (existsSync(
|
|
421
|
-
console.error("Error:
|
|
528
|
+
if (existsSync(configPath)) {
|
|
529
|
+
console.error("Error: assets.json already exists in this directory.");
|
|
422
530
|
process.exit(1);
|
|
423
531
|
}
|
|
424
532
|
|
|
425
533
|
const rl = createRL();
|
|
426
534
|
|
|
427
535
|
console.log("\n Welcome to open-assets!\n");
|
|
428
|
-
console.log(" This will set up your asset project with
|
|
536
|
+
console.log(" This will set up your asset project with an assets.json");
|
|
429
537
|
console.log(" and starter HTML templates.\n");
|
|
430
538
|
|
|
431
539
|
// 1. Project name
|
|
@@ -529,11 +637,11 @@ export async function scaffoldProject(dir) {
|
|
|
529
637
|
collections,
|
|
530
638
|
};
|
|
531
639
|
|
|
532
|
-
writeFileSync(
|
|
533
|
-
console.log(" Created
|
|
640
|
+
writeFileSync(configPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
641
|
+
console.log(" Created assets.json");
|
|
534
642
|
|
|
535
643
|
// Write .gitignore
|
|
536
|
-
writeFileSync(join(dir, ".gitignore"), "dist/\nexports/\
|
|
644
|
+
writeFileSync(join(dir, ".gitignore"), "dist/\nexports/\nassets.lock\nnode_modules/\n");
|
|
537
645
|
console.log(" Created .gitignore");
|
|
538
646
|
|
|
539
647
|
console.log();
|
package/lib/list.mjs
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { flatSizes } from "./manifest.mjs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Filter collections by tag. Returns filtered array.
|
|
5
|
+
* @param {object} manifest
|
|
6
|
+
* @param {{ tag?: string }} [options]
|
|
7
|
+
* @returns {{ collections: object[], error?: string }}
|
|
8
|
+
*/
|
|
9
|
+
export function listCollections(manifest, options = {}) {
|
|
10
|
+
let cols = manifest.collections || [];
|
|
11
|
+
if (options.tag) {
|
|
12
|
+
cols = cols.filter((c) => (c.tags || []).includes(options.tag));
|
|
13
|
+
if (cols.length === 0) {
|
|
14
|
+
return { collections: [], error: `No collections found with tag "${options.tag}"` };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return { collections: cols };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Format a manifest's collections into a human-readable string.
|
|
22
|
+
* @param {object} manifest
|
|
23
|
+
* @param {object[]} collections
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function formatCollectionList(manifest, collections) {
|
|
27
|
+
const lines = [];
|
|
28
|
+
|
|
29
|
+
if (manifest.tags && manifest.tags.length > 0) {
|
|
30
|
+
lines.push(`Tags: ${manifest.tags.map((t) => t.id).join(", ")}`);
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
lines.push(manifest.name);
|
|
35
|
+
lines.push("");
|
|
36
|
+
|
|
37
|
+
for (const col of collections) {
|
|
38
|
+
const templateCount = col.templates.length;
|
|
39
|
+
const sizeCount = flatSizes(col).length;
|
|
40
|
+
const tagStr =
|
|
41
|
+
col.tags && col.tags.length > 0 ? ` [${col.tags.join(", ")}]` : "";
|
|
42
|
+
lines.push(
|
|
43
|
+
`${col.label} (${col.id})${tagStr} \u2014 ${templateCount} template(s), ${sizeCount} size(s)`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
for (const template of col.templates) {
|
|
47
|
+
lines.push(` \u2022 ${template.label || template.name} \u2192 ${template.src}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (const entry of col.export || []) {
|
|
51
|
+
lines.push(` \u21b3 ${entry.label} (${entry.size.width}\u00d7${entry.size.height})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (col.outputs) {
|
|
55
|
+
for (const output of col.outputs) {
|
|
56
|
+
if (output.type === "xcode") {
|
|
57
|
+
lines.push(` \u21b3 Xcode: ${output.path}`);
|
|
58
|
+
} else if (output.type === "copy-source") {
|
|
59
|
+
lines.push(` \u21b3 Copy source: ${output.format}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return lines.join("\n");
|
|
66
|
+
}
|
package/lib/lockfile.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
3
|
import { resolve, join } from "path";
|
|
4
4
|
|
|
5
|
-
const LOCK_FILENAME = "
|
|
5
|
+
const LOCK_FILENAME = "assets.lock";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Read the lockfile from disk, or return an empty structure.
|
package/lib/manifest.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
+
import { resolve } from "path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Get all export sizes from a collection.
|
|
6
|
+
*/
|
|
7
|
+
export function flatSizes(collection) {
|
|
8
|
+
return collection.export || [];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load and parse a config file from disk.
|
|
13
|
+
* @param {string} projectDir - Absolute path to the project directory
|
|
14
|
+
* @param {string} [configName="assets.json"] - Config filename
|
|
15
|
+
* @returns {object} Parsed config
|
|
16
|
+
* @throws {Error} If the file is missing or contains invalid JSON
|
|
17
|
+
*/
|
|
18
|
+
export function loadConfig(projectDir, configName = "assets.json") {
|
|
19
|
+
const configPath = resolve(projectDir, configName);
|
|
20
|
+
if (!existsSync(configPath)) {
|
|
21
|
+
throw new Error(`${configName} not found in ${projectDir}`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(readFileSync(configPath, "utf-8"));
|
|
25
|
+
} catch (err) {
|
|
26
|
+
throw new Error(`Invalid JSON in ${configName}: ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save a config to disk.
|
|
32
|
+
* @param {string} projectDir - Absolute path to the project directory
|
|
33
|
+
* @param {string} configName - Config filename
|
|
34
|
+
* @param {object} config - The config object to save
|
|
35
|
+
*/
|
|
36
|
+
export function saveConfig(projectDir, configName, config) {
|
|
37
|
+
const configPath = resolve(projectDir, configName);
|
|
38
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Filter collections by ID and/or tag.
|
|
43
|
+
* @param {object[]} collections
|
|
44
|
+
* @param {{ collection?: string, tag?: string }} filters
|
|
45
|
+
* @returns {{ collections: object[], error?: string }}
|
|
46
|
+
*/
|
|
47
|
+
export function filterCollections(collections, filters = {}) {
|
|
48
|
+
let result = collections;
|
|
49
|
+
if (filters.collection) {
|
|
50
|
+
result = result.filter((c) => c.id === filters.collection);
|
|
51
|
+
}
|
|
52
|
+
if (filters.tag) {
|
|
53
|
+
result = result.filter((c) => (c.tags || []).includes(filters.tag));
|
|
54
|
+
}
|
|
55
|
+
if (result.length === 0) {
|
|
56
|
+
const filter = filters.collection
|
|
57
|
+
? `collection "${filters.collection}"`
|
|
58
|
+
: `tag "${filters.tag}"`;
|
|
59
|
+
return { collections: [], error: `No collections matched ${filter} in config` };
|
|
60
|
+
}
|
|
61
|
+
return { collections: result };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Filter templates by name or label.
|
|
66
|
+
* @param {object[]} templates
|
|
67
|
+
* @param {string} [templateName] - If provided, filter to matching name/label
|
|
68
|
+
* @returns {object[]}
|
|
69
|
+
*/
|
|
70
|
+
export function filterTemplates(templates, templateName) {
|
|
71
|
+
if (!templateName) return templates;
|
|
72
|
+
return templates.filter(
|
|
73
|
+
(t) => t.name === templateName || t.label === templateName
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Determine which sizes to render for a collection given CLI options.
|
|
79
|
+
* @param {object} collection - The collection from the config
|
|
80
|
+
* @param {{ width?: string|number, height?: string|number, size?: string, all?: boolean, platform?: string }} opts
|
|
81
|
+
* @returns {{ sizes: object[], warning?: string }}
|
|
82
|
+
*/
|
|
83
|
+
export function resolveRenderSizes(collection, opts = {}) {
|
|
84
|
+
const sourceW = collection.sourceSize.width;
|
|
85
|
+
const sourceH = collection.sourceSize.height;
|
|
86
|
+
|
|
87
|
+
if (opts.width && opts.height) {
|
|
88
|
+
const w = typeof opts.width === "number" ? opts.width : parseInt(opts.width);
|
|
89
|
+
const h = typeof opts.height === "number" ? opts.height : parseInt(opts.height);
|
|
90
|
+
return {
|
|
91
|
+
sizes: [{
|
|
92
|
+
name: `${w}x${h}`,
|
|
93
|
+
label: `${w}x${h}`,
|
|
94
|
+
size: { width: w, height: h },
|
|
95
|
+
}],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (opts.size) {
|
|
100
|
+
const found = flatSizes(collection).find(
|
|
101
|
+
(s) => s.name === opts.size || s.label === opts.size
|
|
102
|
+
);
|
|
103
|
+
if (found) {
|
|
104
|
+
return { sizes: [found] };
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
sizes: [],
|
|
108
|
+
warning: `size "${opts.size}" not found in collection "${collection.id}", skipping`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (opts.all) {
|
|
113
|
+
const sizes = flatSizes(collection);
|
|
114
|
+
return { sizes };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Default: render at source size
|
|
118
|
+
return {
|
|
119
|
+
sizes: [{
|
|
120
|
+
name: `${sourceW}x${sourceH}`,
|
|
121
|
+
label: `${sourceW}x${sourceH}`,
|
|
122
|
+
size: { width: sourceW, height: sourceH },
|
|
123
|
+
}],
|
|
124
|
+
};
|
|
125
|
+
}
|