resuml 1.3.1 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +93 -1
- package/bin/resuml +21 -0
- package/dist/{api.js → api.cjs} +50 -45
- package/dist/api.cjs.map +1 -0
- package/dist/api.d.cts +9 -0
- package/dist/{index.js → index.cjs} +1180 -118
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +88 -0
- package/dist/{loadResume-BFCirLAW.d.ts → themeLoader-C7CBqNiC.d.cts} +14 -1
- package/package.json +53 -47
- package/scripts/build-builder.js +25 -0
- package/scripts/bundle-themes.js +233 -0
- package/scripts/dev-server.js +392 -0
- package/scripts/quick-bundle.cjs +129 -0
- package/dist/api.d.ts +0 -8
- package/dist/api.js.map +0 -1
- package/dist/index.d.ts +0 -55
- package/dist/index.js.map +0 -1
- /package/scripts/{generate-types.js → generate-types.cjs} +0 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local dev server for the resuml builder.
|
|
3
|
+
* - Watches & rebuilds the frontend with esbuild
|
|
4
|
+
* - Serves docs/ statically
|
|
5
|
+
* - Handles /api/render, /api/themes locally (same logic as Vercel functions)
|
|
6
|
+
*
|
|
7
|
+
* Usage: node scripts/dev-server.js [--port 3000]
|
|
8
|
+
*/
|
|
9
|
+
import http from 'node:http';
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
import { execFileSync } from 'node:child_process';
|
|
16
|
+
import { context } from 'esbuild';
|
|
17
|
+
|
|
18
|
+
const require = createRequire(import.meta.url);
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
22
|
+
const DOCS_DIR = path.resolve(ROOT, 'docs');
|
|
23
|
+
const PORT = parseInt(process.argv.includes('--port') ? process.argv[process.argv.indexOf('--port') + 1] : '3000', 10);
|
|
24
|
+
|
|
25
|
+
// ── MIME types ──────────────────────────────────────────────────────
|
|
26
|
+
const MIME = {
|
|
27
|
+
'.html': 'text/html',
|
|
28
|
+
'.js': 'text/javascript',
|
|
29
|
+
'.mjs': 'text/javascript',
|
|
30
|
+
'.css': 'text/css',
|
|
31
|
+
'.json': 'application/json',
|
|
32
|
+
'.png': 'image/png',
|
|
33
|
+
'.jpg': 'image/jpeg',
|
|
34
|
+
'.svg': 'image/svg+xml',
|
|
35
|
+
'.ico': 'image/x-icon',
|
|
36
|
+
'.map': 'application/json',
|
|
37
|
+
'.woff': 'font/woff',
|
|
38
|
+
'.woff2': 'font/woff2',
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// ── Theme helpers (CDN-based: fetch tarball + extract + install deps) ──
|
|
42
|
+
const SAFE_NAME = /^[a-zA-Z0-9._-]+$/;
|
|
43
|
+
const THEME_CACHE_DIR = path.join(os.tmpdir(), 'resuml-themes');
|
|
44
|
+
|
|
45
|
+
function toPackageName(theme) {
|
|
46
|
+
if (!SAFE_NAME.test(theme)) throw new Error('Invalid theme name');
|
|
47
|
+
return theme.startsWith('jsonresume-theme-') ? theme : `jsonresume-theme-${theme}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureThemeInstalled(theme) {
|
|
51
|
+
const pkg = toPackageName(theme);
|
|
52
|
+
const pkgDir = path.join(THEME_CACHE_DIR, pkg);
|
|
53
|
+
|
|
54
|
+
if (fs.existsSync(path.join(pkgDir, 'package.json'))) {
|
|
55
|
+
return pkgDir;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log(`📦 Downloading ${pkg} from npm registry...`);
|
|
59
|
+
|
|
60
|
+
// Fetch tarball URL
|
|
61
|
+
const metaRes = await fetch(`https://registry.npmjs.org/${pkg}/latest`);
|
|
62
|
+
if (!metaRes.ok) throw new Error(`Theme "${pkg}" not found on npm (${metaRes.status})`);
|
|
63
|
+
const meta = await metaRes.json();
|
|
64
|
+
const tarballUrl = meta.dist.tarball;
|
|
65
|
+
|
|
66
|
+
// Download and extract
|
|
67
|
+
const tarRes = await fetch(tarballUrl);
|
|
68
|
+
const buf = Buffer.from(await tarRes.arrayBuffer());
|
|
69
|
+
fs.mkdirSync(pkgDir, { recursive: true });
|
|
70
|
+
const tarPath = path.join(THEME_CACHE_DIR, `${pkg}.tgz`);
|
|
71
|
+
fs.writeFileSync(tarPath, buf);
|
|
72
|
+
execFileSync('tar', ['xzf', tarPath, '-C', pkgDir, '--strip-components=1'], { timeout: 10_000 });
|
|
73
|
+
fs.unlinkSync(tarPath);
|
|
74
|
+
|
|
75
|
+
// Install prod deps (ignore lifecycle scripts for security)
|
|
76
|
+
const pkgJson = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
77
|
+
if (pkgJson.dependencies && Object.keys(pkgJson.dependencies).length > 0) {
|
|
78
|
+
console.log(` Installing dependencies for ${pkg}...`);
|
|
79
|
+
execFileSync('npm', ['install', '--omit=dev', '--ignore-scripts', '--legacy-peer-deps'], {
|
|
80
|
+
timeout: 30_000,
|
|
81
|
+
stdio: 'pipe',
|
|
82
|
+
cwd: pkgDir,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check if the main entry point exists; if not, the theme needs building
|
|
87
|
+
const mainEntry = pkgJson.main || 'index.js';
|
|
88
|
+
const mainPath = path.join(pkgDir, mainEntry);
|
|
89
|
+
if (!fs.existsSync(mainPath)) {
|
|
90
|
+
const buildScript = pkgJson.scripts?.build || pkgJson.scripts?.prepare;
|
|
91
|
+
if (buildScript) {
|
|
92
|
+
console.log(` 🔨 Theme needs building, installing all deps + running build...`);
|
|
93
|
+
// Install all deps (including devDependencies needed for the build)
|
|
94
|
+
execFileSync('npm', ['install', '--ignore-scripts'], {
|
|
95
|
+
timeout: 60_000,
|
|
96
|
+
stdio: 'pipe',
|
|
97
|
+
cwd: pkgDir,
|
|
98
|
+
});
|
|
99
|
+
execFileSync('npm', ['run', 'build'], {
|
|
100
|
+
timeout: 60_000,
|
|
101
|
+
stdio: 'pipe',
|
|
102
|
+
cwd: pkgDir,
|
|
103
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(` ✅ ${pkg} ready`);
|
|
109
|
+
return pkgDir;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Returns a Proxy-wrapped array that:
|
|
114
|
+
* - Supports .map/.forEach/.filter on empty arrays (no crash)
|
|
115
|
+
* - Returns a safe empty-string object for out-of-bounds index access (prevents arr[0].prop crash)
|
|
116
|
+
*/
|
|
117
|
+
function createSafeArray(arr) {
|
|
118
|
+
return new Proxy(arr, {
|
|
119
|
+
get(target, prop, receiver) {
|
|
120
|
+
const val = Reflect.get(target, prop, receiver);
|
|
121
|
+
if (val !== undefined) return val;
|
|
122
|
+
if (typeof prop === 'string' && /^\d+$/.test(prop)) {
|
|
123
|
+
return new Proxy({}, { get: (_, p) => typeof p === 'symbol' ? undefined : '' });
|
|
124
|
+
}
|
|
125
|
+
return val;
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Pad resume with safe defaults so themes don't crash on missing fields.
|
|
132
|
+
* All array sections are wrapped in createSafeArray so that:
|
|
133
|
+
* - theme.work.map(fn) works on empty data
|
|
134
|
+
* - theme.work[0].position returns '' instead of crashing
|
|
135
|
+
*/
|
|
136
|
+
function padResume(r) {
|
|
137
|
+
const basics = r.basics || {};
|
|
138
|
+
const location = basics.location || {};
|
|
139
|
+
const safe = { ...r };
|
|
140
|
+
safe.basics = {
|
|
141
|
+
name: '', label: '', image: '', email: '', phone: '', url: '', summary: '',
|
|
142
|
+
...basics,
|
|
143
|
+
location: { address: '', postalCode: '', city: '', countryCode: '', region: '', ...location },
|
|
144
|
+
};
|
|
145
|
+
const arraySections = ['work','volunteer','education','awards','certificates','publications','skills','languages','interests','references','projects'];
|
|
146
|
+
for (const key of arraySections) {
|
|
147
|
+
safe[key] = createSafeArray(Array.isArray(safe[key]) ? safe[key] : []);
|
|
148
|
+
}
|
|
149
|
+
safe.basics.profiles = createSafeArray(Array.isArray(safe.basics.profiles) ? safe.basics.profiles : []);
|
|
150
|
+
return safe;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function renderWithTheme(themeName, resume) {
|
|
154
|
+
const paddedResume = padResume(resume);
|
|
155
|
+
const pkgDir = await ensureThemeInstalled(themeName);
|
|
156
|
+
|
|
157
|
+
// Clear require cache for this dir so hot-reload works
|
|
158
|
+
for (const key of Object.keys(require.cache)) {
|
|
159
|
+
if (key.startsWith(pkgDir)) delete require.cache[key];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let mod;
|
|
163
|
+
try {
|
|
164
|
+
mod = require(pkgDir);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
const pkg = toPackageName(themeName);
|
|
167
|
+
const hint = err.code === 'MODULE_NOT_FOUND'
|
|
168
|
+
? `. This theme may require a build step that isn't included in the published package.`
|
|
169
|
+
: '';
|
|
170
|
+
throw new Error(`Could not load theme "${pkg}"${hint}`);
|
|
171
|
+
}
|
|
172
|
+
if (typeof mod.render !== 'function') {
|
|
173
|
+
throw new Error(`Theme "${toPackageName(themeName)}" does not export a render function`);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
// Temporarily change cwd to theme dir so themes that read files
|
|
177
|
+
// with relative paths (e.g. fs.readFileSync('./index.css')) work
|
|
178
|
+
const originalCwd = process.cwd();
|
|
179
|
+
process.chdir(pkgDir);
|
|
180
|
+
try {
|
|
181
|
+
const result = mod.render(paddedResume);
|
|
182
|
+
return result instanceof Promise ? await result : result;
|
|
183
|
+
} finally {
|
|
184
|
+
process.chdir(originalCwd);
|
|
185
|
+
}
|
|
186
|
+
} catch (err) {
|
|
187
|
+
throw new Error(
|
|
188
|
+
`Theme "${toPackageName(themeName)}" crashed while rendering: ${err.message}. ` +
|
|
189
|
+
`This usually means the theme expects resume fields that are missing (e.g. work, education).`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Theme registry (mirrors api/_lib/themeRegistry.ts) ──────────────
|
|
195
|
+
let cachedThemes = null;
|
|
196
|
+
let cacheExpires = 0;
|
|
197
|
+
|
|
198
|
+
async function fetchThemeList() {
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (cachedThemes && cacheExpires > now) return cachedThemes;
|
|
201
|
+
|
|
202
|
+
const res = await fetch('https://registry.npmjs.org/-/v1/search?text=jsonresume-theme-&size=250');
|
|
203
|
+
if (!res.ok) throw new Error(`npm registry returned ${res.status}`);
|
|
204
|
+
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
const themes = [];
|
|
207
|
+
for (const result of data.objects || []) {
|
|
208
|
+
const pkg = result.package;
|
|
209
|
+
const name = pkg.name;
|
|
210
|
+
if (!name.startsWith('jsonresume-theme-')) continue;
|
|
211
|
+
const shortName = name.replace('jsonresume-theme-', '');
|
|
212
|
+
themes.push({
|
|
213
|
+
name: shortName,
|
|
214
|
+
displayName: shortName.charAt(0).toUpperCase() + shortName.slice(1).replace(/-/g, ' '),
|
|
215
|
+
description: pkg.description || '',
|
|
216
|
+
version: pkg.version || '0.0.0',
|
|
217
|
+
weeklyDownloads: result.score?.detail?.popularity
|
|
218
|
+
? Math.round(result.score.detail.popularity * 10000)
|
|
219
|
+
: 0,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
themes.sort((a, b) => b.weeklyDownloads - a.weeklyDownloads);
|
|
223
|
+
cachedThemes = themes;
|
|
224
|
+
cacheExpires = now + 3600_000;
|
|
225
|
+
return themes;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Parse JSON body ─────────────────────────────────────────────────
|
|
229
|
+
function readBody(req, maxBytes = 512 * 1024) {
|
|
230
|
+
return new Promise((resolve, reject) => {
|
|
231
|
+
const chunks = [];
|
|
232
|
+
let size = 0;
|
|
233
|
+
req.on('data', (chunk) => {
|
|
234
|
+
size += chunk.length;
|
|
235
|
+
if (size > maxBytes) { req.destroy(); reject(new Error('Body too large')); return; }
|
|
236
|
+
chunks.push(chunk);
|
|
237
|
+
});
|
|
238
|
+
req.on('end', () => {
|
|
239
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString())); }
|
|
240
|
+
catch { reject(new Error('Invalid JSON')); }
|
|
241
|
+
});
|
|
242
|
+
req.on('error', reject);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ── API route handlers ──────────────────────────────────────────────
|
|
247
|
+
async function handleApiRender(req, res) {
|
|
248
|
+
if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; }
|
|
249
|
+
if (req.method !== 'POST') { jsonError(res, 405, 'Method not allowed'); return; }
|
|
250
|
+
|
|
251
|
+
let body;
|
|
252
|
+
try { body = await readBody(req); } catch (e) { jsonError(res, 400, e.message); return; }
|
|
253
|
+
|
|
254
|
+
const { resume, theme } = body;
|
|
255
|
+
if (!resume || typeof resume !== 'object') { jsonError(res, 400, 'resume is required'); return; }
|
|
256
|
+
if (!theme || typeof theme !== 'string' || !SAFE_NAME.test(theme)) { jsonError(res, 400, 'Invalid theme name'); return; }
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
console.log(`🎨 Rendering with theme: ${theme}`);
|
|
260
|
+
const html = await renderWithTheme(theme, resume);
|
|
261
|
+
res.writeHead(200, {
|
|
262
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
263
|
+
'Access-Control-Allow-Origin': '*',
|
|
264
|
+
});
|
|
265
|
+
res.end(html);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
console.error('Render error:', e.message);
|
|
268
|
+
jsonError(res, 500, e.message);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function handleApiThemes(_req, res) {
|
|
273
|
+
try {
|
|
274
|
+
const themes = await fetchThemeList();
|
|
275
|
+
res.writeHead(200, {
|
|
276
|
+
'Content-Type': 'application/json',
|
|
277
|
+
'Access-Control-Allow-Origin': '*',
|
|
278
|
+
'Cache-Control': 'public, max-age=3600',
|
|
279
|
+
});
|
|
280
|
+
res.end(JSON.stringify(themes));
|
|
281
|
+
} catch (e) {
|
|
282
|
+
jsonError(res, 500, e.message);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function jsonError(res, status, message) {
|
|
287
|
+
res.writeHead(status, {
|
|
288
|
+
'Content-Type': 'application/json',
|
|
289
|
+
'Access-Control-Allow-Origin': '*',
|
|
290
|
+
});
|
|
291
|
+
res.end(JSON.stringify({ error: message }));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ── Static file serving ─────────────────────────────────────────────
|
|
295
|
+
function serveStatic(req, res) {
|
|
296
|
+
let filePath = path.join(DOCS_DIR, req.url === '/' ? 'index.html' : req.url);
|
|
297
|
+
|
|
298
|
+
// SPA fallback: if file doesn't exist and no extension, serve index.html
|
|
299
|
+
if (!fs.existsSync(filePath) && !path.extname(filePath)) {
|
|
300
|
+
filePath = path.join(DOCS_DIR, 'index.html');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!fs.existsSync(filePath)) {
|
|
304
|
+
res.writeHead(404);
|
|
305
|
+
res.end('Not found');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const ext = path.extname(filePath);
|
|
310
|
+
const mime = MIME[ext] || 'application/octet-stream';
|
|
311
|
+
const content = fs.readFileSync(filePath);
|
|
312
|
+
|
|
313
|
+
res.writeHead(200, { 'Content-Type': mime });
|
|
314
|
+
res.end(content);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ── esbuild watch ───────────────────────────────────────────────────
|
|
318
|
+
async function startBuildWatch() {
|
|
319
|
+
const ctx = await context({
|
|
320
|
+
entryPoints: [path.resolve(ROOT, 'src/builder/index.tsx')],
|
|
321
|
+
bundle: true,
|
|
322
|
+
sourcemap: true,
|
|
323
|
+
format: 'esm',
|
|
324
|
+
target: 'es2022',
|
|
325
|
+
outfile: path.resolve(DOCS_DIR, 'app/main.js'),
|
|
326
|
+
jsx: 'automatic',
|
|
327
|
+
define: { 'process.env.NODE_ENV': '"development"' },
|
|
328
|
+
loader: { '.ts': 'ts', '.tsx': 'tsx' },
|
|
329
|
+
logLevel: 'info',
|
|
330
|
+
// Don't minify in dev for better debugging
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
await ctx.watch();
|
|
334
|
+
console.log('👀 Watching for frontend changes...');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// ── HTTP server ─────────────────────────────────────────────────────
|
|
338
|
+
async function main() {
|
|
339
|
+
// Build frontend first
|
|
340
|
+
await startBuildWatch();
|
|
341
|
+
|
|
342
|
+
const server = http.createServer(async (req, res) => {
|
|
343
|
+
// CORS preflight
|
|
344
|
+
if (req.method === 'OPTIONS') {
|
|
345
|
+
res.writeHead(200, {
|
|
346
|
+
'Access-Control-Allow-Origin': '*',
|
|
347
|
+
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
|
|
348
|
+
'Access-Control-Allow-Headers': 'Content-Type',
|
|
349
|
+
});
|
|
350
|
+
res.end();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const url = new URL(req.url, `http://localhost:${PORT}`);
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
if (url.pathname === '/api/render') {
|
|
358
|
+
await handleApiRender(req, res);
|
|
359
|
+
} else if (url.pathname === '/api/themes') {
|
|
360
|
+
await handleApiThemes(req, res);
|
|
361
|
+
} else {
|
|
362
|
+
serveStatic(req, res);
|
|
363
|
+
}
|
|
364
|
+
} catch (e) {
|
|
365
|
+
console.error('Server error:', e);
|
|
366
|
+
if (!res.headersSent) {
|
|
367
|
+
jsonError(res, 500, 'Internal server error');
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
server.listen(PORT, () => {
|
|
373
|
+
console.log('');
|
|
374
|
+
console.log(` 🚀 resuml builder running at:`);
|
|
375
|
+
console.log(` http://localhost:${PORT}`);
|
|
376
|
+
console.log('');
|
|
377
|
+
console.log(' API endpoints:');
|
|
378
|
+
console.log(` POST http://localhost:${PORT}/api/render`);
|
|
379
|
+
console.log(` GET http://localhost:${PORT}/api/themes`);
|
|
380
|
+
console.log('');
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
main().catch((err) => {
|
|
385
|
+
console.error('Failed to start dev server:', err);
|
|
386
|
+
process.exit(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// Prevent theme crashes from killing the server
|
|
390
|
+
process.on('uncaughtException', (err) => {
|
|
391
|
+
console.error('⚠️ Uncaught exception (server still running):', err.message);
|
|
392
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quick bundler: takes a theme already in node_modules and bundles it
|
|
3
|
+
* for the browser as an ESM module.
|
|
4
|
+
*
|
|
5
|
+
* Shims Node.js builtins (fs, path, url, crypto) so themes that
|
|
6
|
+
* reference them at import time still bundle — the shims are no-ops
|
|
7
|
+
* or minimal polyfills that cover what Handlebars-based themes need.
|
|
8
|
+
*/
|
|
9
|
+
const esbuild = require('esbuild');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
|
|
13
|
+
const THEMES_DIR = path.resolve(__dirname, '../docs/themes');
|
|
14
|
+
const SHIMS_DIR = path.resolve(__dirname, '_shims');
|
|
15
|
+
fs.mkdirSync(THEMES_DIR, { recursive: true });
|
|
16
|
+
fs.mkdirSync(SHIMS_DIR, { recursive: true });
|
|
17
|
+
|
|
18
|
+
// Write browser shims for Node built-ins
|
|
19
|
+
fs.writeFileSync(path.join(SHIMS_DIR, 'fs.js'), `
|
|
20
|
+
export function readFileSync() { return ''; }
|
|
21
|
+
export function writeFileSync() {}
|
|
22
|
+
export function existsSync() { return false; }
|
|
23
|
+
export function mkdirSync() {}
|
|
24
|
+
export default { readFileSync, writeFileSync, existsSync, mkdirSync };
|
|
25
|
+
`);
|
|
26
|
+
fs.writeFileSync(path.join(SHIMS_DIR, 'path.js'), `
|
|
27
|
+
export function join(...p) { return p.join('/'); }
|
|
28
|
+
export function resolve(...p) { return p.join('/'); }
|
|
29
|
+
export function dirname(p) { return p.replace(/\\/[^/]*$/, ''); }
|
|
30
|
+
export function basename(p) { return p.replace(/.*\\//, ''); }
|
|
31
|
+
export function extname(p) { const m = p.match(/\\.[^.]+$/); return m ? m[0] : ''; }
|
|
32
|
+
export const sep = '/';
|
|
33
|
+
export default { join, resolve, dirname, basename, extname, sep };
|
|
34
|
+
`);
|
|
35
|
+
fs.writeFileSync(path.join(SHIMS_DIR, 'url.js'), `
|
|
36
|
+
export function fileURLToPath(u) { return u.replace('file://', ''); }
|
|
37
|
+
export function pathToFileURL(p) { return 'file://' + p; }
|
|
38
|
+
export class URL { constructor(u) { this.href = u; } }
|
|
39
|
+
export default { fileURLToPath, pathToFileURL, URL };
|
|
40
|
+
`);
|
|
41
|
+
fs.writeFileSync(path.join(SHIMS_DIR, 'crypto.js'), `
|
|
42
|
+
export function randomUUID() {
|
|
43
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
44
|
+
var r = Math.random() * 16 | 0;
|
|
45
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export function createHash() {
|
|
49
|
+
return { update() { return this; }, digest() { return 'stub'; } };
|
|
50
|
+
}
|
|
51
|
+
export default { randomUUID, createHash };
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
// Themes to bundle (must already be in node_modules)
|
|
55
|
+
const themes = [
|
|
56
|
+
'stackoverflow',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
async function bundleTheme(name) {
|
|
60
|
+
const pkg = `jsonresume-theme-${name}`;
|
|
61
|
+
const entryCode = `
|
|
62
|
+
var theme = require("${pkg}");
|
|
63
|
+
export var render = theme.render;
|
|
64
|
+
export var pdfRenderOptions = theme.pdfRenderOptions;
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const entryFile = path.join(THEMES_DIR, `_entry_${name}.js`);
|
|
68
|
+
fs.writeFileSync(entryFile, entryCode);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
await esbuild.build({
|
|
72
|
+
entryPoints: [entryFile],
|
|
73
|
+
bundle: true,
|
|
74
|
+
minify: true,
|
|
75
|
+
format: 'esm',
|
|
76
|
+
target: 'es2022',
|
|
77
|
+
platform: 'browser',
|
|
78
|
+
outfile: path.join(THEMES_DIR, `${name}.js`),
|
|
79
|
+
define: {
|
|
80
|
+
'process.env.NODE_ENV': '"production"',
|
|
81
|
+
'global': 'globalThis',
|
|
82
|
+
},
|
|
83
|
+
alias: {
|
|
84
|
+
'fs': path.join(SHIMS_DIR, 'fs.js'),
|
|
85
|
+
'path': path.join(SHIMS_DIR, 'path.js'),
|
|
86
|
+
'url': path.join(SHIMS_DIR, 'url.js'),
|
|
87
|
+
'node:crypto': path.join(SHIMS_DIR, 'crypto.js'),
|
|
88
|
+
'crypto': path.join(SHIMS_DIR, 'crypto.js'),
|
|
89
|
+
'node:fs': path.join(SHIMS_DIR, 'fs.js'),
|
|
90
|
+
'node:path': path.join(SHIMS_DIR, 'path.js'),
|
|
91
|
+
'node:url': path.join(SHIMS_DIR, 'url.js'),
|
|
92
|
+
},
|
|
93
|
+
logLevel: 'warning',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
fs.unlinkSync(entryFile);
|
|
97
|
+
const size = fs.statSync(path.join(THEMES_DIR, `${name}.js`)).size;
|
|
98
|
+
console.log(`OK ${name}: ${(size / 1024).toFixed(0)}KB`);
|
|
99
|
+
return { name, ok: true, size };
|
|
100
|
+
} catch (err) {
|
|
101
|
+
fs.unlinkSync(entryFile);
|
|
102
|
+
console.error(`FAIL ${name}: ${err.message}`);
|
|
103
|
+
return { name, ok: false };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function main() {
|
|
108
|
+
const results = [];
|
|
109
|
+
for (const t of themes) {
|
|
110
|
+
results.push(await bundleTheme(t));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Write manifest
|
|
114
|
+
const manifest = results.map(r => ({
|
|
115
|
+
name: r.name,
|
|
116
|
+
displayName: r.name.charAt(0).toUpperCase() + r.name.slice(1).replace(/-/g, ' '),
|
|
117
|
+
description: '',
|
|
118
|
+
browserCompatible: r.ok,
|
|
119
|
+
fileSize: r.size || 0,
|
|
120
|
+
}));
|
|
121
|
+
fs.writeFileSync(path.join(THEMES_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
|
|
122
|
+
|
|
123
|
+
// Cleanup shims
|
|
124
|
+
fs.rmSync(SHIMS_DIR, { recursive: true, force: true });
|
|
125
|
+
|
|
126
|
+
console.log(`\nManifest written with ${results.filter(r => r.ok).length}/${results.length} themes`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main();
|
package/dist/api.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export { R as Resume, l as loadResumeFiles, p as processResumeData } from './loadResume-BFCirLAW.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Node.js API: Only load theme from node_modules, do not auto-install
|
|
5
|
-
*/
|
|
6
|
-
declare function loadTheme(themeName: string): Promise<any>;
|
|
7
|
-
|
|
8
|
-
export { loadTheme };
|