resuml 1.20.1 → 2.0.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.
@@ -1,1104 +0,0 @@
1
- /**
2
- * Theme Bundling Script
3
- *
4
- * Bundles jsonresume-theme packages for the browser with:
5
- * - Per-theme fs shim with embedded assets (CSS, templates, etc.)
6
- * - CSS extraction (separate .css files)
7
- * - Build-time snapshots (pre-rendered HTML for instant preview)
8
- * - Node.js built-in shims (buffer, stream, util, events, os, etc.)
9
- *
10
- * Output:
11
- * docs/themes/{name}.js , bundled render module
12
- * docs/themes/{name}.css , extracted CSS (if any)
13
- * docs/themes/{name}.snapshot.html: pre-rendered HTML with sample data
14
- * docs/themes/manifest.json , theme metadata registry
15
- *
16
- * Usage:
17
- * node scripts/bundle-themes.js # Bundle popular themes
18
- * node scripts/bundle-themes.js --all # Discover + bundle all from npm
19
- * node scripts/bundle-themes.js --themes elegant,even,kendall # Specific themes
20
- */
21
-
22
- import { build } from 'esbuild';
23
- import { resolve, dirname, join } from 'path';
24
- import { fileURLToPath } from 'url';
25
- import { execSync } from 'child_process';
26
- import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync } from 'fs';
27
- import { createRequire } from 'module';
28
-
29
- const __dirname = dirname(fileURLToPath(import.meta.url));
30
- const require = createRequire(import.meta.url);
31
- const THEMES_DIR = resolve(__dirname, '../docs/themes');
32
-
33
- // Popular themes that are known to work in the browser
34
- const POPULAR_THEMES = [
35
- 'stackoverflow',
36
- 'elegant',
37
- 'even',
38
- 'kendall',
39
- 'flat',
40
- 'macchiato',
41
- 'class',
42
- 'paper',
43
- 'spartan',
44
- 'short',
45
- 'caffeine',
46
- 'onepage',
47
- 'kwan',
48
- 'autumn',
49
- 'relaxed',
50
- 'compact',
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
-
59
- // Sample resume for generating snapshots
60
- const SAMPLE_RESUME = {
61
- basics: {
62
- name: 'Jane Smith',
63
- label: 'Senior Software Engineer',
64
- image: '',
65
- email: 'jane@example.com',
66
- phone: '+1-555-987-6543',
67
- url: 'https://janesmith.dev',
68
- summary: 'Full-stack engineer with 8+ years of experience building scalable web applications. Passionate about clean architecture, performance optimization, and mentoring teams.',
69
- location: { address: '', postalCode: '', city: 'San Francisco', countryCode: 'US', region: 'California' },
70
- profiles: [
71
- { network: 'LinkedIn', username: 'janesmith', url: 'https://linkedin.com/in/janesmith' },
72
- { network: 'GitHub', username: 'janesmith', url: 'https://github.com/janesmith' },
73
- ],
74
- },
75
- work: [
76
- {
77
- name: 'Tech Corp',
78
- position: 'Senior Software Engineer',
79
- url: 'https://techcorp.com',
80
- startDate: '2020-03-01',
81
- summary: 'Lead engineer for the platform team, owning core API infrastructure.',
82
- highlights: [
83
- 'Reduced API latency by 45% through caching and query optimization',
84
- 'Designed microservices architecture serving 2M daily requests',
85
- 'Mentored 4 junior engineers through technical growth plans',
86
- ],
87
- },
88
- {
89
- name: 'StartupXYZ',
90
- position: 'Full Stack Developer',
91
- startDate: '2017-06-01',
92
- endDate: '2020-02-28',
93
- summary: 'Built customer-facing web applications from prototype to production.',
94
- highlights: [
95
- 'Built real-time dashboard used by 15,000+ daily active users',
96
- 'Implemented CI/CD pipeline reducing deploy time from 2 hours to 8 minutes',
97
- ],
98
- },
99
- ],
100
- education: [
101
- {
102
- institution: 'University of Technology',
103
- area: 'Computer Science',
104
- studyType: 'Bachelor of Science',
105
- startDate: '2013-09-01',
106
- endDate: '2017-05-15',
107
- score: '3.8',
108
- },
109
- ],
110
- skills: [
111
- { name: 'Languages', level: 'Expert', keywords: ['TypeScript', 'JavaScript', 'Python', 'Go'] },
112
- { name: 'Frontend', level: 'Expert', keywords: ['React', 'Next.js', 'HTML/CSS', 'Tailwind'] },
113
- { name: 'Backend & Cloud', level: 'Advanced', keywords: ['Node.js', 'PostgreSQL', 'AWS', 'Docker'] },
114
- ],
115
- projects: [
116
- {
117
- name: 'Open Source CLI Tool',
118
- description: 'Developer productivity tool with 2,000+ GitHub stars',
119
- url: 'https://github.com/janesmith/cli-tool',
120
- startDate: '2022-01-01',
121
- highlights: ['Published to npm with 5,000+ weekly downloads'],
122
- },
123
- ],
124
- volunteer: [],
125
- awards: [],
126
- certificates: [],
127
- publications: [],
128
- languages: [],
129
- interests: [],
130
- references: [],
131
- };
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 meta = r.meta ?? {};
141
- const palette = meta.palette ?? {};
142
- const safe = {
143
- ...r,
144
- basics: {
145
- name: '', label: '', image: '', email: '', phone: '', url: '', summary: '',
146
- ...basics,
147
- location: { address: '', postalCode: '', city: '', countryCode: '', region: '', ...location },
148
- },
149
- // Guard against themes like material that read meta.palette.primary without
150
- // defensive checks.
151
- meta: { ...meta, palette: { primary: '', secondary: '', ...palette } },
152
- };
153
- const arraySections = ['work','volunteer','education','awards','certificates','publications','skills','languages','interests','references','projects'];
154
- for (const key of arraySections) {
155
- safe[key] = Array.isArray(safe[key]) ? safe[key] : [];
156
- }
157
- const safeBasics = safe.basics;
158
- safeBasics.profiles = Array.isArray(safeBasics.profiles) ? safeBasics.profiles : [];
159
- return safe;
160
- }
161
-
162
- // ── Theme file collection ────────────────────────────────────────────
163
-
164
- /** Recursively collect text files from a theme directory for the fs shim. */
165
- function collectThemeFiles(themeDir) {
166
- const files = {};
167
- const dirs = {};
168
-
169
- function walk(dir, relPrefix = '') {
170
- let entries;
171
- try { entries = readdirSync(dir, { withFileTypes: true }); }
172
- catch { return; }
173
-
174
- const childNames = [];
175
- for (const entry of entries) {
176
- if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue;
177
- childNames.push(entry.name);
178
- const full = join(dir, entry.name);
179
- const rel = relPrefix ? `${relPrefix}/${entry.name}` : entry.name;
180
-
181
- if (entry.isDirectory()) {
182
- walk(full, rel);
183
- } else {
184
- const ext = (entry.name.split('.').pop() || '').toLowerCase();
185
- if (TEXT_EXTS.has(ext)) {
186
- try { files[rel] = readFileSync(full, 'utf-8'); }
187
- catch { /* skip */ }
188
- }
189
- }
190
- }
191
- dirs[relPrefix || '.'] = childNames;
192
- }
193
-
194
- walk(themeDir);
195
- return { files, dirs };
196
- }
197
-
198
- /** Generate an fs shim that embeds the theme's files. */
199
- function generateThemeFsShim(themeFiles) {
200
- const { files, dirs } = themeFiles;
201
- const lines = [
202
- `var __files = ${JSON.stringify(files)};`,
203
- `var __dirs = ${JSON.stringify(dirs)};`,
204
- '',
205
- 'function normalizePath(p) {',
206
- ' return String(p).replace(/[\\\\]+/g, "/").replace(/^\\/+/, "");',
207
- '}',
208
- '',
209
- 'function matchFile(p) {',
210
- ' var clean = normalizePath(p);',
211
- ' if (__files[clean] !== undefined) return __files[clean];',
212
- ' var keys = Object.keys(__files);',
213
- ' for (var i = 0; i < keys.length; i++) {',
214
- ' if (clean.endsWith("/" + keys[i]) || clean === keys[i]) return __files[keys[i]];',
215
- ' }',
216
- ' return undefined;',
217
- '}',
218
- '',
219
- 'function matchDir(p) {',
220
- ' var clean = normalizePath(p);',
221
- ' // Root ("/"/".") after stripping leading slash is "", look up "." which',
222
- ' // the collector uses for the top-level directory listing.',
223
- ' if (clean === "" || clean === ".") {',
224
- ' if (__dirs["."] !== undefined) return __dirs["."];',
225
- ' }',
226
- ' if (__dirs[clean] !== undefined) return __dirs[clean];',
227
- ' var keys = Object.keys(__dirs);',
228
- ' for (var i = 0; i < keys.length; i++) {',
229
- ' if (clean.endsWith("/" + keys[i]) || clean === keys[i]) return __dirs[keys[i]];',
230
- ' }',
231
- ' return undefined;',
232
- '}',
233
- '',
234
- 'export var readFileSync = function(p, encoding) {',
235
- ' var r = matchFile(p);',
236
- ' if (r !== undefined) return r;',
237
- ' return typeof encoding === "string" ? "" : "";',
238
- '};',
239
- '',
240
- 'export var readdirSync = function(p, opts) {',
241
- ' var r = matchDir(p);',
242
- ' if (r === undefined) r = [];',
243
- ' if (opts && opts.withFileTypes) {',
244
- ' return r.map(function(name) {',
245
- ' var childPath = normalizePath(p) + "/" + name;',
246
- ' var isDir = matchDir(childPath) !== undefined;',
247
- ' return { name: name, isFile: function() { return !isDir; }, isDirectory: function() { return isDir; } };',
248
- ' });',
249
- ' }',
250
- ' return r;',
251
- '};',
252
- '',
253
- 'export var existsSync = function(p) { return matchFile(p) !== undefined || matchDir(p) !== undefined; };',
254
- 'export var writeFileSync = function() {};',
255
- 'export var mkdirSync = function() {};',
256
- 'export var statSync = function() { return { isFile: function() { return true; }, isDirectory: function() { return false; }, size: 0, mtime: new Date() }; };',
257
- 'export var lstatSync = statSync;',
258
- 'export var unlinkSync = function() {};',
259
- 'export var rmdirSync = function() {};',
260
- 'export var createReadStream = function() { return { pipe: function(d) { return d; }, on: function() { return this; } }; };',
261
- 'export var createWriteStream = function() { return { write: function() {}, end: function() {}, on: function() { return this; } }; };',
262
- 'export default { readFileSync: readFileSync, readdirSync: readdirSync, existsSync: existsSync, writeFileSync: writeFileSync, mkdirSync: mkdirSync, statSync: statSync, lstatSync: lstatSync, unlinkSync: unlinkSync, rmdirSync: rmdirSync, createReadStream: createReadStream, createWriteStream: createWriteStream };',
263
- ];
264
- return lines.join('\n');
265
- }
266
-
267
- // ── CSS extraction ───────────────────────────────────────────────────
268
-
269
- /** Extract <style> blocks from rendered HTML and write to a .css file. */
270
- function extractCss(html, shortName) {
271
- const styleRegex = /<style[^>]*>([\s\S]*?)<\/style>/gi;
272
- let css = '';
273
- let match;
274
- while ((match = styleRegex.exec(html)) !== null) {
275
- css += match[1] + '\n';
276
- }
277
- if (css.trim()) {
278
- writeFileSync(resolve(THEMES_DIR, `${shortName}.css`), css.trim());
279
- return true;
280
- }
281
- return false;
282
- }
283
-
284
- // ── Snapshot generation ──────────────────────────────────────────────
285
-
286
- /** Render a theme with sample data in Node.js and save as snapshot. */
287
- function generateSnapshot(shortName, packageName) {
288
- try {
289
- // Clear require cache to get a fresh instance
290
- const pkgPath = require.resolve(packageName);
291
- delete require.cache[pkgPath];
292
-
293
- const theme = require(packageName);
294
- const render = theme.render || (theme.default && theme.default.render);
295
- if (typeof render !== 'function') return null;
296
-
297
- // Deep-clone SAMPLE_RESUME, some themes preprocess (e.g. markdown-ify)
298
- // fields and mutate the input, which would then pollute subsequent
299
- // themes' snapshots (keywords end up as "<p>TypeScript</p>" etc.).
300
- const paddedResume = padResume(JSON.parse(JSON.stringify(SAMPLE_RESUME)));
301
- const result = render(paddedResume);
302
- // render() may return a string or a Promise
303
- if (typeof result === 'string') {
304
- writeFileSync(resolve(THEMES_DIR, `${shortName}.snapshot.html`), result);
305
- extractCss(result, shortName);
306
- return result;
307
- }
308
- if (result && typeof result.then === 'function') {
309
- return result.then((html) => {
310
- if (typeof html === 'string') {
311
- writeFileSync(resolve(THEMES_DIR, `${shortName}.snapshot.html`), html);
312
- extractCss(html, shortName);
313
- return html;
314
- }
315
- return null;
316
- }).catch(() => null);
317
- }
318
- return null;
319
- } catch (e) {
320
- console.log(` (snapshot failed: ${e.message})`);
321
- return null;
322
- }
323
- }
324
-
325
- // ── npm discovery ────────────────────────────────────────────────────
326
-
327
- async function discoverThemes() {
328
- const themes = [];
329
- let from = 0;
330
- const size = 250;
331
-
332
- console.log('🔍 Discovering themes from npm...');
333
-
334
- for (let page = 0; page < 4; page++) {
335
- const url = `https://registry.npmjs.org/-/v1/search?text=jsonresume-theme&size=${size}&from=${from}`;
336
- const res = await fetch(url);
337
- if (!res.ok) break;
338
- const data = await res.json();
339
-
340
- for (const pkg of data.objects) {
341
- const name = pkg.package.name;
342
- if (!name.startsWith('jsonresume-theme-')) continue;
343
- const shortName = name.replace('jsonresume-theme-', '');
344
- if (pkg.score?.detail?.popularity === 0) continue;
345
-
346
- themes.push({
347
- name: shortName,
348
- packageName: name,
349
- description: pkg.package.description || '',
350
- version: pkg.package.version,
351
- });
352
- }
353
-
354
- if (data.objects.length < size) break;
355
- from += size;
356
- }
357
-
358
- console.log(` Found ${themes.length} themes`);
359
- return themes;
360
- }
361
-
362
- // ── esbuild bundling ─────────────────────────────────────────────────
363
-
364
- /**
365
- * esbuild plugin that fixes legacy CJS patterns that break under ESM's
366
- * implicit strict mode. Specifically: top-level implicit-global assignments
367
- * like `COURSES_COLUMNS = 3;` (onepage, several older themes) which throw
368
- * `ReferenceError` in strict mode. Rewrite them to `var` declarations.
369
- *
370
- * Scope: applied to any .js file inside jsonresume-theme-* packages. Keep the
371
- * regex conservative, only match top-level (start-of-line) SCREAMING_SNAKE
372
- * identifier assignments. This misses camelCase globals but avoids rewriting
373
- * legitimate property assignments inside functions.
374
- */
375
- /**
376
- * Known-bad CJS patterns in specific dependencies. Applied via esbuild onLoad.
377
- * Keep patches minimal and scoped, a regex that's too broad breaks working
378
- * themes that happen to match.
379
- */
380
- const LEGACY_PATCHES = [
381
- // swag 0.7.x branches on `typeof window` and, in a browser, assigns to
382
- // `window.Swag` instead of `module.exports`. After ESM bundling, the theme
383
- // then gets `undefined` from `require('swag')`. Force the module-exports
384
- // branch by hiding `window` only for this file.
385
- {
386
- filter: /node_modules\/swag\/lib\/swag\.js$/,
387
- transform: (src) => src.replace(
388
- /typeof\s+window\s*!==\s*["']undefined["']\s*&&\s*window\s*!==\s*null/g,
389
- 'false',
390
- ),
391
- },
392
- // onepage uses implicit globals inside its render function: `splitCourses`,
393
- // `columnIndex`. Declare them up-front so strict mode doesn't reject them.
394
- {
395
- filter: /node_modules\/jsonresume-theme-onepage\/[^/]*\.js$/,
396
- transform: (src) => {
397
- // Only insert declarations once at top of render() body
398
- return src.replace(
399
- /(function\s+render\s*\([^)]*\)\s*\{)/,
400
- '$1\n var splitCourses, columnIndex;\n',
401
- );
402
- },
403
- },
404
- // jsonresume-theme-react tries to assign `window` and `document` which are
405
- // read-only in the browser (we're already in a browser, no need to
406
- // polyfill). Guard the assignments so they don't throw. It also uses a
407
- // pattern not covered by the generic rewrite:
408
- // const modulePath = path.join(__dirname, 'dist/index.cjs');
409
- // require(modulePath);
410
- // Inline the variable so esbuild can statically bundle dist/index.cjs.
411
- {
412
- filter: /node_modules\/jsonresume-theme-react\/index\.cjs$/,
413
- transform: (src) => src
414
- .replace(
415
- /global\.window\s*=\s*global\.window\s*\|\|\s*\{[^}]*\};?/g,
416
- 'try { if (typeof window === "undefined") globalThis.window = {}; } catch (_) {}',
417
- )
418
- .replace(
419
- /global\.document\s*=\s*global\.document\s*\|\|\s*\{[\s\S]*?\};/g,
420
- 'try { if (typeof document === "undefined") globalThis.document = { createElement: () => ({}), addEventListener: () => {} }; } catch (_) {}',
421
- )
422
- .replace(
423
- /const\s+modulePath\s*=\s*path\.join\(\s*__dirname\s*,\s*['"]([^'"]+)['"]\s*\);([\s\S]*?)require\(modulePath\)/,
424
- (_, rel, middle) => `const modulePath = './${rel}';${middle}require('./${rel}')`,
425
- ),
426
- },
427
- ];
428
-
429
- /**
430
- * esbuild plugin that fixes legacy CJS patterns that break under ESM's
431
- * implicit strict mode. Two layers:
432
- * 1. Targeted LEGACY_PATCHES for known bad packages (swag, specific themes).
433
- * 2. Generic: rewrite top-level `SCREAMING_SNAKE = ...` assignments to
434
- * `var SCREAMING_SNAKE = ...` so implicit globals work under strict.
435
- */
436
- function legacyThemeGlobalsPlugin() {
437
- return {
438
- name: 'legacy-theme-globals',
439
- setup(build) {
440
- build.onLoad({ filter: /\.c?js$/ }, async (args) => {
441
- const { readFile } = await import('node:fs/promises');
442
- const isThemePkg = /node_modules\/jsonresume-theme-[^/]+\/[^/]*\.js$/.test(args.path);
443
- const matchingPatches = LEGACY_PATCHES.filter((p) => p.filter.test(args.path));
444
- if (!isThemePkg && matchingPatches.length === 0) return null;
445
-
446
- const original = await readFile(args.path, 'utf8');
447
- let contents = original;
448
-
449
- // 1. Generic rewrite for theme packages: a line-leading `ident = expr`
450
- // (where `ident` is not a JS keyword) becomes `var ident = expr`. ESM
451
- // modules are strict-mode-only and throw `ReferenceError` on implicit
452
- // global assignments, themes that skip `var` (e.g. onepage's
453
- // `COURSES_COLUMNS = 3` at module scope, riga's `w = resume.work[i]`
454
- // inside a for-loop) die at runtime without this.
455
- //
456
- // Property-chain assignments (`foo.bar = x`) don't match, the dot
457
- // breaks the identifier before we reach `=`. The `(?!=)` guard skips
458
- // `==` comparisons.
459
- if (isThemePkg) {
460
- const RESERVED = new Set([
461
- 'var', 'let', 'const', 'function', 'return', 'if', 'else', 'for',
462
- 'while', 'do', 'switch', 'case', 'default', 'break', 'continue',
463
- 'throw', 'try', 'catch', 'finally', 'new', 'delete', 'typeof',
464
- 'void', 'in', 'of', 'instanceof', 'class', 'extends', 'super',
465
- 'this', 'import', 'export', 'from', 'as', 'async', 'await',
466
- 'yield', 'true', 'false', 'null', 'undefined', 'debugger',
467
- ]);
468
- contents = contents.replace(
469
- /^([ \t]*)([a-zA-Z_$][\w$]*)\s*=(?!=)/gm,
470
- (match, indent, name, offset, full) => {
471
- if (RESERVED.has(name)) return match;
472
- // Skip expression-context assignments (not a new statement):
473
- // - `var a, b,\n c = ...` (multi-var continuation)
474
- // - `foo(\n bar = 1\n)` (argument expression)
475
- // - `[\n x = 1\n]` (array element assignment)
476
- // - `a ?\n b = 1 :\n c = 2` (ternary branch)
477
- // - `a ||\n b = 1` (short-circuit)
478
- // A new statement context, on the other hand, lives after
479
- // `{` `}` `;`, in those cases the rewrite is correct.
480
- const before = full.slice(0, offset).replace(/\s+$/, '');
481
- const prevChar = before.charAt(before.length - 1);
482
- if (prevChar === ',' || prevChar === '(' || prevChar === '[' ||
483
- prevChar === '?' || prevChar === ':') return match;
484
- const prev2 = before.slice(-2);
485
- if (prev2 === '||' || prev2 === '&&' || prev2 === '=>') return match;
486
- // Inside `for (` init clause, rare but valid (var isn't allowed
487
- // there unless at the very start, and esbuild will handle the
488
- // already-correct `for (var i = 0; ...)` form).
489
- const lineStart = full.lastIndexOf('\n', offset - 1) + 1;
490
- const lineBefore = full.slice(lineStart, offset);
491
- if (/\bfor\s*\(\s*$/.test(lineBefore)) return match;
492
- return `${indent}var ${name} =`;
493
- }
494
- );
495
- }
496
-
497
- // 2. Targeted source patches (layered on top of the generic rewrite)
498
- for (const patch of matchingPatches) contents = patch.transform(contents);
499
-
500
- // 3. Resolve dynamic requires statically so esbuild can bundle them.
501
- // Two common patterns in themes:
502
- // a) const HELPERS = join(__dirname, 'theme/hbs-helpers');
503
- // require(join(HELPERS, 'file.js'))
504
- // b) require(path.join(__dirname, 'dist/index.cjs'))
505
- // esbuild leaves computed requires as runtime calls, which our
506
- // browser shim rejects. Rewrite to static relative paths.
507
- if (isThemePkg) {
508
- // Pattern (b): inline `require((path.)?join(__dirname, 'literal'))`
509
- contents = contents.replace(
510
- /require\(\s*(?:\w+\.)?join\(\s*__dirname\s*,\s*['"]([^'"]+)['"]\s*\)\s*\)/g,
511
- (_, rel) => `require(${JSON.stringify('./' + rel)})`,
512
- );
513
- // Pattern (a): resolve via tracked join-constants.
514
- const joinConsts = {};
515
- for (const m of contents.matchAll(
516
- /\b(?:const|var|let)\s+(\w+)\s*=\s*(?:\w+\.)?join\(\s*__dirname\s*,\s*['"]([^'"]+)['"]\s*\)/g
517
- )) {
518
- joinConsts[m[1]] = m[2];
519
- }
520
- if (Object.keys(joinConsts).length > 0) {
521
- contents = contents.replace(
522
- /require\(\s*(?:\w+\.)?join\(\s*(\w+)\s*,\s*['"]([^'"]+)['"]\s*\)\s*\)/g,
523
- (match, name, file) => {
524
- const prefix = joinConsts[name];
525
- if (prefix === undefined) return match;
526
- const rel = `./${prefix}/${file}`.replace(/\/+/g, '/');
527
- return `require(${JSON.stringify(rel)})`;
528
- }
529
- );
530
- }
531
- }
532
-
533
- return contents !== original ? { contents, loader: 'js' } : null;
534
- });
535
- },
536
- };
537
- }
538
-
539
- async function bundleTheme(shortName, packageName, shimsDir) {
540
- const themeDir = resolve(__dirname, `../node_modules/${packageName}`);
541
-
542
- // Generate per-theme fs shim with embedded files
543
- const themeFiles = collectThemeFiles(themeDir);
544
- writeFileSync(resolve(shimsDir, 'fs.js'), generateThemeFsShim(themeFiles));
545
-
546
- const entryContent = `
547
- import * as themeNs from '${packageName}';
548
- const _t = themeNs.default ?? themeNs;
549
- export const render = _t.render ?? themeNs.render;
550
- export const pdfRenderOptions = _t.pdfRenderOptions ?? themeNs.pdfRenderOptions;
551
- `;
552
-
553
- const entryFile = resolve(THEMES_DIR, `_entry_${shortName}.js`);
554
- writeFileSync(entryFile, entryContent);
555
-
556
- try {
557
- await build({
558
- entryPoints: [entryFile],
559
- bundle: true,
560
- minify: true,
561
- format: 'esm',
562
- target: 'es2022',
563
- platform: 'browser',
564
- conditions: ['browser', 'require', 'default'],
565
- mainFields: ['browser', 'main'],
566
- outfile: resolve(THEMES_DIR, `${shortName}.js`),
567
- // Runtime prelude prepended to every bundled theme:
568
- // - `require.extensions` is a Node CJS API that handlebars-wax uses
569
- // unconditionally (to intercept .handlebars/.hbs during require).
570
- // In browser ESM, `require` is undefined, so we stub it.
571
- // - We deliberately do NOT alias `window = globalThis`. Legacy CJS
572
- // packages like `swag` branch on `typeof window`, if window is
573
- // defined they assign `window.Swag = Swag = {}` and skip setting
574
- // `module.exports`, leaving the theme with `Swag.registerHelpers`
575
- // undefined. We want the module-exports branch in workers.
576
- banner: {
577
- js: [
578
- // `require.extensions` is read by handlebars-wax at module load; no
579
- // ESM equivalent, so we provide a stub.
580
- 'var require = globalThis.require || (function(){ var r = function(){ throw new Error("require not available in browser"); }; r.extensions = {}; r.cache = {}; return r; })();',
581
- 'if (!require.extensions) require.extensions = {};',
582
- // Runtime process shim: `process.env.X` literal access is handled
583
- // by esbuild's `define`, but dynamic access like `process.cwd()` or
584
- // `process.stdout.write` needs an actual object at runtime.
585
- 'if (typeof globalThis.process === "undefined") globalThis.process = { env: { NODE_ENV: "production" }, browser: true, platform: "browser", version: "v20.0.0", versions: { node: "20.0.0", v8: "11.3.0" }, stdout: { write: function(){} }, stderr: { write: function(){} }, cwd: function(){ return "/"; }, chdir: function(){}, nextTick: function(fn){ Promise.resolve().then(fn); }, argv: [], pid: 1, title: "browser" };',
586
- ].join(''),
587
- },
588
- define: {
589
- 'process.env.NODE_ENV': '"production"',
590
- 'process.env.LANG': '""',
591
- 'global': 'globalThis',
592
- '__dirname': '"/"',
593
- '__filename': '"/index.js"',
594
- 'process.browser': 'true',
595
- 'process.platform': '"browser"',
596
- 'process.version': '"v20.0.0"',
597
- 'process.versions': '{}',
598
- 'process.stdout': 'false',
599
- 'process.stderr': 'false',
600
- },
601
- alias: {
602
- 'path': resolve(shimsDir, 'path.js'),
603
- 'fs': resolve(shimsDir, 'fs.js'),
604
- 'url': resolve(shimsDir, 'url.js'),
605
- 'assert': resolve(shimsDir, 'assert.js'),
606
- 'buffer': resolve(shimsDir, 'buffer.js'),
607
- 'stream': resolve(shimsDir, 'stream.js'),
608
- 'util': resolve(shimsDir, 'util.js'),
609
- 'events': resolve(shimsDir, 'events.js'),
610
- 'os': resolve(shimsDir, 'os.js'),
611
- // node: prefixed variants
612
- 'node:fs': resolve(shimsDir, 'fs.js'),
613
- 'node:path': resolve(shimsDir, 'path.js'),
614
- 'node:url': resolve(shimsDir, 'url.js'),
615
- 'node:crypto': resolve(shimsDir, 'crypto.js'),
616
- 'node:buffer': resolve(shimsDir, 'buffer.js'),
617
- 'node:stream': resolve(shimsDir, 'stream.js'),
618
- 'node:util': resolve(shimsDir, 'util.js'),
619
- 'node:events': resolve(shimsDir, 'events.js'),
620
- 'node:os': resolve(shimsDir, 'os.js'),
621
- 'node:assert': resolve(shimsDir, 'assert.js'),
622
- // Glob shims for themes that use glob-based partial loading
623
- 'glob': resolve(shimsDir, 'glob.js'),
624
- 'globby': resolve(shimsDir, 'globby.js'),
625
- 'require-glob': resolve(shimsDir, 'require-glob.js'),
626
- 'fast-glob': resolve(shimsDir, 'globby.js'),
627
- },
628
- plugins: [legacyThemeGlobalsPlugin()],
629
- logLevel: 'silent',
630
- });
631
-
632
- execSync(`rm -f "${entryFile}"`);
633
- return true;
634
- } catch (e) {
635
- console.log(` (bundle error: ${e.message})`);
636
- execSync(`rm -f "${entryFile}"`);
637
- return false;
638
- }
639
- }
640
-
641
- // ── Shim file writers ────────────────────────────────────────────────
642
-
643
- function writeShims(shimsDir) {
644
- writeFileSync(resolve(shimsDir, 'path.js'), [
645
- 'export var join = function() { return [].slice.call(arguments).join("/"); };',
646
- 'export var resolve = function() { return [].slice.call(arguments).join("/"); };',
647
- 'export var dirname = function(p) { return p.split("/").slice(0, -1).join("/"); };',
648
- 'export var basename = function(p, ext) { var b = p.split("/").pop() || ""; return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; };',
649
- 'export var extname = function(p) { var m = p.match(/\\.[^.]+$/); return m ? m[0] : ""; };',
650
- 'export var sep = "/";',
651
- 'export var isAbsolute = function(p) { return p.charAt(0) === "/"; };',
652
- 'export var normalize = function(p) { return p; };',
653
- 'export var relative = function(from, to) { return to; };',
654
- 'export var parse = function(p) { return { root: "", dir: dirname(p), base: basename(p), ext: extname(p), name: basename(p, extname(p)) }; };',
655
- 'export default { join: join, resolve: resolve, dirname: dirname, basename: basename, extname: extname, sep: sep, isAbsolute: isAbsolute, normalize: normalize, relative: relative, parse: parse };',
656
- ].join('\n'));
657
-
658
- // fs shim is generated per-theme in bundleTheme()
659
-
660
- writeFileSync(resolve(shimsDir, 'url.js'), [
661
- 'export var URL = globalThis.URL;',
662
- 'export var URLSearchParams = globalThis.URLSearchParams;',
663
- 'export var fileURLToPath = function(u) { return u.replace(/^file:\\/\\//, ""); };',
664
- 'export var pathToFileURL = function(p) { return new globalThis.URL("file://" + p); };',
665
- 'export var format = function(u) { return typeof u === "string" ? u : u.href; };',
666
- 'export var parse = function(u) { return new globalThis.URL(u); };',
667
- 'export default { URL: URL, URLSearchParams: URLSearchParams, fileURLToPath: fileURLToPath, pathToFileURL: pathToFileURL, format: format, parse: parse };',
668
- ].join('\n'));
669
-
670
- writeFileSync(resolve(shimsDir, 'crypto.js'), [
671
- 'export var createHash = function() { return { update: function() { return this; }, digest: function() { return ""; } }; };',
672
- 'export var randomBytes = function(n) { return new Uint8Array(n); };',
673
- 'export var createHmac = function() { return { update: function() { return this; }, digest: function() { return ""; } }; };',
674
- 'export default { createHash: createHash, randomBytes: randomBytes, createHmac: createHmac };',
675
- ].join('\n'));
676
-
677
- writeFileSync(resolve(shimsDir, 'assert.js'), [
678
- 'var assert = function(v, msg) { if (!v) throw new Error(msg || "Assertion failed"); };',
679
- 'assert.ok = assert;',
680
- 'assert.strictEqual = function(a, b) { if (a !== b) throw new Error("Not equal"); };',
681
- 'assert.deepStrictEqual = function() {};',
682
- 'assert.fail = function(msg) { throw new Error(msg); };',
683
- 'export default assert;',
684
- 'export var ok = assert;',
685
- 'export var strictEqual = assert.strictEqual;',
686
- ].join('\n'));
687
-
688
- writeFileSync(resolve(shimsDir, 'buffer.js'), [
689
- 'var _enc = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;',
690
- 'var _dec = typeof TextDecoder !== "undefined" ? new TextDecoder() : null;',
691
- '',
692
- 'function BufferShim(arg) {',
693
- ' if (typeof arg === "number") return new Uint8Array(arg);',
694
- ' if (arg instanceof Uint8Array) return arg;',
695
- ' if (arg instanceof ArrayBuffer) return new Uint8Array(arg);',
696
- ' return new Uint8Array(0);',
697
- '}',
698
- '',
699
- 'BufferShim.from = function(data, encoding) {',
700
- ' if (typeof data === "string") {',
701
- ' if (encoding === "base64") {',
702
- ' var bin = atob(data);',
703
- ' var bytes = new Uint8Array(bin.length);',
704
- ' for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);',
705
- ' return bytes;',
706
- ' }',
707
- ' return _enc ? _enc.encode(data) : new Uint8Array(0);',
708
- ' }',
709
- ' if (Array.isArray(data)) return new Uint8Array(data);',
710
- ' if (data instanceof ArrayBuffer) return new Uint8Array(data);',
711
- ' if (data instanceof Uint8Array) return new Uint8Array(data);',
712
- ' return new Uint8Array(0);',
713
- '};',
714
- 'BufferShim.alloc = function(size) { return new Uint8Array(size); };',
715
- 'BufferShim.allocUnsafe = function(size) { return new Uint8Array(size); };',
716
- 'BufferShim.isBuffer = function(obj) { return obj instanceof Uint8Array; };',
717
- 'BufferShim.isEncoding = function() { return true; };',
718
- 'BufferShim.byteLength = function(str) { return _enc ? _enc.encode(str).length : str.length; };',
719
- 'BufferShim.concat = function(list) {',
720
- ' var total = 0;',
721
- ' for (var i = 0; i < list.length; i++) total += list[i].length;',
722
- ' var result = new Uint8Array(total);',
723
- ' var offset = 0;',
724
- ' for (var j = 0; j < list.length; j++) { result.set(list[j], offset); offset += list[j].length; }',
725
- ' return result;',
726
- '};',
727
- '',
728
- 'export var Buffer = BufferShim;',
729
- 'export var SlowBuffer = BufferShim;',
730
- 'export var INSPECT_MAX_BYTES = 50;',
731
- 'export var kMaxLength = 2147483647;',
732
- 'export default { Buffer: BufferShim, SlowBuffer: BufferShim, INSPECT_MAX_BYTES: 50, kMaxLength: 2147483647 };',
733
- ].join('\n'));
734
-
735
- writeFileSync(resolve(shimsDir, 'stream.js'), [
736
- 'function EventBase() { this._e = {}; }',
737
- 'EventBase.prototype.on = function(ev, fn) { (this._e[ev] = this._e[ev] || []).push(fn); return this; };',
738
- 'EventBase.prototype.addListener = EventBase.prototype.on;',
739
- '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); };',
740
- 'EventBase.prototype.emit = function(ev) { var args = [].slice.call(arguments, 1); (this._e[ev] || []).forEach(function(fn) { fn.apply(null, args); }); return true; };',
741
- '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; };',
742
- 'EventBase.prototype.off = EventBase.prototype.removeListener;',
743
- 'EventBase.prototype.removeAllListeners = function(ev) { if (ev) delete this._e[ev]; else this._e = {}; return this; };',
744
- 'EventBase.prototype.setMaxListeners = function() { return this; };',
745
- 'EventBase.prototype.listeners = function(ev) { return this._e[ev] || []; };',
746
- '',
747
- 'function Readable() { EventBase.call(this); } Readable.prototype = Object.create(EventBase.prototype);',
748
- 'Readable.prototype.read = function() { return null; };',
749
- 'Readable.prototype.pipe = function(d) { return d; };',
750
- 'Readable.prototype.unpipe = function() { return this; };',
751
- 'Readable.prototype.destroy = function() { return this; };',
752
- '',
753
- 'function Writable() { EventBase.call(this); } Writable.prototype = Object.create(EventBase.prototype);',
754
- 'Writable.prototype.write = function() { return true; };',
755
- 'Writable.prototype.end = function() { return this; };',
756
- 'Writable.prototype.destroy = function() { return this; };',
757
- '',
758
- 'function Transform() { EventBase.call(this); } Transform.prototype = Object.create(EventBase.prototype);',
759
- 'Transform.prototype.write = function() { return true; };',
760
- 'Transform.prototype.end = function() { return this; };',
761
- 'Transform.prototype.pipe = function(d) { return d; };',
762
- 'Transform.prototype.destroy = function() { return this; };',
763
- '',
764
- 'function PassThrough() { Transform.call(this); } PassThrough.prototype = Object.create(Transform.prototype);',
765
- 'function Duplex() { EventBase.call(this); } Duplex.prototype = Object.create(EventBase.prototype);',
766
- 'Duplex.prototype.write = function() { return true; };',
767
- 'Duplex.prototype.end = function() { return this; };',
768
- 'Duplex.prototype.read = function() { return null; };',
769
- 'Duplex.prototype.pipe = function(d) { return d; };',
770
- 'Duplex.prototype.destroy = function() { return this; };',
771
- '',
772
- 'function Stream() { EventBase.call(this); } Stream.prototype = Object.create(EventBase.prototype);',
773
- 'Stream.prototype.pipe = function(d) { return d; };',
774
- 'Stream.Readable = Readable; Stream.Writable = Writable; Stream.Transform = Transform;',
775
- 'Stream.PassThrough = PassThrough; Stream.Duplex = Duplex; Stream.Stream = Stream;',
776
- '',
777
- 'export { Readable, Writable, Transform, PassThrough, Duplex, Stream };',
778
- 'export default Stream;',
779
- ].join('\n'));
780
-
781
- writeFileSync(resolve(shimsDir, 'util.js'), [
782
- 'export var inherits = function(ctor, superCtor) { ctor.super_ = superCtor; Object.setPrototypeOf(ctor.prototype, superCtor.prototype); };',
783
- 'export var deprecate = function(fn) { return fn; };',
784
- '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); }); }; };',
785
- 'export var inspect = function(obj) { try { return JSON.stringify(obj); } catch(e) { return String(obj); } };',
786
- '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; }); };',
787
- 'export var debuglog = function() { return function() {}; };',
788
- 'export var isArray = Array.isArray;',
789
- 'export var isBoolean = function(v) { return typeof v === "boolean"; };',
790
- 'export var isNull = function(v) { return v === null; };',
791
- 'export var isNumber = function(v) { return typeof v === "number"; };',
792
- 'export var isString = function(v) { return typeof v === "string"; };',
793
- 'export var isUndefined = function(v) { return v === undefined; };',
794
- 'export var isObject = function(v) { return typeof v === "object" && v !== null; };',
795
- 'export var isFunction = function(v) { return typeof v === "function"; };',
796
- 'export var isRegExp = function(v) { return v instanceof RegExp; };',
797
- 'export var isDate = function(v) { return v instanceof Date; };',
798
- 'export var isError = function(v) { return v instanceof Error; };',
799
- 'export var isPrimitive = function(v) { return v === null || typeof v !== "object" && typeof v !== "function"; };',
800
- 'export var TextEncoder = globalThis.TextEncoder;',
801
- 'export var TextDecoder = globalThis.TextDecoder;',
802
- 'export var types = { isAnyArrayBuffer: function() { return false; }, isTypedArray: function(v) { return ArrayBuffer.isView(v); } };',
803
- '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 };',
804
- ].join('\n'));
805
-
806
- writeFileSync(resolve(shimsDir, 'events.js'), [
807
- 'function EventEmitter() { this._events = {}; this._maxListeners = 10; }',
808
- 'EventEmitter.prototype.on = function(ev, fn) { (this._events[ev] = this._events[ev] || []).push(fn); return this; };',
809
- 'EventEmitter.prototype.addListener = EventEmitter.prototype.on;',
810
- '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); };',
811
- 'EventEmitter.prototype.emit = function(ev) { var args = [].slice.call(arguments, 1); (this._events[ev] || []).forEach(function(fn) { fn.apply(null, args); }); return true; };',
812
- '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; };',
813
- 'EventEmitter.prototype.off = EventEmitter.prototype.removeListener;',
814
- 'EventEmitter.prototype.removeAllListeners = function(ev) { if (ev) delete this._events[ev]; else this._events = {}; return this; };',
815
- 'EventEmitter.prototype.setMaxListeners = function(n) { this._maxListeners = n; return this; };',
816
- 'EventEmitter.prototype.getMaxListeners = function() { return this._maxListeners; };',
817
- 'EventEmitter.prototype.listeners = function(ev) { return this._events[ev] || []; };',
818
- 'EventEmitter.prototype.listenerCount = function(ev) { return (this._events[ev] || []).length; };',
819
- 'EventEmitter.prototype.prependListener = EventEmitter.prototype.on;',
820
- 'EventEmitter.prototype.prependOnceListener = EventEmitter.prototype.once;',
821
- 'EventEmitter.prototype.eventNames = function() { return Object.keys(this._events); };',
822
- 'EventEmitter.EventEmitter = EventEmitter;',
823
- 'EventEmitter.defaultMaxListeners = 10;',
824
- 'export { EventEmitter };',
825
- 'export default EventEmitter;',
826
- ].join('\n'));
827
-
828
- writeFileSync(resolve(shimsDir, 'os.js'), [
829
- 'export var platform = function() { return "browser"; };',
830
- 'export var tmpdir = function() { return "/tmp"; };',
831
- 'export var homedir = function() { return "/"; };',
832
- 'export var hostname = function() { return "localhost"; };',
833
- 'export var type = function() { return "Browser"; };',
834
- 'export var arch = function() { return "wasm"; };',
835
- 'export var release = function() { return "0.0.0"; };',
836
- 'export var EOL = "\\n";',
837
- 'export var endianness = function() { return "LE"; };',
838
- 'export var cpus = function() { return []; };',
839
- 'export var totalmem = function() { return 0; };',
840
- 'export var freemem = function() { return 0; };',
841
- 'export var networkInterfaces = function() { return {}; };',
842
- 'export var userInfo = function() { return { username: "browser", uid: 0, gid: 0, shell: "", homedir: "/" }; };',
843
- '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 };',
844
- ].join('\n'));
845
-
846
- // glob/globby/require-glob shims, these use the fs shim's embedded file list
847
- writeFileSync(resolve(shimsDir, 'glob.js'), [
848
- 'import { readdirSync, readFileSync, existsSync } from "fs";',
849
- '',
850
- '// Minimal glob pattern matching against embedded file list',
851
- 'function collapseSlashes(p) { return String(p).replace(/\\/+/g, "/"); }',
852
- 'function minimatch(filepath, pattern) {',
853
- ' filepath = collapseSlashes(filepath);',
854
- ' pattern = collapseSlashes(pattern);',
855
- ' // Step 1: expand `{a,b,c}` alternatives into placeholders BEFORE',
856
- ' // escaping, otherwise the escape step turns `{hbs,js}` into',
857
- ' // `\\{hbs,js\\}` and the alternation regex no longer sees braces.',
858
- ' var alts = [];',
859
- ' pattern = pattern.replace(/\\{([^}]+)\\}/g, function(_, list) {',
860
- ' alts.push("(" + list.split(",").join("|") + ")");',
861
- ' return "___ALT" + (alts.length - 1) + "___";',
862
- ' });',
863
- ' // Step 2: handle globstars. `**/` matches zero or more full path',
864
- ' // segments (so `a/**/b` matches `a/b`, `a/x/b`, `a/x/y/b`). Lone `**`',
865
- ' // (no trailing slash) matches any chars including `/`.',
866
- ' pattern = pattern.replace(/\\*\\*\\//g, "___GLOBSTAR_SEG___");',
867
- ' pattern = pattern.replace(/\\*\\*/g, "___GLOBSTAR___");',
868
- ' // Step 3: escape remaining regex metachars (but not *, ?, /)',
869
- ' var re = pattern',
870
- ' .replace(/[.+^$()|[\\]\\\\]/g, "\\\\$&")',
871
- ' .replace(/\\*/g, "[^/]*")',
872
- ' .replace(/\\?/g, "[^/]");',
873
- ' re = re.replace(/___GLOBSTAR_SEG___/g, "(?:.*/)?");',
874
- ' re = re.replace(/___GLOBSTAR___/g, ".*");',
875
- ' // Step 4: restore alternations',
876
- ' re = re.replace(/___ALT(\\d+)___/g, function(_, i) { return alts[Number(i)]; });',
877
- ' try { return new RegExp("^" + re + "$").test(filepath); }',
878
- ' catch (e) { return false; }',
879
- '}',
880
- '',
881
- 'function joinPath(dir, name) {',
882
- ' if (!dir || dir === "/") return "/" + name;',
883
- ' if (dir.charAt(dir.length - 1) === "/") return dir + name;',
884
- ' return dir + "/" + name;',
885
- '}',
886
- 'function getAllPaths(dir) {',
887
- ' var results = [];',
888
- ' try {',
889
- ' var entries = readdirSync(dir);',
890
- ' for (var i = 0; i < entries.length; i++) {',
891
- ' var full = joinPath(dir, entries[i]);',
892
- ' results.push(full);',
893
- ' var sub = getAllPaths(full);',
894
- ' for (var j = 0; j < sub.length; j++) results.push(sub[j]);',
895
- ' }',
896
- ' } catch(e) {}',
897
- ' return results;',
898
- '}',
899
- '',
900
- 'export function sync(pattern, opts) {',
901
- ' var cwd = (opts && opts.cwd) || "/";',
902
- ' var allPaths = getAllPaths(cwd);',
903
- ' return allPaths.filter(function(p) { return minimatch(p, pattern); });',
904
- '}',
905
- '',
906
- 'export function globSync(pattern, opts) { return sync(pattern, opts); }',
907
- '',
908
- 'export default function glob(pattern, opts, cb) {',
909
- ' if (typeof opts === "function") { cb = opts; opts = {}; }',
910
- ' try { var r = sync(pattern, opts); if (cb) cb(null, r); return Promise.resolve(r); }',
911
- ' catch(e) { if (cb) cb(e); return Promise.reject(e); }',
912
- '};',
913
- 'glob.sync = sync;',
914
- 'glob.globSync = sync;',
915
- ].join('\n'));
916
-
917
- writeFileSync(resolve(shimsDir, 'globby.js'), [
918
- 'import { sync as globSync } from "glob";',
919
- 'export function globby(patterns, opts) {',
920
- ' var pats = Array.isArray(patterns) ? patterns : [patterns];',
921
- ' var results = [];',
922
- ' for (var i = 0; i < pats.length; i++) {',
923
- ' var matches = globSync(pats[i], opts);',
924
- ' for (var j = 0; j < matches.length; j++) {',
925
- ' if (results.indexOf(matches[j]) === -1) results.push(matches[j]);',
926
- ' }',
927
- ' }',
928
- ' return Promise.resolve(results);',
929
- '}',
930
- 'export function globbySync(patterns, opts) {',
931
- ' var pats = Array.isArray(patterns) ? patterns : [patterns];',
932
- ' var results = [];',
933
- ' for (var i = 0; i < pats.length; i++) {',
934
- ' var matches = globSync(pats[i], opts);',
935
- ' for (var j = 0; j < matches.length; j++) {',
936
- ' if (results.indexOf(matches[j]) === -1) results.push(matches[j]);',
937
- ' }',
938
- ' }',
939
- ' return results;',
940
- '}',
941
- 'export default globby;',
942
- ].join('\n'));
943
-
944
- // require-glob: export as a function with a `.sync` PROPERTY. Consumers do
945
- // `var requireGlob = require('require-glob'); requireGlob.sync(pattern)`,
946
- // the property must survive esbuild's CJS/ESM interop. Default-only export
947
- // would strip `.sync`, so we use a proxy-like object pattern: default is
948
- // the function, and the function has `.sync` set before export.
949
- writeFileSync(resolve(shimsDir, 'require-glob.js'), [
950
- 'import { sync as globSync } from "glob";',
951
- 'import { readFileSync } from "fs";',
952
- '',
953
- 'function requireGlob(pattern, opts) {',
954
- ' var cwd = (opts && opts.cwd) || "/";',
955
- ' var matches = globSync(pattern, { cwd: cwd });',
956
- ' var result = {};',
957
- ' for (var i = 0; i < matches.length; i++) {',
958
- ' var name = matches[i].replace(/.*\\//, "").replace(/\\.[^.]+$/, "");',
959
- ' try {',
960
- ' var content = readFileSync(matches[i], "utf-8");',
961
- ' result[name] = content;',
962
- ' } catch(e) {}',
963
- ' }',
964
- ' return result;',
965
- '}',
966
- 'requireGlob.sync = requireGlob;',
967
- '// Named export preserves `.sync` through esbuild\'s CJS interop,',
968
- '// consumers that `require(\'require-glob\')` receive the function itself.',
969
- 'export { requireGlob as default };',
970
- 'export { requireGlob };',
971
- 'export var sync = requireGlob;',
972
- ].join('\n'));
973
- }
974
-
975
- // ── Main ─────────────────────────────────────────────────────────────
976
-
977
- async function main() {
978
- const args = process.argv.slice(2);
979
- const doAll = args.includes('--all');
980
- const themesArg = args.find(a => a.startsWith('--themes='));
981
- const specificThemes = themesArg ? themesArg.split('=')[1].split(',') : null;
982
-
983
- mkdirSync(THEMES_DIR, { recursive: true });
984
-
985
- // Create shims directory and write all shims
986
- const shimsDir = resolve(__dirname, 'shims');
987
- mkdirSync(shimsDir, { recursive: true });
988
- writeShims(shimsDir);
989
-
990
- let themes;
991
- if (specificThemes) {
992
- themes = specificThemes.map(name => ({
993
- name,
994
- packageName: `jsonresume-theme-${name}`,
995
- description: '',
996
- version: '',
997
- }));
998
- } else if (doAll) {
999
- themes = await discoverThemes();
1000
- } else {
1001
- themes = POPULAR_THEMES.map(name => ({
1002
- name,
1003
- packageName: `jsonresume-theme-${name}`,
1004
- description: '',
1005
- version: '',
1006
- }));
1007
- }
1008
-
1009
- console.log(`📦 Bundling ${themes.length} themes...\n`);
1010
-
1011
- const manifest = [];
1012
-
1013
- for (const theme of themes) {
1014
- process.stdout.write(` ${theme.name}... `);
1015
-
1016
- // Install the theme
1017
- try {
1018
- execSync(`npm install --no-save --prefer-offline ${theme.packageName} 2>/dev/null`, {
1019
- cwd: resolve(__dirname, '..'),
1020
- timeout: 60000,
1021
- });
1022
- } catch {
1023
- console.log('❌ install failed');
1024
- continue;
1025
- }
1026
-
1027
- // Read package metadata
1028
- try {
1029
- const pkgPath = resolve(__dirname, `../node_modules/${theme.packageName}/package.json`);
1030
- if (existsSync(pkgPath)) {
1031
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
1032
- theme.description = theme.description || pkg.description || '';
1033
- theme.version = theme.version || pkg.version || '';
1034
- }
1035
- } catch {}
1036
-
1037
- // Step 1: Generate snapshot (pre-render in Node.js)
1038
- let hasSnapshot = false;
1039
- let hasCss = false;
1040
- const snapshotResult = generateSnapshot(theme.name, theme.packageName);
1041
- if (snapshotResult && typeof snapshotResult.then === 'function') {
1042
- const html = await snapshotResult;
1043
- hasSnapshot = !!html;
1044
- hasCss = existsSync(resolve(THEMES_DIR, `${theme.name}.css`));
1045
- } else {
1046
- hasSnapshot = !!snapshotResult;
1047
- hasCss = existsSync(resolve(THEMES_DIR, `${theme.name}.css`));
1048
- }
1049
-
1050
- // Step 2: Bundle for browser
1051
- const success = await bundleTheme(theme.name, theme.packageName, shimsDir);
1052
-
1053
- if (success) {
1054
- const outFile = resolve(THEMES_DIR, `${theme.name}.js`);
1055
- const stat = existsSync(outFile) ? readFileSync(outFile).length : 0;
1056
- const cssFile = resolve(THEMES_DIR, `${theme.name}.css`);
1057
- const cssSize = hasCss && existsSync(cssFile) ? readFileSync(cssFile).length : 0;
1058
-
1059
- manifest.push({
1060
- name: theme.name,
1061
- displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
1062
- description: theme.description,
1063
- version: theme.version || '',
1064
- browserCompatible: true,
1065
- hasSnapshot,
1066
- hasCss,
1067
- fileSize: stat,
1068
- cssSize,
1069
- });
1070
-
1071
- const parts = [`✅ (${(stat / 1024).toFixed(0)}KB`];
1072
- if (hasCss) parts.push(`css:${(cssSize / 1024).toFixed(0)}KB`);
1073
- if (hasSnapshot) parts.push('snapshot');
1074
- console.log(parts.join(', ') + ')');
1075
- } else {
1076
- manifest.push({
1077
- name: theme.name,
1078
- displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
1079
- description: theme.description,
1080
- version: theme.version || '',
1081
- browserCompatible: false,
1082
- hasSnapshot,
1083
- hasCss: false,
1084
- fileSize: 0,
1085
- cssSize: 0,
1086
- });
1087
- const note = hasSnapshot ? '⚠️ browser incompatible (has snapshot)' : '⚠️ browser incompatible';
1088
- console.log(note);
1089
- }
1090
- }
1091
-
1092
- // Write manifest
1093
- writeFileSync(resolve(THEMES_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
1094
-
1095
- const successful = manifest.filter(t => t.browserCompatible).length;
1096
- const snapshots = manifest.filter(t => t.hasSnapshot).length;
1097
- console.log(`\n✅ Done! ${successful}/${manifest.length} themes bundled, ${snapshots} snapshots generated`);
1098
- console.log(`📁 Output: docs/themes/`);
1099
-
1100
- // Clean up shims
1101
- execSync(`rm -rf "${shimsDir}"`);
1102
- }
1103
-
1104
- main().catch(console.error);