resuml 1.5.1 → 1.6.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/package.json +1 -1
- package/scripts/bundle-themes.js +278 -35
package/package.json
CHANGED
package/scripts/bundle-themes.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme Bundling Script
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Bundles jsonresume-theme packages for the browser with:
|
|
5
|
+
* - CSS extraction (separate .css files)
|
|
6
|
+
* - Build-time snapshots (pre-rendered HTML for instant preview)
|
|
7
|
+
* - Per-theme fs shim with embedded assets
|
|
6
8
|
*
|
|
7
9
|
* Output:
|
|
8
|
-
* docs/themes/{name}.js
|
|
9
|
-
* docs/themes/
|
|
10
|
+
* docs/themes/{name}.js — bundled render module
|
|
11
|
+
* docs/themes/{name}.css — extracted CSS (if any)
|
|
12
|
+
* docs/themes/{name}.snapshot.html— pre-rendered HTML with sample data
|
|
13
|
+
* docs/themes/manifest.json — theme metadata registry
|
|
10
14
|
*
|
|
11
15
|
* Usage:
|
|
12
16
|
* node scripts/bundle-themes.js # Bundle popular themes
|
|
@@ -15,12 +19,14 @@
|
|
|
15
19
|
*/
|
|
16
20
|
|
|
17
21
|
import { build } from 'esbuild';
|
|
18
|
-
import { resolve, dirname } from 'path';
|
|
22
|
+
import { resolve, dirname, join } from 'path';
|
|
19
23
|
import { fileURLToPath } from 'url';
|
|
20
24
|
import { execSync } from 'child_process';
|
|
21
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
|
|
25
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync } from 'fs';
|
|
26
|
+
import { createRequire } from 'module';
|
|
22
27
|
|
|
23
28
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const require = createRequire(import.meta.url);
|
|
24
30
|
const THEMES_DIR = resolve(__dirname, '../docs/themes');
|
|
25
31
|
|
|
26
32
|
// Popular themes that are known to work in the browser
|
|
@@ -43,6 +49,214 @@ const POPULAR_THEMES = [
|
|
|
43
49
|
'compact',
|
|
44
50
|
];
|
|
45
51
|
|
|
52
|
+
// Sample resume for generating snapshots
|
|
53
|
+
const SAMPLE_RESUME = {
|
|
54
|
+
basics: {
|
|
55
|
+
name: 'Jane Smith',
|
|
56
|
+
label: 'Senior Software Engineer',
|
|
57
|
+
email: 'jane@example.com',
|
|
58
|
+
phone: '+1-555-987-6543',
|
|
59
|
+
url: 'https://janesmith.dev',
|
|
60
|
+
summary: 'Full-stack engineer with 8+ years of experience building scalable web applications. Passionate about clean architecture, performance optimization, and mentoring teams.',
|
|
61
|
+
location: { city: 'San Francisco', countryCode: 'US', region: 'California' },
|
|
62
|
+
profiles: [
|
|
63
|
+
{ network: 'LinkedIn', username: 'janesmith', url: 'https://linkedin.com/in/janesmith' },
|
|
64
|
+
{ network: 'GitHub', username: 'janesmith', url: 'https://github.com/janesmith' },
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
work: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Tech Corp',
|
|
70
|
+
position: 'Senior Software Engineer',
|
|
71
|
+
url: 'https://techcorp.com',
|
|
72
|
+
startDate: '2020-03-01',
|
|
73
|
+
summary: 'Lead engineer for the platform team, owning core API infrastructure.',
|
|
74
|
+
highlights: [
|
|
75
|
+
'Reduced API latency by 45% through caching and query optimization',
|
|
76
|
+
'Designed microservices architecture serving 2M daily requests',
|
|
77
|
+
'Mentored 4 junior engineers through technical growth plans',
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'StartupXYZ',
|
|
82
|
+
position: 'Full Stack Developer',
|
|
83
|
+
startDate: '2017-06-01',
|
|
84
|
+
endDate: '2020-02-28',
|
|
85
|
+
summary: 'Built customer-facing web applications from prototype to production.',
|
|
86
|
+
highlights: [
|
|
87
|
+
'Built real-time dashboard used by 15,000+ daily active users',
|
|
88
|
+
'Implemented CI/CD pipeline reducing deploy time from 2 hours to 8 minutes',
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
education: [
|
|
93
|
+
{
|
|
94
|
+
institution: 'University of Technology',
|
|
95
|
+
area: 'Computer Science',
|
|
96
|
+
studyType: 'Bachelor of Science',
|
|
97
|
+
startDate: '2013-09-01',
|
|
98
|
+
endDate: '2017-05-15',
|
|
99
|
+
score: '3.8',
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
skills: [
|
|
103
|
+
{ name: 'Languages', level: 'Expert', keywords: ['TypeScript', 'JavaScript', 'Python', 'Go'] },
|
|
104
|
+
{ name: 'Frontend', level: 'Expert', keywords: ['React', 'Next.js', 'HTML/CSS', 'Tailwind'] },
|
|
105
|
+
{ name: 'Backend & Cloud', level: 'Advanced', keywords: ['Node.js', 'PostgreSQL', 'AWS', 'Docker'] },
|
|
106
|
+
],
|
|
107
|
+
projects: [
|
|
108
|
+
{
|
|
109
|
+
name: 'Open Source CLI Tool',
|
|
110
|
+
description: 'Developer productivity tool with 2,000+ GitHub stars',
|
|
111
|
+
url: 'https://github.com/janesmith/cli-tool',
|
|
112
|
+
startDate: '2022-01-01',
|
|
113
|
+
highlights: ['Published to npm with 5,000+ weekly downloads'],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
// Provide empty arrays for sections themes might iterate over
|
|
117
|
+
volunteer: [],
|
|
118
|
+
awards: [],
|
|
119
|
+
certificates: [],
|
|
120
|
+
publications: [],
|
|
121
|
+
languages: [],
|
|
122
|
+
interests: [],
|
|
123
|
+
references: [],
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// ── Theme file collection ────────────────────────────────────────────
|
|
127
|
+
|
|
128
|
+
/** Recursively collect text files from a theme directory for the fs shim. */
|
|
129
|
+
function collectThemeFiles(themeDir) {
|
|
130
|
+
const files = {};
|
|
131
|
+
const dirs = {};
|
|
132
|
+
|
|
133
|
+
function walk(dir, relPrefix = '') {
|
|
134
|
+
let entries;
|
|
135
|
+
try { entries = readdirSync(dir, { withFileTypes: true }); }
|
|
136
|
+
catch { return; }
|
|
137
|
+
|
|
138
|
+
const childNames = [];
|
|
139
|
+
for (const entry of entries) {
|
|
140
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
|
|
141
|
+
childNames.push(entry.name);
|
|
142
|
+
const full = join(dir, entry.name);
|
|
143
|
+
const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
|
|
144
|
+
|
|
145
|
+
if (entry.isDirectory()) {
|
|
146
|
+
walk(full, rel);
|
|
147
|
+
} else {
|
|
148
|
+
const ext = (entry.name.split('.').pop() || '').toLowerCase();
|
|
149
|
+
if (['css', 'hbs', 'html', 'json', 'txt', 'handlebars', 'mustache', 'template'].includes(ext)) {
|
|
150
|
+
try { files[rel] = readFileSync(full, 'utf-8'); }
|
|
151
|
+
catch { /* skip */ }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
dirs[relPrefix || '.'] = childNames;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
walk(themeDir);
|
|
159
|
+
return { files, dirs };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Generate an fs shim that embeds the theme's files. */
|
|
163
|
+
function generateThemeFsShim(themeFiles) {
|
|
164
|
+
const { files, dirs } = themeFiles;
|
|
165
|
+
const lines = [
|
|
166
|
+
`const __files = ${JSON.stringify(files)};`,
|
|
167
|
+
`const __dirs = ${JSON.stringify(dirs)};`,
|
|
168
|
+
'',
|
|
169
|
+
'function normalizePath(p) {',
|
|
170
|
+
' return p.replace(/[\\\\]+/g, "/").replace(/^\\/+/, "");',
|
|
171
|
+
'}',
|
|
172
|
+
'',
|
|
173
|
+
'function matchFile(p) {',
|
|
174
|
+
' var clean = normalizePath(p);',
|
|
175
|
+
' if (__files[clean] !== undefined) return __files[clean];',
|
|
176
|
+
' var keys = Object.keys(__files);',
|
|
177
|
+
' for (var i = 0; i < keys.length; i++) {',
|
|
178
|
+
' if (clean.endsWith("/" + keys[i]) || clean.endsWith(keys[i])) return __files[keys[i]];',
|
|
179
|
+
' }',
|
|
180
|
+
' return undefined;',
|
|
181
|
+
'}',
|
|
182
|
+
'',
|
|
183
|
+
'function matchDir(p) {',
|
|
184
|
+
' var clean = normalizePath(p);',
|
|
185
|
+
' if (__dirs[clean] !== undefined) return __dirs[clean];',
|
|
186
|
+
' var keys = Object.keys(__dirs);',
|
|
187
|
+
' for (var i = 0; i < keys.length; i++) {',
|
|
188
|
+
' if (clean.endsWith("/" + keys[i]) || clean.endsWith(keys[i])) return __dirs[keys[i]];',
|
|
189
|
+
' }',
|
|
190
|
+
' return undefined;',
|
|
191
|
+
'}',
|
|
192
|
+
'',
|
|
193
|
+
'export var readFileSync = function(p) { var r = matchFile(p); return r !== undefined ? r : ""; };',
|
|
194
|
+
'export var readdirSync = function(p) { var r = matchDir(p); return r !== undefined ? r : []; };',
|
|
195
|
+
'export var existsSync = function(p) { return matchFile(p) !== undefined || matchDir(p) !== undefined; };',
|
|
196
|
+
'export var writeFileSync = function() {};',
|
|
197
|
+
'export var mkdirSync = function() {};',
|
|
198
|
+
'export default { readFileSync: readFileSync, readdirSync: readdirSync, existsSync: existsSync, writeFileSync: writeFileSync, mkdirSync: mkdirSync };',
|
|
199
|
+
];
|
|
200
|
+
return lines.join('\n');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ── CSS extraction ───────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
/** Extract <style> blocks from rendered HTML and write to a .css file. */
|
|
206
|
+
function extractCss(html, shortName) {
|
|
207
|
+
const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
|
|
208
|
+
let css = '';
|
|
209
|
+
let match;
|
|
210
|
+
while ((match = styleRegex.exec(html)) !== null) {
|
|
211
|
+
css += match[1] + '\n';
|
|
212
|
+
}
|
|
213
|
+
if (css.trim()) {
|
|
214
|
+
writeFileSync(resolve(THEMES_DIR, `${shortName}.css`), css.trim());
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Snapshot generation ──────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/** Render a theme with sample data in Node.js and save as snapshot. */
|
|
223
|
+
function generateSnapshot(shortName, packageName) {
|
|
224
|
+
try {
|
|
225
|
+
// Clear require cache to get a fresh instance
|
|
226
|
+
const pkgPath = require.resolve(packageName);
|
|
227
|
+
delete require.cache[pkgPath];
|
|
228
|
+
|
|
229
|
+
const theme = require(packageName);
|
|
230
|
+
const render = theme.render || (theme.default && theme.default.render);
|
|
231
|
+
if (typeof render !== 'function') return null;
|
|
232
|
+
|
|
233
|
+
const result = render(SAMPLE_RESUME);
|
|
234
|
+
// render() may return a string or a Promise
|
|
235
|
+
if (typeof result === 'string') {
|
|
236
|
+
writeFileSync(resolve(THEMES_DIR, `${shortName}.snapshot.html`), result);
|
|
237
|
+
extractCss(result, shortName);
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
// If it's a promise, we need to await it
|
|
241
|
+
if (result && typeof result.then === 'function') {
|
|
242
|
+
return result.then((html) => {
|
|
243
|
+
if (typeof html === 'string') {
|
|
244
|
+
writeFileSync(resolve(THEMES_DIR, `${shortName}.snapshot.html`), html);
|
|
245
|
+
extractCss(html, shortName);
|
|
246
|
+
return html;
|
|
247
|
+
}
|
|
248
|
+
return null;
|
|
249
|
+
}).catch(() => null);
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
} catch (e) {
|
|
253
|
+
console.log(` (snapshot failed: ${e.message})`);
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ── npm discovery ────────────────────────────────────────────────────
|
|
259
|
+
|
|
46
260
|
async function discoverThemes() {
|
|
47
261
|
const themes = [];
|
|
48
262
|
let from = 0;
|
|
@@ -50,7 +264,6 @@ async function discoverThemes() {
|
|
|
50
264
|
|
|
51
265
|
console.log('🔍 Discovering themes from npm...');
|
|
52
266
|
|
|
53
|
-
// npm search API has pagination, fetch up to 1000
|
|
54
267
|
for (let page = 0; page < 4; page++) {
|
|
55
268
|
const url = `https://registry.npmjs.org/-/v1/search?text=jsonresume-theme&size=${size}&from=${from}`;
|
|
56
269
|
const res = await fetch(url);
|
|
@@ -61,8 +274,6 @@ async function discoverThemes() {
|
|
|
61
274
|
const name = pkg.package.name;
|
|
62
275
|
if (!name.startsWith('jsonresume-theme-')) continue;
|
|
63
276
|
const shortName = name.replace('jsonresume-theme-', '');
|
|
64
|
-
|
|
65
|
-
// Skip themes with 0 downloads or very old
|
|
66
277
|
if (pkg.score?.detail?.popularity === 0) continue;
|
|
67
278
|
|
|
68
279
|
themes.push({
|
|
@@ -81,7 +292,15 @@ async function discoverThemes() {
|
|
|
81
292
|
return themes;
|
|
82
293
|
}
|
|
83
294
|
|
|
84
|
-
|
|
295
|
+
// ── esbuild bundling ─────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
async function bundleTheme(shortName, packageName, shimsDir) {
|
|
298
|
+
const themeDir = resolve(__dirname, `../node_modules/${packageName}`);
|
|
299
|
+
|
|
300
|
+
// Generate per-theme fs shim with embedded files
|
|
301
|
+
const themeFiles = collectThemeFiles(themeDir);
|
|
302
|
+
writeFileSync(resolve(shimsDir, 'fs.js'), generateThemeFsShim(themeFiles));
|
|
303
|
+
|
|
85
304
|
const entryContent = `
|
|
86
305
|
import * as themeNs from '${packageName}';
|
|
87
306
|
const _t = themeNs.default ?? themeNs;
|
|
@@ -100,12 +319,7 @@ async function bundleTheme(shortName, packageName) {
|
|
|
100
319
|
format: 'esm',
|
|
101
320
|
target: 'es2022',
|
|
102
321
|
platform: 'browser',
|
|
103
|
-
// Use 'require' condition so packages like underscore/lodash resolve to their
|
|
104
|
-
// CJS/UMD builds (which export a callable function) instead of their ESM builds
|
|
105
|
-
// (which export a namespace object that breaks _(collection) call syntax).
|
|
106
322
|
conditions: ['browser', 'require', 'default'],
|
|
107
|
-
// Prefer the CJS 'main' field over the ESM 'module' field for packages
|
|
108
|
-
// that don't use the exports map (older packages).
|
|
109
323
|
mainFields: ['browser', 'main'],
|
|
110
324
|
outfile: resolve(THEMES_DIR, `${shortName}.js`),
|
|
111
325
|
define: {
|
|
@@ -117,27 +331,28 @@ async function bundleTheme(shortName, packageName) {
|
|
|
117
331
|
'process.platform': '"browser"',
|
|
118
332
|
'process.version': '"v18.0.0"',
|
|
119
333
|
},
|
|
120
|
-
// Polyfill Node.js built-ins as no-ops for browser
|
|
121
334
|
alias: {
|
|
122
|
-
'path': resolve(
|
|
123
|
-
'fs': resolve(
|
|
124
|
-
'url': resolve(
|
|
125
|
-
'node:url': resolve(
|
|
126
|
-
'node:crypto': resolve(
|
|
127
|
-
'assert': resolve(
|
|
335
|
+
'path': resolve(shimsDir, 'path.js'),
|
|
336
|
+
'fs': resolve(shimsDir, 'fs.js'),
|
|
337
|
+
'url': resolve(shimsDir, 'url.js'),
|
|
338
|
+
'node:url': resolve(shimsDir, 'url.js'),
|
|
339
|
+
'node:crypto': resolve(shimsDir, 'crypto.js'),
|
|
340
|
+
'assert': resolve(shimsDir, 'assert.js'),
|
|
128
341
|
},
|
|
129
342
|
logLevel: 'silent',
|
|
130
343
|
});
|
|
131
344
|
|
|
132
|
-
// Clean up entry file
|
|
133
345
|
execSync(`rm -f "${entryFile}"`);
|
|
134
346
|
return true;
|
|
135
347
|
} catch (e) {
|
|
348
|
+
console.log(` (bundle error: ${e.message})`);
|
|
136
349
|
execSync(`rm -f "${entryFile}"`);
|
|
137
350
|
return false;
|
|
138
351
|
}
|
|
139
352
|
}
|
|
140
353
|
|
|
354
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
355
|
+
|
|
141
356
|
async function main() {
|
|
142
357
|
const args = process.argv.slice(2);
|
|
143
358
|
const doAll = args.includes('--all');
|
|
@@ -149,19 +364,17 @@ async function main() {
|
|
|
149
364
|
// Create shims directory
|
|
150
365
|
const shimsDir = resolve(__dirname, 'shims');
|
|
151
366
|
mkdirSync(shimsDir, { recursive: true });
|
|
367
|
+
|
|
152
368
|
writeFileSync(resolve(shimsDir, 'path.js'), `
|
|
153
369
|
export const join = (...parts) => parts.join('/');
|
|
154
370
|
export const resolve = (...parts) => parts.join('/');
|
|
155
371
|
export const dirname = (p) => p.split('/').slice(0, -1).join('/');
|
|
156
372
|
export const basename = (p) => p.split('/').pop();
|
|
157
373
|
export const extname = (p) => { const m = p.match(/\\.[^.]+$/); return m ? m[0] : ''; };
|
|
158
|
-
export
|
|
159
|
-
|
|
160
|
-
writeFileSync(resolve(shimsDir, 'fs.js'), `
|
|
161
|
-
export const readFileSync = () => '';
|
|
162
|
-
export const existsSync = () => false;
|
|
163
|
-
export default { readFileSync, existsSync };
|
|
374
|
+
export const sep = '/';
|
|
375
|
+
export default { join, resolve, dirname, basename, extname, sep };
|
|
164
376
|
`);
|
|
377
|
+
// fs shim is generated per-theme in bundleTheme()
|
|
165
378
|
writeFileSync(resolve(shimsDir, 'url.js'), `
|
|
166
379
|
export const URL = globalThis.URL;
|
|
167
380
|
export const URLSearchParams = globalThis.URLSearchParams;
|
|
@@ -225,7 +438,7 @@ async function main() {
|
|
|
225
438
|
continue;
|
|
226
439
|
}
|
|
227
440
|
|
|
228
|
-
//
|
|
441
|
+
// Read package metadata
|
|
229
442
|
try {
|
|
230
443
|
const pkgPath = resolve(__dirname, `../node_modules/${theme.packageName}/package.json`);
|
|
231
444
|
if (existsSync(pkgPath)) {
|
|
@@ -235,31 +448,60 @@ async function main() {
|
|
|
235
448
|
}
|
|
236
449
|
} catch {}
|
|
237
450
|
|
|
238
|
-
//
|
|
239
|
-
|
|
451
|
+
// Step 1: Generate snapshot (pre-render in Node.js)
|
|
452
|
+
let hasSnapshot = false;
|
|
453
|
+
let hasCss = false;
|
|
454
|
+
const snapshotResult = generateSnapshot(theme.name, theme.packageName);
|
|
455
|
+
// Handle promise if async render
|
|
456
|
+
if (snapshotResult && typeof snapshotResult.then === 'function') {
|
|
457
|
+
const html = await snapshotResult;
|
|
458
|
+
hasSnapshot = !!html;
|
|
459
|
+
hasCss = existsSync(resolve(THEMES_DIR, `${theme.name}.css`));
|
|
460
|
+
} else {
|
|
461
|
+
hasSnapshot = !!snapshotResult;
|
|
462
|
+
hasCss = existsSync(resolve(THEMES_DIR, `${theme.name}.css`));
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Step 2: Bundle for browser
|
|
466
|
+
const success = await bundleTheme(theme.name, theme.packageName, shimsDir);
|
|
240
467
|
|
|
241
468
|
if (success) {
|
|
242
469
|
const outFile = resolve(THEMES_DIR, `${theme.name}.js`);
|
|
243
470
|
const stat = existsSync(outFile) ? readFileSync(outFile).length : 0;
|
|
471
|
+
const cssFile = resolve(THEMES_DIR, `${theme.name}.css`);
|
|
472
|
+
const cssSize = hasCss && existsSync(cssFile) ? readFileSync(cssFile).length : 0;
|
|
473
|
+
|
|
244
474
|
manifest.push({
|
|
245
475
|
name: theme.name,
|
|
246
476
|
displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
|
|
247
477
|
description: theme.description,
|
|
248
478
|
version: theme.version || '',
|
|
249
479
|
browserCompatible: true,
|
|
480
|
+
hasSnapshot,
|
|
481
|
+
hasCss,
|
|
250
482
|
fileSize: stat,
|
|
483
|
+
cssSize,
|
|
251
484
|
});
|
|
252
|
-
|
|
485
|
+
|
|
486
|
+
const parts = [`✅ (${(stat / 1024).toFixed(0)}KB`];
|
|
487
|
+
if (hasCss) parts.push(`css:${(cssSize / 1024).toFixed(0)}KB`);
|
|
488
|
+
if (hasSnapshot) parts.push('snapshot');
|
|
489
|
+
console.log(parts.join(', ') + ')');
|
|
253
490
|
} else {
|
|
491
|
+
// Bundle failed — still useful if we have a snapshot (server fallback)
|
|
254
492
|
manifest.push({
|
|
255
493
|
name: theme.name,
|
|
256
494
|
displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
|
|
257
495
|
description: theme.description,
|
|
258
496
|
version: theme.version || '',
|
|
259
497
|
browserCompatible: false,
|
|
498
|
+
hasSnapshot,
|
|
499
|
+
hasCss: false,
|
|
260
500
|
fileSize: 0,
|
|
501
|
+
cssSize: 0,
|
|
261
502
|
});
|
|
262
|
-
|
|
503
|
+
const note = hasSnapshot ? '⚠️ browser incompatible (has snapshot)' : '⚠️ browser incompatible';
|
|
504
|
+
console.log(note);
|
|
263
505
|
}
|
|
264
506
|
}
|
|
265
507
|
|
|
@@ -267,7 +509,8 @@ async function main() {
|
|
|
267
509
|
writeFileSync(resolve(THEMES_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
268
510
|
|
|
269
511
|
const successful = manifest.filter(t => t.browserCompatible).length;
|
|
270
|
-
|
|
512
|
+
const snapshots = manifest.filter(t => t.hasSnapshot).length;
|
|
513
|
+
console.log(`\n✅ Done! ${successful}/${manifest.length} themes bundled, ${snapshots} snapshots generated`);
|
|
271
514
|
console.log(`📁 Output: docs/themes/`);
|
|
272
515
|
|
|
273
516
|
// Clean up shims
|