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,392 +0,0 @@
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
- });
@@ -1,156 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Enrich docs/themes/manifest.json with popularity + render-health signals.
4
- *
5
- * For each theme we add:
6
- * - npmWeeklyDownloads: rough "actual usage" proxy (npm API)
7
- * - renderOk: whether the bundled theme renders a sample resume without
8
- * throwing (Playwright + blob worker). Themes that fail here are the
9
- * ones that silently showed sample data before, surface them as broken.
10
- *
11
- * Usage:
12
- * node scripts/enrich-themes-manifest.mjs # both
13
- * node scripts/enrich-themes-manifest.mjs --downloads-only
14
- * node scripts/enrich-themes-manifest.mjs --render-only # needs dev server at :3010
15
- *
16
- * The render probe is slow (tens of seconds), keep the output committed.
17
- */
18
-
19
- import { readFileSync, writeFileSync } from 'node:fs';
20
- import { get } from 'node:https';
21
-
22
- const MANIFEST = 'docs/themes/manifest.json';
23
- const DOWNLOADS_BATCH = 'https://api.npmjs.org/downloads/point/last-week/';
24
- const args = new Set(process.argv.slice(2));
25
-
26
- function fetchJson(url) {
27
- return new Promise((resolve, reject) => {
28
- get(url, { headers: { 'User-Agent': 'resuml-enrich' } }, (res) => {
29
- let data = '';
30
- res.on('data', (c) => { data += c; });
31
- res.on('end', () => {
32
- if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
33
- try { resolve(JSON.parse(data)); } catch (e) { reject(e); }
34
- });
35
- }).on('error', reject);
36
- });
37
- }
38
-
39
- async function enrichDownloads(manifest) {
40
- console.log(`📦 Fetching npm download counts for ${manifest.length} themes…`);
41
- // npm batch endpoint accepts up to 128 comma-separated packages
42
- const CHUNK = 100;
43
- for (let i = 0; i < manifest.length; i += CHUNK) {
44
- const slice = manifest.slice(i, i + CHUNK);
45
- const names = slice.map((t) => `jsonresume-theme-${t.name}`).join(',');
46
- try {
47
- const resp = await fetchJson(DOWNLOADS_BATCH + encodeURIComponent(names));
48
- // Batch response shape: { pkg: { downloads, ... }, ... } or { downloads, ... } for single
49
- for (const t of slice) {
50
- const key = `jsonresume-theme-${t.name}`;
51
- const entry = resp[key] ?? (slice.length === 1 ? resp : null);
52
- t.npmWeeklyDownloads = entry?.downloads ?? 0;
53
- }
54
- } catch (e) {
55
- console.warn(` batch ${i}-${i + slice.length} failed: ${e.message}`);
56
- // Fall back per-package so one bad name doesn't kill the batch
57
- for (const t of slice) {
58
- try {
59
- const r = await fetchJson(DOWNLOADS_BATCH + encodeURIComponent(`jsonresume-theme-${t.name}`));
60
- t.npmWeeklyDownloads = r.downloads ?? 0;
61
- } catch { t.npmWeeklyDownloads = 0; }
62
- }
63
- }
64
- process.stdout.write(` ${Math.min(i + CHUNK, manifest.length)}/${manifest.length}\r`);
65
- }
66
- console.log('');
67
- }
68
-
69
- async function enrichRender(manifest) {
70
- const { chromium } = await import('playwright');
71
- console.log(`🎭 Probing render for ${manifest.length} themes via Playwright (dev server must be on :3010)…`);
72
-
73
- const b = await chromium.launch();
74
- const page = await b.newPage();
75
- await page.goto('http://localhost:3010', { waitUntil: 'networkidle' });
76
-
77
- // Installed once on the page; reused per theme
78
- const sampleResume = {
79
- basics: {
80
- name: 'Test User',
81
- label: 'Engineer',
82
- email: 'test@example.com',
83
- phone: '+1-555-0123',
84
- summary: 'Sample summary for render probe.',
85
- location: { city: 'City', region: 'Region', countryCode: 'US', address: '1 Way', postalCode: '10000' },
86
- profiles: [{ network: 'GitHub', username: 'x', url: 'https://github.com/x' }],
87
- url: 'https://example.com',
88
- },
89
- work: [{ name: 'Co', position: 'Engineer', startDate: '2020-01-01', endDate: '2022-01-01', summary: 'S', highlights: ['H'] }],
90
- skills: [{ name: 'Cat', level: 'Expert', keywords: ['K'] }],
91
- education: [{ institution: 'U', area: 'A', studyType: 'M', startDate: '2015-01-01', endDate: '2019-01-01' }],
92
- projects: [{ name: 'P', description: 'D', highlights: ['H'] }],
93
- languages: [{ language: 'English', fluency: 'Native' }],
94
- interests: [{ name: 'I', keywords: ['k'] }],
95
- references: [{ name: 'R', reference: 'Ref' }],
96
- publications: [{ name: 'Pub', publisher: 'Pr', releaseDate: '2020', summary: 'S' }],
97
- certificates: [{ name: 'C', date: '2020', issuer: 'Is' }],
98
- awards: [{ title: 'A', date: '2020', awarder: 'Aw', summary: 'S' }],
99
- volunteer: [{ organization: 'V', position: 'P', startDate: '2020', summary: 'S', highlights: ['H'] }],
100
- // Include `meta.palette` so themes like material (which set
101
- // meta.palette.primary without defensive checks) don't trip the probe
102
- //, matches what the runtime padResume provides.
103
- meta: { theme: 'test', palette: { primary: '', secondary: '' } },
104
- };
105
-
106
- for (let i = 0; i < manifest.length; i++) {
107
- const t = manifest[i];
108
- if (!t.browserCompatible) { t.renderOk = false; continue; }
109
- const result = await page.evaluate(async ({ name, resume }) => {
110
- try {
111
- if (typeof globalThis.process === 'undefined') {
112
- globalThis.process = {
113
- env: { NODE_ENV: 'production' }, browser: true, platform: 'browser',
114
- version: 'v20.0.0', versions: {}, stdout: { write: () => {} }, stderr: { write: () => {} },
115
- cwd: () => '/', chdir: () => {}, nextTick: (fn) => Promise.resolve().then(fn),
116
- argv: [], pid: 1, title: 'browser',
117
- };
118
- }
119
- if (typeof globalThis.global === 'undefined') globalThis.global = globalThis;
120
- const mod = await import(/* @vite-ignore */ '/themes/' + name + '.js');
121
- const render = mod.render ?? mod.default?.render;
122
- if (typeof render !== 'function') return false;
123
- const out = render(resume);
124
- const html = typeof out === 'string' ? out : await out;
125
- return typeof html === 'string' && html.length > 200;
126
- } catch { return false; }
127
- }, { name: t.name, resume: sampleResume });
128
- t.renderOk = !!result;
129
- if ((i + 1) % 20 === 0 || i === manifest.length - 1) {
130
- process.stdout.write(` ${i + 1}/${manifest.length}\r`);
131
- }
132
- }
133
- console.log('');
134
- await b.close();
135
-
136
- const ok = manifest.filter((t) => t.renderOk).length;
137
- const broken = manifest.filter((t) => t.browserCompatible && !t.renderOk);
138
- console.log(` ${ok}/${manifest.length} render successfully. ${broken.length} marked browserCompatible but fail at render time.`);
139
- if (broken.length) {
140
- console.log(' Broken at render time:', broken.map((t) => t.name).slice(0, 20).join(', ') + (broken.length > 20 ? '…' : ''));
141
- }
142
- }
143
-
144
- async function main() {
145
- const manifest = JSON.parse(readFileSync(MANIFEST, 'utf8'));
146
- const doDownloads = !args.has('--render-only');
147
- const doRender = !args.has('--downloads-only');
148
-
149
- if (doDownloads) await enrichDownloads(manifest);
150
- if (doRender) await enrichRender(manifest);
151
-
152
- writeFileSync(MANIFEST, JSON.stringify(manifest, null, 2));
153
- console.log(`✅ Wrote ${MANIFEST}`);
154
- }
155
-
156
- main().catch((e) => { console.error(e); process.exit(1); });
@@ -1,55 +0,0 @@
1
- const { execSync } = require('child_process');
2
- const fs = require('fs');
3
- const path = require('path');
4
-
5
- const SCHEMA_URL = 'https://raw.githubusercontent.com/jsonresume/resume-schema/master/schema.json';
6
- const SCHEMA_FILE = 'resume.schema.json';
7
- const OUTPUT_DIR = path.join('src', 'types');
8
- const OUTPUT_FILE = path.join(OUTPUT_DIR, 'resume.ts');
9
-
10
- function runCommand(command, description) {
11
- console.log(`⏳ ${description}...`);
12
- try {
13
- execSync(command, { stdio: 'inherit' });
14
- console.log(`✅ ${description} successful!`);
15
- } catch (error) {
16
- console.error(`❌ Error during ${description}:`, error.message);
17
- if (error.stderr) {
18
- console.error(error.stderr.toString());
19
- }
20
- if (error.stdout) {
21
- console.error(error.stdout.toString());
22
- }
23
- process.exit(1);
24
- }
25
- }
26
-
27
- console.log('🚀 Starting type generation process...');
28
-
29
- // 1. Fetch the schema
30
- runCommand(`curl -s -L -o ${SCHEMA_FILE} ${SCHEMA_URL}`, 'Fetching latest JSON Resume schema');
31
-
32
- // 2. Create output directory if it doesn't exist
33
- if (!fs.existsSync(OUTPUT_DIR)) {
34
- console.log(`⏳ Creating directory ${OUTPUT_DIR}...`);
35
- fs.mkdirSync(OUTPUT_DIR, { recursive: true });
36
- console.log(`✅ Directory ${OUTPUT_DIR} created.`);
37
- }
38
-
39
- // 3. Generate TypeScript types
40
- runCommand(
41
- `npx json-schema-to-typescript ${SCHEMA_FILE} -o ${OUTPUT_FILE}`,
42
- 'Generating TypeScript types'
43
- );
44
-
45
- // 4. Remove the downloaded schema file
46
- console.log(`⏳ Removing temporary schema file ${SCHEMA_FILE}...`);
47
- try {
48
- fs.unlinkSync(SCHEMA_FILE);
49
- console.log(`✅ Temporary schema file ${SCHEMA_FILE} removed.`);
50
- } catch (error) {
51
- console.error(`❌ Error removing temporary schema file ${SCHEMA_FILE}:`, error.message);
52
- // Non-fatal, so don't exit
53
- }
54
-
55
- console.log('🎉 TypeScript types generated successfully from JSON Resume schema!');