resuml 1.6.0 → 1.7.1

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.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "Generate JSON resumes from YAML with theme support",
5
5
  "type": "module",
6
6
  "main": "./dist/api.js",
@@ -28,6 +28,8 @@
28
28
  "dev:builder": "node scripts/dev-server.js",
29
29
  "prepublishOnly": "npm run generate:types && npm run build:lib",
30
30
  "generate:types": "node scripts/generate-types.cjs",
31
+ "bundle:themes": "node scripts/bundle-themes.js",
32
+ "bundle:themes:all": "node scripts/bundle-themes.js --all",
31
33
  "test": "vitest run",
32
34
  "test:watch": "vitest",
33
35
  "lint": "eslint src api --ext .ts,.tsx",
@@ -2,9 +2,10 @@
2
2
  * Theme Bundling Script
3
3
  *
4
4
  * Bundles jsonresume-theme packages for the browser with:
5
+ * - Per-theme fs shim with embedded assets (CSS, templates, etc.)
5
6
  * - CSS extraction (separate .css files)
6
7
  * - Build-time snapshots (pre-rendered HTML for instant preview)
7
- * - Per-theme fs shim with embedded assets
8
+ * - Node.js built-in shims (buffer, stream, util, events, os, etc.)
8
9
  *
9
10
  * Output:
10
11
  * docs/themes/{name}.js — bundled render module
@@ -49,16 +50,23 @@ const POPULAR_THEMES = [
49
50
  'compact',
50
51
  ];
51
52
 
53
+ // File extensions to embed in the fs shim
54
+ const TEXT_EXTS = new Set([
55
+ 'css', 'hbs', 'html', 'json', 'txt', 'handlebars', 'mustache', 'template',
56
+ 'pug', 'jade', 'ejs', 'svg', 'less', 'scss',
57
+ ]);
58
+
52
59
  // Sample resume for generating snapshots
