resuml 1.5.2 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resuml",
3
- "version": "1.5.2",
3
+ "version": "1.6.0",
4
4
  "description": "Generate JSON resumes from YAML with theme support",
5
5
  "type": "module",
6
6
  "main": "./dist/api.js",
@@ -1,12 +1,16 @@
1
1
  /**
2
2
  * Theme Bundling Script
3
3
  *
4
- * Discovers jsonresume-theme packages from npm, installs them,
5
- * and bundles each into a browser-compatible ES module.
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 — bundled theme module
9
- * docs/themes/manifest.json theme metadata registry
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
@@ -19,8 +23,10 @@ import { resolve, dirname, join } from 'path';
19
23
  import { fileURLToPath } from 'url';
20
24
  import { execSync } from 'child_process';
21
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,45 +49,83 @@ const POPULAR_THEMES = [
43
49
  'compact',
44
50
  ];
45
51
 
46
- async function discoverThemes() {
47
- const themes = [];
48
- let from = 0;
49
- const size = 250;
50
-
51
- console.log('🔍 Discovering themes from npm...');
52
-
53
- // npm search API has pagination, fetch up to 1000
54
- for (let page = 0; page < 4; page++) {
55
- const url = `https://registry.npmjs.org/-/v1/search?text=jsonresume-theme&size=${size}&from=${from}`;
56
- const res = await fetch(url);
57
- if (!res.ok) break;
58
- const data = await res.json();
59
-
60
- for (const pkg of data.objects) {
61
- const name = pkg.package.name;
62
- if (!name.startsWith('jsonresume-theme-')) continue;
63
- const shortName = name.replace('jsonresume-theme-', '');
64
-
65
- // Skip themes with 0 downloads or very old
66
- if (pkg.score?.detail?.popularity === 0) continue;
67
-
68
- themes.push({
69
- name: shortName,
70
- packageName: name,
71
- description: pkg.package.description || '',
72
- version: pkg.package.version,
73
- });
74
- }
75
-
76
- if (data.objects.length < size) break;
77
- from += size;
78
- }
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
+ };
79
125
 
80
- console.log(` Found ${themes.length} themes`);
81
- return themes;
82
- }
126
+ // ── Theme file collection ────────────────────────────────────────────
83
127
 
84
- /** Recursively collect text files from a theme directory for embedding in the fs shim. */
128
+ /** Recursively collect text files from a theme directory for the fs shim. */
85
129
  function collectThemeFiles(themeDir) {
86
130
  const files = {};
87
131
  const dirs = {};
@@ -102,9 +146,9 @@ function collectThemeFiles(themeDir) {
102
146
  walk(full, rel);
103
147
  } else {
104
148
  const ext = (entry.name.split('.').pop() || '').toLowerCase();
105
- if (['css', 'hbs', 'html', 'json', 'txt', 'handlebars', 'mustache'].includes(ext)) {
149
+ if (['css', 'hbs', 'html', 'json', 'txt', 'handlebars', 'mustache', 'template'].includes(ext)) {
106
150
  try { files[rel] = readFileSync(full, 'utf-8'); }
107
- catch { /* skip unreadable */ }
151
+ catch { /* skip */ }
108
152
  }
109
153
  }
110
154
  }
@@ -115,53 +159,145 @@ function collectThemeFiles(themeDir) {
115
159
  return { files, dirs };
116
160
  }
117
161
 
118
- /** Generate an fs shim that embeds the theme's files so readFileSync/readdirSync work at runtime. */
162
+ /** Generate an fs shim that embeds the theme's files. */
119
163
  function generateThemeFsShim(themeFiles) {
120
164
  const { files, dirs } = themeFiles;
121
- return `
122
- const __files = ${JSON.stringify(files)};
123
- const __dirs = ${JSON.stringify(dirs)};
124
-
125
- function matchFile(path) {
126
- const clean = path.replace(/\\/+/g, '/').replace(/^\\/+/, '');
127
- if (__files[clean] !== undefined) return __files[clean];
128
- for (const key of Object.keys(__files)) {
129
- if (clean.endsWith('/' + key) || clean.endsWith(key)) return __files[key];
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;
130
216
  }
131
- return undefined;
217
+ return false;
132
218
  }
133
219
 
134
- function matchDir(path) {
135
- const clean = path.replace(/\\/+/g, '/').replace(/^\\/+/, '');
136
- if (__dirs[clean] !== undefined) return __dirs[clean];
137
- for (const key of Object.keys(__dirs)) {
138
- if (clean.endsWith('/' + key) || clean.endsWith(key)) return __dirs[key];
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;
139
255
  }
140
- return undefined;
141
256
  }
142
257
 
143
- export const readFileSync = (path, encoding) => {
144
- const r = matchFile(path);
145
- return r !== undefined ? r : '';
146
- };
258
+ // ── npm discovery ────────────────────────────────────────────────────
147
259
 
148
- export const readdirSync = (path) => {
149
- const r = matchDir(path);
150
- return r !== undefined ? r : [];
151
- };
260
+ async function discoverThemes() {
261
+ const themes = [];
262
+ let from = 0;
263
+ const size = 250;
152
264
 
153
- export const existsSync = (path) => {
154
- return matchFile(path) !== undefined || matchDir(path) !== undefined;
155
- };
265
+ console.log('🔍 Discovering themes from npm...');
266
+
267
+ for (let page = 0; page < 4; page++) {
268
+ const url = `https://registry.npmjs.org/-/v1/search?text=jsonresume-theme&size=${size}&from=${from}`;
269
+ const res = await fetch(url);
270
+ if (!res.ok) break;
271
+ const data = await res.json();
272
+
273
+ for (const pkg of data.objects) {
274
+ const name = pkg.package.name;
275
+ if (!name.startsWith('jsonresume-theme-')) continue;
276
+ const shortName = name.replace('jsonresume-theme-', '');
277
+ if (pkg.score?.detail?.popularity === 0) continue;
278
+
279
+ themes.push({
280
+ name: shortName,
281
+ packageName: name,
282
+ description: pkg.package.description || '',
283
+ version: pkg.package.version,
284
+ });
285
+ }
286
+
287
+ if (data.objects.length < size) break;
288
+ from += size;
289
+ }
156
290
 
157
- export default { readFileSync, readdirSync, existsSync };
158
- `;
291
+ console.log(` Found ${themes.length} themes`);
292
+ return themes;
159
293
  }
160
294
 
295
+ // ── esbuild bundling ─────────────────────────────────────────────────
296
+
161
297
  async function bundleTheme(shortName, packageName, shimsDir) {
162
- // Generate a theme-specific fs shim with embedded file contents so that
163
- // readFileSync / readdirSync return real CSS, templates, and partials at runtime.
164
298
  const themeDir = resolve(__dirname, `../node_modules/${packageName}`);
299
+
300
+ // Generate per-theme fs shim with embedded files
165
301
  const themeFiles = collectThemeFiles(themeDir);
166
302
  writeFileSync(resolve(shimsDir, 'fs.js'), generateThemeFsShim(themeFiles));
167
303
 
@@ -183,12 +319,7 @@ async function bundleTheme(shortName, packageName, shimsDir) {
183
319
  format: 'esm',
184
320
  target: 'es2022',
185
321
  platform: 'browser',
186
- // Use 'require' condition so packages like underscore/lodash resolve to their
187
- // CJS/UMD builds (which export a callable function) instead of their ESM builds
188
- // (which export a namespace object that breaks _(collection) call syntax).
189
322
  conditions: ['browser', 'require', 'default'],
190
- // Prefer the CJS 'main' field over the ESM 'module' field for packages
191
- // that don't use the exports map (older packages).
192
323
  mainFields: ['browser', 'main'],
193
324
  outfile: resolve(THEMES_DIR, `${shortName}.js`),
194
325
  define: {
@@ -200,27 +331,28 @@ async function bundleTheme(shortName, packageName, shimsDir) {
200
331
  'process.platform': '"browser"',
201
332
  'process.version': '"v18.0.0"',
202
333
  },
203
- // Polyfill Node.js built-ins as no-ops for browser
204
334
  alias: {
205
- 'path': resolve(__dirname, 'shims/path.js'),
206
- 'fs': resolve(__dirname, 'shims/fs.js'),
207
- 'url': resolve(__dirname, 'shims/url.js'),
208
- 'node:url': resolve(__dirname, 'shims/url.js'),
209
- 'node:crypto': resolve(__dirname, 'shims/crypto.js'),
210
- 'assert': resolve(__dirname, 'shims/assert.js'),
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'),
211
341
  },
212
342
  logLevel: 'silent',
213
343
  });
214
344
 
215
- // Clean up entry file
216
345
  execSync(`rm -f "${entryFile}"`);
217
346
  return true;
218
347
  } catch (e) {
348
+ console.log(` (bundle error: ${e.message})`);
219
349
  execSync(`rm -f "${entryFile}"`);
220
350
  return false;
221
351
  }
222
352
  }
223
353
 
354
+ // ── Main ─────────────────────────────────────────────────────────────
355
+
224
356
  async function main() {
225
357
  const args = process.argv.slice(2);
226
358
  const doAll = args.includes('--all');
@@ -232,15 +364,17 @@ async function main() {
232
364
  // Create shims directory
233
365
  const shimsDir = resolve(__dirname, 'shims');
234
366
  mkdirSync(shimsDir, { recursive: true });
367
+
235
368
  writeFileSync(resolve(shimsDir, 'path.js'), `
236
369
  export const join = (...parts) => parts.join('/');
237
370
  export const resolve = (...parts) => parts.join('/');
238
371
  export const dirname = (p) => p.split('/').slice(0, -1).join('/');
239
372
  export const basename = (p) => p.split('/').pop();
240
373
  export const extname = (p) => { const m = p.match(/\\.[^.]+$/); return m ? m[0] : ''; };
241
- export default { join, resolve, dirname, basename, extname };
374
+ export const sep = '/';
375
+ export default { join, resolve, dirname, basename, extname, sep };
242
376
  `);
243
- // fs shim is generated per-theme in bundleTheme() with embedded file contents
377
+ // fs shim is generated per-theme in bundleTheme()
244
378
  writeFileSync(resolve(shimsDir, 'url.js'), `
245
379
  export const URL = globalThis.URL;
246
380
  export const URLSearchParams = globalThis.URLSearchParams;
@@ -304,7 +438,7 @@ async function main() {
304
438
  continue;
305
439
  }
306
440
 
307
- // Try to get description from installed package
441
+ // Read package metadata
308
442
  try {
309
443
  const pkgPath = resolve(__dirname, `../node_modules/${theme.packageName}/package.json`);
310
444
  if (existsSync(pkgPath)) {
@@ -314,31 +448,60 @@ async function main() {
314
448
  }
315
449
  } catch {}
316
450
 
317
- // Bundle it
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
318
466
  const success = await bundleTheme(theme.name, theme.packageName, shimsDir);
319
467
 
320
468
  if (success) {
321
469
  const outFile = resolve(THEMES_DIR, `${theme.name}.js`);
322
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
+
323
474
  manifest.push({
324
475
  name: theme.name,
325
476
  displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
326
477
  description: theme.description,
327
478
  version: theme.version || '',
328
479
  browserCompatible: true,
480
+ hasSnapshot,
481
+ hasCss,
329
482
  fileSize: stat,
483
+ cssSize,
330
484
  });
331
- console.log(`✅ (${(stat / 1024).toFixed(0)}KB)`);
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(', ') + ')');
332
490
  } else {
491
+ // Bundle failed — still useful if we have a snapshot (server fallback)
333
492
  manifest.push({
334
493
  name: theme.name,
335
494
  displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
336
495
  description: theme.description,
337
496
  version: theme.version || '',
338
497
  browserCompatible: false,
498
+ hasSnapshot,
499
+ hasCss: false,
339
500
  fileSize: 0,
501
+ cssSize: 0,
340
502
  });
341
- console.log('⚠️ browser incompatible');
503
+ const note = hasSnapshot ? '⚠️ browser incompatible (has snapshot)' : '⚠️ browser incompatible';
504
+ console.log(note);
342
505
  }
343
506
  }
344
507
 
@@ -346,7 +509,8 @@ async function main() {
346
509
  writeFileSync(resolve(THEMES_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
347
510
 
348
511
  const successful = manifest.filter(t => t.browserCompatible).length;
349
- console.log(`\n✅ Done! ${successful}/${manifest.length} themes bundled for the browser`);
512
+ const snapshots = manifest.filter(t => t.hasSnapshot).length;
513
+ console.log(`\n✅ Done! ${successful}/${manifest.length} themes bundled, ${snapshots} snapshots generated`);
350
514
  console.log(`📁 Output: docs/themes/`);
351
515
 
352
516
  // Clean up shims