@open-assets/open-assets 0.1.11 → 0.2.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 +110 -120
- package/bin/open-assets.mjs +165 -319
- package/lib/add.mjs +476 -0
- package/lib/init.mjs +215 -107
- package/lib/list.mjs +64 -0
- package/lib/lockfile.mjs +53 -2
- package/lib/manifest.mjs +132 -0
- package/lib/render.mjs +219 -0
- package/lib/renderer.mjs +57 -7
- package/lib/server.mjs +58 -27
- package/lib/validate.mjs +93 -0
- package/package.json +7 -2
- package/public/img/banner/1280/banner.png +0 -0
- package/public/img/examples/cat-iq-test-icon.png +0 -0
- package/public/img/examples/cat-iq-test.png +0 -0
- package/public/img/examples/no-context-icon.png +0 -0
- package/public/img/examples/no-context.png +0 -0
- package/public/img/examples/only-recipes-icon.png +0 -0
- package/public/img/examples/only-recipes.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 +59 -56
- package/skills/open-assets/SKILL.md +121 -76
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/\nnode_modules/\n");
|
|
537
645
|
console.log(" Created .gitignore");
|
|
538
646
|
|
|
539
647
|
console.log();
|
package/lib/list.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { flatSizes, flatOutputs } 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 flatSizes(col)) {
|
|
51
|
+
lines.push(` \u21b3 ${entry.label} (${entry.size.width}\u00d7${entry.size.height})`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const output of flatOutputs(col)) {
|
|
55
|
+
if (output.type === "xcode") {
|
|
56
|
+
lines.push(` \u21b3 Xcode: ${output.path}`);
|
|
57
|
+
} else if (output.type === "copy-source") {
|
|
58
|
+
lines.push(` \u21b3 Copy source: ${output.format}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return lines.join("\n");
|
|
64
|
+
}
|
package/lib/lockfile.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
2
2
|
import { createHash } from "crypto";
|
|
3
|
-
import { resolve, join } from "path";
|
|
3
|
+
import { resolve, join, dirname } 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.
|
|
@@ -27,13 +27,64 @@ export function writeLockfile(projectDir, lockData) {
|
|
|
27
27
|
writeFileSync(lockPath, JSON.stringify(lockData, null, 2) + "\n");
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Extract local file dependencies referenced by an HTML file.
|
|
32
|
+
* Parses src="...", href="...", and url(...) references, filtering out
|
|
33
|
+
* remote URLs and returning only paths that exist on disk.
|
|
34
|
+
* Returns a sorted, deduplicated array of absolute paths.
|
|
35
|
+
*/
|
|
36
|
+
export function extractLocalDeps(filePath) {
|
|
37
|
+
if (!existsSync(filePath)) return [];
|
|
38
|
+
const content = readFileSync(filePath, "utf-8");
|
|
39
|
+
const dir = dirname(filePath);
|
|
40
|
+
|
|
41
|
+
const refs = new Set();
|
|
42
|
+
const srcHrefPattern = /(?:src|href)=["']([^"']+)["']/gi;
|
|
43
|
+
const cssUrlPattern = /url\(["']?([^"')]+)["']?\)/gi;
|
|
44
|
+
|
|
45
|
+
for (const pattern of [srcHrefPattern, cssUrlPattern]) {
|
|
46
|
+
let match;
|
|
47
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
48
|
+
refs.add(match[1]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const localPaths = [];
|
|
53
|
+
for (const ref of refs) {
|
|
54
|
+
if (/^(https?:\/\/|data:|\/\/|#)/.test(ref)) continue;
|
|
55
|
+
if (!ref) continue;
|
|
56
|
+
const abs = resolve(dir, ref);
|
|
57
|
+
if (existsSync(abs)) {
|
|
58
|
+
localPaths.push(abs);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [...new Set(localPaths)].sort();
|
|
63
|
+
}
|
|
64
|
+
|
|
30
65
|
/**
|
|
31
66
|
* Compute SHA256 checksum of a file's contents.
|
|
67
|
+
* For .html files, includes the content of all local dependencies
|
|
68
|
+
* (images, SVGs, etc.) referenced via src/href/url() attributes.
|
|
32
69
|
* Returns "sha256:<hex>" or null if file doesn't exist.
|
|
33
70
|
*/
|
|
34
71
|
export function computeChecksum(filePath) {
|
|
35
72
|
if (!existsSync(filePath)) return null;
|
|
36
73
|
const content = readFileSync(filePath);
|
|
74
|
+
|
|
75
|
+
if (filePath.endsWith(".html")) {
|
|
76
|
+
const deps = extractLocalDeps(filePath);
|
|
77
|
+
if (deps.length > 0) {
|
|
78
|
+
const hash = createHash("sha256");
|
|
79
|
+
hash.update(content);
|
|
80
|
+
for (const dep of deps) {
|
|
81
|
+
hash.update("\0");
|
|
82
|
+
hash.update(readFileSync(dep));
|
|
83
|
+
}
|
|
84
|
+
return `sha256:${hash.digest("hex")}`;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
37
88
|
const hash = createHash("sha256").update(content).digest("hex");
|
|
38
89
|
return `sha256:${hash}`;
|
|
39
90
|
}
|