53
60
  const SAMPLE_RESUME = {
54
61
  basics: {
55
62
  name: 'Jane Smith',
56
63
  label: 'Senior Software Engineer',
64
+ image: '',
57
65
  email: 'jane@example.com',
58
66
  phone: '+1-555-987-6543',
59
67
  url: 'https://janesmith.dev',
60
68
  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' },
69
+ location: { address: '', postalCode: '', city: 'San Francisco', countryCode: 'US', region: 'California' },
62
70
  profiles: [
63
71
  { network: 'LinkedIn', username: 'janesmith', url: 'https://linkedin.com/in/janesmith' },
64
72
  { network: 'GitHub', username: 'janesmith', url: 'https://github.com/janesmith' },
@@ -113,7 +121,6 @@ const SAMPLE_RESUME = {
113
121
  highlights: ['Published to npm with 5,000+ weekly downloads'],
114
122
  },
115
123
  ],
116
- // Provide empty arrays for sections themes might iterate over
117
124
  volunteer: [],
118
125
  awards: [],
119
126
  certificates: [],
@@ -123,6 +130,30 @@ const SAMPLE_RESUME = {
123
130
  references: [],
124
131
  };
125
132
 
133
+ /**
134
+ * Pad resume with safe defaults so themes don't crash on missing fields.
135
+ * Ported from api/_lib/themeInstaller.ts.
136
+ */
137
+ function padResume(r) {
138
+ const basics = r.basics ?? {};
139
+ const location = basics.location ?? {};
140
+ const safe = {
141
+ ...r,
142
+ basics: {
143
+ name: '', label: '', image: '', email: '', phone: '', url: '', summary: '',
144
+ ...basics,
145
+ location: { address: '', postalCode: '', city: '', countryCode: '', region: '', ...location },
146
+ },
147
+ };
148
+ const arraySections = ['work','volunteer','education','awards','certificates','publications','skills','languages','interests','references','projects'];
149
+ for (const key of arraySections) {
150
+ safe[key] = Array.isArray(safe[key]) ? safe[key] : [];
151
+ }
152
+ const safeBasics = safe.basics;
153
+ safeBasics.profiles = Array.isArray(safeBasics.profiles) ? safeBasics.profiles : [];
154
+ return safe;
155
+ }
156
+
126
157
  // ── Theme file collection ────────────────────────────────────────────
127
158
 
128
159
  /** Recursively collect text files from a theme directory for the fs shim. */
@@ -146,7 +177,7 @@ function collectThemeFiles(themeDir) {
146
177
  walk(full, rel);
147
178
  } else {
148
179
  const ext = (entry.name.split('.').pop() || '').toLowerCase();
149
- if (['css', 'hbs', 'html', 'json', 'txt', 'handlebars', 'mustache', 'template'].includes(ext)) {
180
+ if (TEXT_EXTS.has(ext)) {
150
181
  try { files[rel] = readFileSync(full, 'utf-8'); }
151
182
  catch { /* skip */ }
152
183
  }
@@ -163,11 +194,11 @@ function collectThemeFiles(themeDir) {
163
194
  function generateThemeFsShim(themeFiles) {
164
195
  const { files, dirs } = themeFiles;
165
196
  const lines = [
166
- `const __files = ${JSON.stringify(files)};`,
167
- `const __dirs = ${JSON.stringify(dirs)};`,
197
+ `var __files = ${JSON.stringify(files)};`,
198
+ `var __dirs = ${JSON.stringify(dirs)};`,
168
199
  '',
169
200
  'function normalizePath(p) {',
170
- ' return p.replace(/[\\\\]+/g, "/").replace(/^\\/+/, "");',
201
+ ' return String(p).replace(/[\\\\]+/g, "/").replace(/^\\/+/, "");',
171
202
  '}',
172
203
  '',
173
204
  'function matchFile(p) {',
@@ -175,7 +206,7 @@ function generateThemeFsShim(themeFiles) {
175
206
  ' if (__files[clean] !== undefined) return __files[clean];',
176
207
  ' var keys = Object.keys(__files);',
177
208
  ' for (var i = 0; i < keys.length; i++) {',
178
- ' if (clean.endsWith("/" + keys[i]) || clean.endsWith(keys[i])) return __files[keys[i]];',
209
+ ' if (clean.endsWith("/" + keys[i]) || clean === keys[i]) return __files[keys[i]];',
179
210
  ' }',
180
211
  ' return undefined;',
181
212
  '}',
@@ -185,17 +216,40 @@ function generateThemeFsShim(themeFiles) {
185
216
  ' if (__dirs[clean] !== undefined) return __dirs[clean];',
186
217
  ' var keys = Object.keys(__dirs);',
187
218
  ' for (var i = 0; i < keys.length; i++) {',
188
- ' if (clean.endsWith("/" + keys[i]) || clean.endsWith(keys[i])) return __dirs[keys[i]];',
219
+ ' if (clean.endsWith("/" + keys[i]) || clean === keys[i]) return __dirs[keys[i]];',
189
220
  ' }',
190
221
  ' return undefined;',
191
222
  '}',
192
223
  '',
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 : []; };',
224
+ 'export var readFileSync = function(p, encoding) {',
225
+ ' var r = matchFile(p);',
226
+ ' if (r !== undefined) return r;',
227
+ ' return typeof encoding === "string" ? "" : "";',
228
+ '};',
229
+ '',
230
+ 'export var readdirSync = function(p, opts) {',
231
+ ' var r = matchDir(p);',
232
+ ' if (r === undefined) r = [];',
233
+ ' if (opts && opts.withFileTypes) {',
234
+ ' return r.map(function(name) {',
235
+ ' var childPath = normalizePath(p) + "/" + name;',
236
+ ' var isDir = matchDir(childPath) !== undefined;',
237
+ ' return { name: name, isFile: function() { return !isDir; }, isDirectory: function() { return isDir; } };',
238
+ ' });',
239
+ ' }',
240
+ ' return r;',
241
+ '};',
242
+ '',
195
243
  'export var existsSync = function(p) { return matchFile(p) !== undefined || matchDir(p) !== undefined; };',
196
244
  'export var writeFileSync = function() {};',
197
245
  'export var mkdirSync = function() {};',
198
- 'export default { readFileSync: readFileSync, readdirSync: readdirSync, existsSync: existsSync, writeFileSync: writeFileSync, mkdirSync: mkdirSync };',
246
+ 'export var statSync = function() { return { isFile: function() { return true; }, isDirectory: function() { return false; }, size: 0, mtime: new Date() }; };',
247
+ 'export var lstatSync = statSync;',
248
+ 'export var unlinkSync = function() {};',
249
+ 'export var rmdirSync = function() {};',
250
+ 'export var createReadStream = function() { return { pipe: function(d) { return d; }, on: function() { return this; } }; };',
251
+ 'export var createWriteStream = function() { return { write: function() {}, end: function() {}, on: function() { return this; } }; };',
252
+ 'export default { readFileSync: readFileSync, readdirSync: readdirSync, existsSync: existsSync, writeFileSync: writeFileSync, mkdirSync: mkdirSync, statSync: statSync, lstatSync: lstatSync, unlinkSync: unlinkSync, rmdirSync: rmdirSync, createReadStream: createReadStream, createWriteStream: createWriteStream };',
199
253
  ];
200
254
  return lines.join('\n');
201
255
  }
@@ -230,14 +284,14 @@ function generateSnapshot(shortName, packageName) {
230
284
  const render = theme.render || (theme.default && theme.default.render);
231
285
  if (typeof render !== 'function') return null;
232
286
 
233
- const result = render(SAMPLE_RESUME);
287
+ const paddedResume = padResume(SAMPLE_RESUME);
288
+ const result = render(paddedResume);
234
289
  // render() may return a string or a Promise
235
290
  if (typeof result === 'string') {
236
291
  writeFileSync(resolve(THEMES_DIR, `${shortName}.snapshot.html`), result);
237
292
  extractCss(result, shortName);
238
293
  return result;
239
294
  }
240
- // If it's a promise, we need to await it
241
295
  if (result && typeof result.then === 'function') {
242
296
  return result.then((html) => {
243
297
  if (typeof html === 'string') {
@@ -324,20 +378,38 @@ async function bundleTheme(shortName, packageName, shimsDir) {
324
378
  outfile: resolve(THEMES_DIR, `${shortName}.js`),
325
379
  define: {
326
380
  'process.env.NODE_ENV': '"production"',
381
+ 'process.env.LANG': '""',
327
382
  'global': 'globalThis',
328
383
  '__dirname': '"/"',
329
384
  '__filename': '"/index.js"',
330
385
  'process.browser': 'true',
331
386
  'process.platform': '"browser"',
332
- 'process.version': '"v18.0.0"',
387
+ 'process.version': '"v20.0.0"',
388
+ 'process.versions': '{}',
389
+ 'process.stdout': 'false',
390
+ 'process.stderr': 'false',
333
391
  },
334
392
  alias: {
335
393
  'path': resolve(shimsDir, 'path.js'),
336
394
  'fs': resolve(shimsDir, 'fs.js'),
337
395
  'url': resolve(shimsDir, 'url.js'),
396
+ 'assert': resolve(shimsDir, 'assert.js'),
397
+ 'buffer': resolve(shimsDir, 'buffer.js'),
398
+ 'stream': resolve(shimsDir, 'stream.js'),
399
+ 'util': resolve(shimsDir, 'util.js'),
400
+ 'events': resolve(shimsDir, 'events.js'),
401
+ 'os': resolve(shimsDir, 'os.js'),
402
+ // node: prefixed variants
403
+ 'node:fs': resolve(shimsDir, 'fs.js'),
404
+ 'node:path': resolve(shimsDir, 'path.js'),
338
405
  'node:url': resolve(shimsDir, 'url.js'),
339
406
  'node:crypto': resolve(shimsDir, 'crypto.js'),
340
- 'assert': resolve(shimsDir, 'assert.js'),
407
+ 'node:buffer': resolve(shimsDir, 'buffer.js'),
408
+ 'node:stream': resolve(shimsDir, 'stream.js'),
409
+ 'node:util': resolve(shimsDir, 'util.js'),
410
+ 'node:events': resolve(shimsDir, 'events.js'),
411
+ 'node:os': resolve(shimsDir, 'os.js'),
412
+ 'node:assert': resolve(shimsDir, 'assert.js'),
341
413
  },
342
414
  logLevel: 'silent',
343
415
  });
@@ -351,6 +423,212 @@ async function bundleTheme(shortName, packageName, shimsDir) {
351
423
  }
352
424
  }
353
425
 
426
+ // ── Shim file writers ────────────────────────────────────────────────
427
+
428
+ function writeShims(shimsDir) {
429
+ writeFileSync(resolve(shimsDir, 'path.js'), [
430
+ 'export var join = function() { return [].slice.call(arguments).join("/"); };',
431
+ 'export var resolve = function() { return [].slice.call(arguments).join("/"); };',
432
+ 'export var dirname = function(p) { return p.split("/").slice(0, -1).join("/"); };',
433
+ 'export var basename = function(p, ext) { var b = p.split("/").pop() || ""; return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; };',
434
+ 'export var extname = function(p) { var m = p.match(/\\.[^.]+$/); return m ? m[0] : ""; };',
435
+ 'export var sep = "/";',
436
+ 'export var isAbsolute = function(p) { return p.charAt(0) === "/"; };',
437
+ 'export var normalize = function(p) { return p; };',
438
+ 'export var relative = function(from, to) { return to; };',
439
+ 'export var parse = function(p) { return { root: "", dir: dirname(p), base: basename(p), ext: extname(p), name: basename(p, extname(p)) }; };',
440
+ 'export default { join: join, resolve: resolve, dirname: dirname, basename: basename, extname: extname, sep: sep, isAbsolute: isAbsolute, normalize: normalize, relative: relative, parse: parse };',
441
+ ].join('\n'));
442
+
443
+ // fs shim is generated per-theme in bundleTheme()
444
+
445
+ writeFileSync(resolve(shimsDir, 'url.js'), [
446
+ 'export var URL = globalThis.URL;',
447
+ 'export var URLSearchParams = globalThis.URLSearchParams;',
448
+ 'export var fileURLToPath = function(u) { return u.replace(/^file:\\/\\//, ""); };',
449
+ 'export var pathToFileURL = function(p) { return new globalThis.URL("file://" + p); };',
450
+ 'export var format = function(u) { return typeof u === "string" ? u : u.href; };',
451
+ 'export var parse = function(u) { return new globalThis.URL(u); };',
452
+ 'export default { URL: URL, URLSearchParams: URLSearchParams, fileURLToPath: fileURLToPath, pathToFileURL: pathToFileURL, format: format, parse: parse };',
453
+ ].join('\n'));
454
+
455
+ writeFileSync(resolve(shimsDir, 'crypto.js'), [
456
+ 'export var createHash = function() { return { update: function() { return this; }, digest: function() { return ""; } }; };',
457
+ 'export var randomBytes = function(n) { return new Uint8Array(n); };',
458
+ 'export var createHmac = function() { return { update: function() { return this; }, digest: function() { return ""; } }; };',
459
+ 'export default { createHash: createHash, randomBytes: randomBytes, createHmac: createHmac };',
460
+ ].join('\n'));
461
+
462
+ writeFileSync(resolve(shimsDir, 'assert.js'), [
463
+ 'var assert = function(v, msg) { if (!v) throw new Error(msg || "Assertion failed"); };',
464
+ 'assert.ok = assert;',
465
+ 'assert.strictEqual = function(a, b) { if (a !== b) throw new Error("Not equal"); };',
466
+ 'assert.deepStrictEqual = function() {};',
467
+ 'assert.fail = function(msg) { throw new Error(msg); };',
468
+ 'export default assert;',
469
+ 'export var ok = assert;',
470
+ 'export var strictEqual = assert.strictEqual;',
471
+ ].join('\n'));
472
+
473
+ writeFileSync(resolve(shimsDir, 'buffer.js'), [
474
+ 'var _enc = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;',
475
+ 'var _dec = typeof TextDecoder !== "undefined" ? new TextDecoder() : null;',
476
+ '',
477
+ 'function BufferShim(arg) {',
478
+ ' if (typeof arg === "number") return new Uint8Array(arg);',
479
+ ' if (arg instanceof Uint8Array) return arg;',
480
+ ' if (arg instanceof ArrayBuffer) return new Uint8Array(arg);',
481
+ ' return new Uint8Array(0);',
482
+ '}',
483
+ '',
484
+ 'BufferShim.from = function(data, encoding) {',
485
+ ' if (typeof data === "string") {',
486
+ ' if (encoding === "base64") {',
487
+ ' var bin = atob(data);',
488
+ ' var bytes = new Uint8Array(bin.length);',
489
+ ' for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);',
490
+ ' return bytes;',
491
+ ' }',
492
+ ' return _enc ? _enc.encode(data) : new Uint8Array(0);',
493
+ ' }',
494
+ ' if (Array.isArray(data)) return new Uint8Array(data);',
495
+ ' if (data instanceof ArrayBuffer) return new Uint8Array(data);',
496
+ ' if (data instanceof Uint8Array) return new Uint8Array(data);',
497
+ ' return new Uint8Array(0);',
498
+ '};',
499
+ 'BufferShim.alloc = function(size) { return new Uint8Array(size); };',
500
+ 'BufferShim.allocUnsafe = function(size) { return new Uint8Array(size); };',
501
+ 'BufferShim.isBuffer = function(obj) { return obj instanceof Uint8Array; };',
502
+ 'BufferShim.isEncoding = function() { return true; };',
503
+ 'BufferShim.byteLength = function(str) { return _enc ? _enc.encode(str).length : str.length; };',
504
+ 'BufferShim.concat = function(list) {',
505
+ ' var total = 0;',
506
+ ' for (var i = 0; i < list.length; i++) total += list[i].length;',
507
+ ' var result = new Uint8Array(total);',
508
+ ' var offset = 0;',
509
+ ' for (var j = 0; j < list.length; j++) { result.set(list[j], offset); offset += list[j].length; }',
510
+ ' return result;',
511
+ '};',
512
+ '',
513
+ 'export var Buffer = BufferShim;',
514
+ 'export var SlowBuffer = BufferShim;',
515
+ 'export var INSPECT_MAX_BYTES = 50;',
516
+ 'export var kMaxLength = 2147483647;',
517
+ 'export default { Buffer: BufferShim, SlowBuffer: BufferShim, INSPECT_MAX_BYTES: 50, kMaxLength: 2147483647 };',
518
+ ].join('\n'));
519
+
520
+ writeFileSync(resolve(shimsDir, 'stream.js'), [
521
+ 'function EventBase() { this._e = {}; }',
522
+ 'EventBase.prototype.on = function(ev, fn) { (this._e[ev] = this._e[ev] || []).push(fn); return this; };',
523
+ 'EventBase.prototype.addListener = EventBase.prototype.on;',
524
+ 'EventBase.prototype.once = function(ev, fn) { var self = this; var w = function() { self.removeListener(ev, w); fn.apply(null, arguments); }; return this.on(ev, w); };',
525
+ 'EventBase.prototype.emit = function(ev) { var args = [].slice.call(arguments, 1); (this._e[ev] || []).forEach(function(fn) { fn.apply(null, args); }); return true; };',
526
+ 'EventBase.prototype.removeListener = function(ev, fn) { var l = this._e[ev]; if (l) this._e[ev] = l.filter(function(f) { return f !== fn; }); return this; };',
527
+ 'EventBase.prototype.off = EventBase.prototype.removeListener;',
528
+ 'EventBase.prototype.removeAllListeners = function(ev) { if (ev) delete this._e[ev]; else this._e = {}; return this; };',
529
+ 'EventBase.prototype.setMaxListeners = function() { return this; };',
530
+ 'EventBase.prototype.listeners = function(ev) { return this._e[ev] || []; };',
531
+ '',
532
+ 'function Readable() { EventBase.call(this); } Readable.prototype = Object.create(EventBase.prototype);',
533
+ 'Readable.prototype.read = function() { return null; };',
534
+ 'Readable.prototype.pipe = function(d) { return d; };',
535
+ 'Readable.prototype.unpipe = function() { return this; };',
536
+ 'Readable.prototype.destroy = function() { return this; };',
537
+ '',
538
+ 'function Writable() { EventBase.call(this); } Writable.prototype = Object.create(EventBase.prototype);',
539
+ 'Writable.prototype.write = function() { return true; };',
540
+ 'Writable.prototype.end = function() { return this; };',
541
+ 'Writable.prototype.destroy = function() { return this; };',
542
+ '',
543
+ 'function Transform() { EventBase.call(this); } Transform.prototype = Object.create(EventBase.prototype);',
544
+ 'Transform.prototype.write = function() { return true; };',
545
+ 'Transform.prototype.end = function() { return this; };',
546
+ 'Transform.prototype.pipe = function(d) { return d; };',
547
+ 'Transform.prototype.destroy = function() { return this; };',
548
+ '',
549
+ 'function PassThrough() { Transform.call(this); } PassThrough.prototype = Object.create(Transform.prototype);',
550
+ 'function Duplex() { EventBase.call(this); } Duplex.prototype = Object.create(EventBase.prototype);',
551
+ 'Duplex.prototype.write = function() { return true; };',
552
+ 'Duplex.prototype.end = function() { return this; };',
553
+ 'Duplex.prototype.read = function() { return null; };',
554
+ 'Duplex.prototype.pipe = function(d) { return d; };',
555
+ 'Duplex.prototype.destroy = function() { return this; };',
556
+ '',
557
+ 'function Stream() { EventBase.call(this); } Stream.prototype = Object.create(EventBase.prototype);',
558
+ 'Stream.prototype.pipe = function(d) { return d; };',
559
+ 'Stream.Readable = Readable; Stream.Writable = Writable; Stream.Transform = Transform;',
560
+ 'Stream.PassThrough = PassThrough; Stream.Duplex = Duplex; Stream.Stream = Stream;',
561
+ '',
562
+ 'export { Readable, Writable, Transform, PassThrough, Duplex, Stream };',
563
+ 'export default Stream;',
564
+ ].join('\n'));
565
+
566
+ writeFileSync(resolve(shimsDir, 'util.js'), [
567
+ 'export var inherits = function(ctor, superCtor) { ctor.super_ = superCtor; Object.setPrototypeOf(ctor.prototype, superCtor.prototype); };',
568
+ 'export var deprecate = function(fn) { return fn; };',
569
+ 'export var promisify = function(fn) { return function() { var args = [].slice.call(arguments); return new Promise(function(ok, fail) { args.push(function(err, res) { err ? fail(err) : ok(res); }); fn.apply(null, args); }); }; };',
570
+ 'export var inspect = function(obj) { try { return JSON.stringify(obj); } catch(e) { return String(obj); } };',
571
+ 'export var format = function(f) { if (typeof f !== "string") return [].map.call(arguments, function(a) { return inspect(a); }).join(" "); var i = 1; var args = arguments; return f.replace(/%[sdj%]/g, function(m) { if (m === "%%") return "%"; if (i >= args.length) return m; var v = args[i++]; if (m === "%s") return String(v); if (m === "%d") return Number(v); if (m === "%j") try { return JSON.stringify(v); } catch(e) { return "[Circular]"; } return m; }); };',
572
+ 'export var debuglog = function() { return function() {}; };',
573
+ 'export var isArray = Array.isArray;',
574
+ 'export var isBoolean = function(v) { return typeof v === "boolean"; };',
575
+ 'export var isNull = function(v) { return v === null; };',
576
+ 'export var isNumber = function(v) { return typeof v === "number"; };',
577
+ 'export var isString = function(v) { return typeof v === "string"; };',
578
+ 'export var isUndefined = function(v) { return v === undefined; };',
579
+ 'export var isObject = function(v) { return typeof v === "object" && v !== null; };',
580
+ 'export var isFunction = function(v) { return typeof v === "function"; };',
581
+ 'export var isRegExp = function(v) { return v instanceof RegExp; };',
582
+ 'export var isDate = function(v) { return v instanceof Date; };',
583
+ 'export var isError = function(v) { return v instanceof Error; };',
584
+ 'export var isPrimitive = function(v) { return v === null || typeof v !== "object" && typeof v !== "function"; };',
585
+ 'export var TextEncoder = globalThis.TextEncoder;',
586
+ 'export var TextDecoder = globalThis.TextDecoder;',
587
+ 'export var types = { isAnyArrayBuffer: function() { return false; }, isTypedArray: function(v) { return ArrayBuffer.isView(v); } };',
588
+ 'export default { inherits: inherits, deprecate: deprecate, promisify: promisify, inspect: inspect, format: format, debuglog: debuglog, isArray: isArray, isBoolean: isBoolean, isNull: isNull, isNumber: isNumber, isString: isString, isUndefined: isUndefined, isObject: isObject, isFunction: isFunction, isRegExp: isRegExp, isDate: isDate, isError: isError, isPrimitive: isPrimitive, TextEncoder: TextEncoder, TextDecoder: TextDecoder, types: types };',
589
+ ].join('\n'));
590
+
591
+ writeFileSync(resolve(shimsDir, 'events.js'), [
592
+ 'function EventEmitter() { this._events = {}; this._maxListeners = 10; }',
593
+ 'EventEmitter.prototype.on = function(ev, fn) { (this._events[ev] = this._events[ev] || []).push(fn); return this; };',
594
+ 'EventEmitter.prototype.addListener = EventEmitter.prototype.on;',
595
+ 'EventEmitter.prototype.once = function(ev, fn) { var self = this; var w = function() { self.removeListener(ev, w); fn.apply(null, arguments); }; return this.on(ev, w); };',
596
+ 'EventEmitter.prototype.emit = function(ev) { var args = [].slice.call(arguments, 1); (this._events[ev] || []).forEach(function(fn) { fn.apply(null, args); }); return true; };',
597
+ 'EventEmitter.prototype.removeListener = function(ev, fn) { var l = this._events[ev]; if (l) this._events[ev] = l.filter(function(f) { return f !== fn; }); return this; };',
598
+ 'EventEmitter.prototype.off = EventEmitter.prototype.removeListener;',
599
+ 'EventEmitter.prototype.removeAllListeners = function(ev) { if (ev) delete this._events[ev]; else this._events = {}; return this; };',
600
+ 'EventEmitter.prototype.setMaxListeners = function(n) { this._maxListeners = n; return this; };',
601
+ 'EventEmitter.prototype.getMaxListeners = function() { return this._maxListeners; };',
602
+ 'EventEmitter.prototype.listeners = function(ev) { return this._events[ev] || []; };',
603
+ 'EventEmitter.prototype.listenerCount = function(ev) { return (this._events[ev] || []).length; };',
604
+ 'EventEmitter.prototype.prependListener = EventEmitter.prototype.on;',
605
+ 'EventEmitter.prototype.prependOnceListener = EventEmitter.prototype.once;',
606
+ 'EventEmitter.prototype.eventNames = function() { return Object.keys(this._events); };',
607
+ 'EventEmitter.EventEmitter = EventEmitter;',
608
+ 'EventEmitter.defaultMaxListeners = 10;',
609
+ 'export { EventEmitter };',
610
+ 'export default EventEmitter;',
611
+ ].join('\n'));
612
+
613
+ writeFileSync(resolve(shimsDir, 'os.js'), [
614
+ 'export var platform = function() { return "browser"; };',
615
+ 'export var tmpdir = function() { return "/tmp"; };',
616
+ 'export var homedir = function() { return "/"; };',
617
+ 'export var hostname = function() { return "localhost"; };',
618
+ 'export var type = function() { return "Browser"; };',
619
+ 'export var arch = function() { return "wasm"; };',
620
+ 'export var release = function() { return "0.0.0"; };',
621
+ 'export var EOL = "\\n";',
622
+ 'export var endianness = function() { return "LE"; };',
623
+ 'export var cpus = function() { return []; };',
624
+ 'export var totalmem = function() { return 0; };',
625
+ 'export var freemem = function() { return 0; };',
626
+ 'export var networkInterfaces = function() { return {}; };',
627
+ 'export var userInfo = function() { return { username: "browser", uid: 0, gid: 0, shell: "", homedir: "/" }; };',
628
+ 'export default { platform: platform, tmpdir: tmpdir, homedir: homedir, hostname: hostname, type: type, arch: arch, release: release, EOL: EOL, endianness: endianness, cpus: cpus, totalmem: totalmem, freemem: freemem, networkInterfaces: networkInterfaces, userInfo: userInfo };',
629
+ ].join('\n'));
630
+ }
631
+
354
632
  // ── Main ─────────────────────────────────────────────────────────────
355
633
 
356
634
  async function main() {
@@ -361,45 +639,10 @@ async function main() {
361
639
 
362
640
  mkdirSync(THEMES_DIR, { recursive: true });
363
641
 
364
- // Create shims directory
642
+ // Create shims directory and write all shims
365
643
  const shimsDir = resolve(__dirname, 'shims');
366
644
  mkdirSync(shimsDir, { recursive: true });
367
-
368
- writeFileSync(resolve(shimsDir, 'path.js'), `
369
- export const join = (...parts) => parts.join('/');
370
- export const resolve = (...parts) => parts.join('/');
371
- export const dirname = (p) => p.split('/').slice(0, -1).join('/');
372
- export const basename = (p) => p.split('/').pop();
373
- export const extname = (p) => { const m = p.match(/\\.[^.]+$/); return m ? m[0] : ''; };
374
- export const sep = '/';
375
- export default { join, resolve, dirname, basename, extname, sep };
376
- `);
377
- // fs shim is generated per-theme in bundleTheme()
378
- writeFileSync(resolve(shimsDir, 'url.js'), `
379
- export const URL = globalThis.URL;
380
- export const URLSearchParams = globalThis.URLSearchParams;
381
- export const fileURLToPath = (u) => u.replace(/^file:\\/\\//, '');
382
- export const pathToFileURL = (p) => new globalThis.URL('file://' + p);
383
- export const format = (u) => (typeof u === 'string' ? u : u.href);
384
- export const parse = (u) => new globalThis.URL(u);
385
- export default { URL, URLSearchParams, fileURLToPath, pathToFileURL, format, parse };
386
- `);
387
- writeFileSync(resolve(shimsDir, 'crypto.js'), `
388
- export const createHash = () => ({ update: function() { return this; }, digest: () => '' });
389
- export const randomBytes = (n) => new Uint8Array(n);
390
- export const createHmac = () => ({ update: function() { return this; }, digest: () => '' });
391
- export default { createHash, randomBytes, createHmac };
392
- `);
393
- writeFileSync(resolve(shimsDir, 'assert.js'), `
394
- const assert = (v, msg) => { if (!v) throw new Error(msg || 'Assertion failed'); };
395
- assert.ok = assert;
396
- assert.strictEqual = (a, b) => { if (a !== b) throw new Error('Not equal'); };
397
- assert.deepStrictEqual = () => {};
398
- assert.fail = (msg) => { throw new Error(msg); };
399
- export default assert;
400
- export const ok = assert;
401
- export const strictEqual = assert.strictEqual;
402
- `);
645
+ writeShims(shimsDir);
403
646
 
404
647
  let themes;
405
648
  if (specificThemes) {
@@ -429,9 +672,9 @@ async function main() {
429
672
 
430
673
  // Install the theme
431
674
  try {
432
- execSync(`npm install --no-save ${theme.packageName} 2>/dev/null`, {
675
+ execSync(`npm install --no-save --prefer-offline ${theme.packageName} 2>/dev/null`, {
433
676
  cwd: resolve(__dirname, '..'),
434
- timeout: 30000,
677
+ timeout: 60000,
435
678
  });
436
679
  } catch {
437
680
  console.log('❌ install failed');
@@ -452,7 +695,6 @@ async function main() {
452
695
  let hasSnapshot = false;
453
696
  let hasCss = false;
454
697
  const snapshotResult = generateSnapshot(theme.name, theme.packageName);
455
- // Handle promise if async render
456
698
  if (snapshotResult && typeof snapshotResult.then === 'function') {
457
699
  const html = await snapshotResult;
458
700
  hasSnapshot = !!html;
@@ -488,7 +730,6 @@ async function main() {
488
730
  if (hasSnapshot) parts.push('snapshot');
489
731
  console.log(parts.join(', ') + ')');
490
732
  } else {
491
- // Bundle failed — still useful if we have a snapshot (server fallback)
492
733
  manifest.push({
493
734
  name: theme.name,
494
735
  displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),