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.
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { R as ResumeSchema } from './themeLoader-C7CBqNiC.cjs';
4
+ export { l as loadResumeFiles, a as loadTheme, p as processResumeData } from './themeLoader-C7CBqNiC.cjs';
5
+
6
+ interface ThemeConfig {
7
+ sections?: {
8
+ order?: string[];
9
+ exclude?: string[];
10
+ };
11
+ layout?: {
12
+ style?: string;
13
+ };
14
+ styling?: Record<string, string | number>;
15
+ labels?: Record<string, string>;
16
+ }
17
+ /**
18
+ * Render a resume using the specified theme
19
+ * @param themeName Name of the theme to use
20
+ * @param resumeData Resume data to render
21
+ * @param themeConfig Theme configuration
22
+ * @param inlineCss Optional CSS to include in the HTML
23
+ * @param language Language code for localization
24
+ * @returns Object containing the rendered HTML
25
+ */
26
+ declare function renderTheme(themeName: string, resumeData: ResumeSchema, themeConfig?: ThemeConfig, inlineCss?: string, language?: string): Promise<{
27
+ htmlOutput: string;
28
+ }>;
29
+ /**
30
+ * Injects CSS into HTML content
31
+ * @param html HTML content
32
+ * @param css CSS to inject
33
+ * @returns HTML with injected CSS
34
+ */
35
+ declare function injectCss(html: string, css?: string): string;
36
+
37
+ declare const themeRender_injectCss: typeof injectCss;
38
+ declare const themeRender_renderTheme: typeof renderTheme;
39
+ declare namespace themeRender {
40
+ export { themeRender_injectCss as injectCss, themeRender_renderTheme as renderTheme };
41
+ }
42
+
43
+ type AtsCheckCategory = 'contact' | 'content' | 'structure' | 'keywords';
44
+ type AtsCheckWeight = 'high' | 'medium' | 'low';
45
+ type AtsRating = 'excellent' | 'good' | 'needs-work' | 'poor';
46
+ interface AtsCheck {
47
+ id: string;
48
+ category: AtsCheckCategory;
49
+ weight: AtsCheckWeight;
50
+ passed: boolean;
51
+ score: number;
52
+ message: string;
53
+ suggestion?: string;
54
+ }
55
+ interface AtsKeywordMatch {
56
+ matched: string[];
57
+ missing: string[];
58
+ matchPercentage: number;
59
+ }
60
+ interface AtsResult {
61
+ score: number;
62
+ rating: AtsRating;
63
+ checks: AtsCheck[];
64
+ keywords?: AtsKeywordMatch;
65
+ summary: string;
66
+ }
67
+ interface AtsOptions {
68
+ language?: string;
69
+ jobDescription?: string;
70
+ threshold?: number;
71
+ }
72
+
73
+ /**
74
+ * Run ATS analysis on a resume.
75
+ *
76
+ * Performs deterministic, offline checks:
77
+ * 1. Generic best-practice checks (contact, content, structure)
78
+ * 2. Optional job-description keyword matching
79
+ *
80
+ * @param resume - Validated resume data
81
+ * @param options - ATS analysis options
82
+ * @returns Full ATS analysis result with score, checks, and suggestions
83
+ */
84
+ declare function analyzeAts(resume: ResumeSchema, options?: AtsOptions): AtsResult;
85
+
86
+ declare const program: Command;
87
+
88
+ export { type AtsOptions, type AtsResult, analyzeAts, program, themeRender };
@@ -356,4 +356,17 @@ declare function loadResumeFiles(inputPath?: string): Promise<{
356
356
  yamlContents: string[];
357
357
  }>;
358
358
 
359
- export { type ResumeSchema as R, loadResumeFiles as l, processResumeData as p };
359
+ interface ThemeModule {
360
+ render: (resume: Record<string, unknown>, options?: Record<string, unknown>) => string | Promise<string>;
361
+ }
362
+ /**
363
+ * Load a theme module by name
364
+ * @param themeName The name of the theme to load
365
+ * @param options Optional settings (autoInstall: boolean)
366
+ * @returns The loaded theme module
367
+ */
368
+ declare function loadTheme(themeName: string, options?: {
369
+ autoInstall?: boolean;
370
+ }): ThemeModule;
371
+
372
+ export { type ResumeSchema as R, type ThemeModule as T, loadTheme as a, loadResumeFiles as l, processResumeData as p };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "resuml",
3
- "version": "1.3.1",
4
- "description": "Resume as Code — Generate beautiful resumes from YAML with JSON Resume theme support, validation, PDF export, and a live dev server.",
3
+ "version": "1.4.2",
4
+ "description": "Generate JSON resumes from YAML with theme support",
5
+ "type": "module",
5
6
  "main": "./dist/api.js",
6
7
  "exports": {
7
8
  ".": {
@@ -11,7 +12,7 @@
11
12
  "./cli": "./dist/index.js"
12
13
  },
13
14
  "bin": {
14
- "resuml": "./bin/resuml.cjs"
15
+ "resuml": "./bin/resuml"
15
16
  },
16
17
  "files": [
17
18
  "bin",
@@ -21,80 +22,85 @@
21
22
  "src/types"
22
23
  ],
23
24
  "scripts": {
24
- "build": "tsup",
25
+ "build": "tsup && npm run build:builder",
26
+ "build:builder": "node scripts/build-builder.js",
27
+ "dev:builder": "node scripts/dev-server.js",
25
28
  "prepublishOnly": "npm run generate:types && npm run build",
26
- "generate:types": "node scripts/generate-types.js",
29
+ "generate:types": "node scripts/generate-types.cjs",
27
30
  "test": "vitest run",
28
31
  "test:watch": "vitest",
29
- "lint": "eslint src --ext .ts",
32
+ "lint": "eslint src api --ext .ts,.tsx",
33
+ "lint:fix": "eslint src api --ext .ts,.tsx --fix",
34
+ "typecheck": "tsc --noEmit",
30
35
  "format": "prettier --write .",
36
+ "format:check": "prettier --check .",
31
37
  "release": "semantic-release",
32
38
  "prepare": "husky"
33
39
  },
34
40
  "dependencies": {
35
- "@jsonresume/schema": "^1.2.1",
36
- "ajv": "^8.18.0",
37
- "chalk": "^5.6.2",
41
+ "@jsonresume/schema": "^1.0.0",
42
+ "ajv": "^8.12.0",
43
+ "chalk": "^5.3.0",
38
44
  "commander": "^11.1.0",
39
- "js-yaml": "^4.1.1",
45
+ "js-yaml": "^4.1.0",
46
+ "jsonresume-theme-stackoverflow": "^2.1.0",
40
47
  "lodash.merge": "^4.6.2",
41
- "yaml": "^2.8.2"
48
+ "lucide-react": "^1.7.0",
49
+ "react": "^19.1.0",
50
+ "react-dom": "^19.1.0",
51
+ "tar": "^7.5.13",
52
+ "yaml": "^2.3.4",
53
+ "zod": "^4.3.6"
42
54
  },
43
55
  "devDependencies": {
56
+ "@codemirror/commands": "^6.10.3",
57
+ "@codemirror/lang-yaml": "^6.1.3",
58
+ "@codemirror/language": "^6.12.3",
59
+ "@codemirror/search": "^6.6.0",
60
+ "@codemirror/state": "^6.6.0",
61
+ "@codemirror/view": "^6.41.0",
44
62
  "@commitlint/cli": "^19.8.1",
45
63
  "@commitlint/config-conventional": "^19.8.1",
46
64
  "@semantic-release/changelog": "^6.0.3",
47
65
  "@semantic-release/commit-analyzer": "^13.0.1",
48
66
  "@semantic-release/git": "^10.0.1",
49
- "@semantic-release/github": "^11.0.6",
50
- "@semantic-release/npm": "^13.1.4",
51
- "@semantic-release/release-notes-generator": "^14.1.0",
67
+ "@semantic-release/github": "^11.0.3",
68
+ "@semantic-release/npm": "^13.1.5",
69
+ "@semantic-release/release-notes-generator": "^14.0.3",
52
70
  "@types/js-yaml": "^4.0.9",
53
71
  "@types/lodash.merge": "^4.6.9",
54
- "@types/node": "^20.19.33",
55
- "@typescript-eslint/eslint-plugin": "^8.55.0",
56
- "@typescript-eslint/parser": "^8.55.0",
57
- "eslint": "^8.57.1",
58
- "eslint-config-prettier": "^9.1.2",
72
+ "@types/node": "^20.11.19",
73
+ "@types/pako": "^2.0.4",
74
+ "@types/react": "^19.2.14",
75
+ "@types/react-dom": "^19.2.3",
76
+ "@typescript-eslint/eslint-plugin": "^8.33.0",
77
+ "@typescript-eslint/parser": "^8.33.0",
78
+ "@vercel/node": "^5.7.2",
79
+ "codemirror": "^6.0.2",
80
+ "esbuild": "^0.28.0",
81
+ "eslint": "^8.0.0",
82
+ "eslint-config-prettier": "^9.0.0",
59
83
  "husky": "^9.1.7",
60
84
  "json-schema-to-typescript": "^15.0.4",
61
- "prettier": "^3.8.1",
85
+ "pako": "^2.1.0",
86
+ "playwright": "^1.52.0",
87
+ "prettier": "^3.0.0",
62
88
  "semantic-release": "^25.0.3",
63
- "tsup": "^8.5.1",
64
- "typescript": "^5.9.3",
65
- "vitest": "^3.2.4"
66
- },
67
- "peerDependencies": {
68
- "puppeteer": ">=20.0.0"
69
- },
70
- "peerDependenciesMeta": {
71
- "puppeteer": {
72
- "optional": true
73
- }
89
+ "tsup": "^8.0.2",
90
+ "typescript": "^5.0.0",
91
+ "vitest": "^3.2.2"
74
92
  },
75
93
  "engines": {
76
94
  "node": ">=20.0.0"
77
95
  },
78
- "homepage": "https://phoinixi.github.io/resuml/",
96
+ "license": "ISC",
97
+ "author": "phoinixi",
79
98
  "repository": {
80
99
  "type": "git",
81
- "url": "https://github.com/phoinixi/resuml.git"
100
+ "url": "https://github.com/phoinixi/resuml"
82
101
  },
83
- "license": "ISC",
84
- "author": "phoinixi",
85
102
  "publishConfig": {
86
103
  "access": "public",
87
104
  "registry": "https://registry.npmjs.org/"
88
- },
89
- "keywords": [
90
- "resume",
91
- "yaml",
92
- "cli",
93
- "json-resume",
94
- "pdf",
95
- "resume-builder",
96
- "resume-generator",
97
- "cv",
98
- "career"
99
- ]
105
+ }
100
106
  }
@@ -0,0 +1,25 @@
1
+ import { build } from 'esbuild';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ await build({
8
+ entryPoints: [resolve(__dirname, '../src/builder/index.tsx')],
9
+ bundle: true,
10
+ minify: true,
11
+ treeShaking: true,
12
+ format: 'esm',
13
+ target: 'es2022',
14
+ outfile: resolve(__dirname, '../docs/app/main.js'),
15
+ jsx: 'automatic',
16
+ define: {
17
+ 'process.env.NODE_ENV': '"production"',
18
+ },
19
+ loader: {
20
+ '.ts': 'ts',
21
+ '.tsx': 'tsx',
22
+ },
23
+ });
24
+
25
+ console.log('✅ Builder built to docs/app/main.js');
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Theme Bundling Script
3
+ *
4
+ * Discovers jsonresume-theme packages from npm, installs them,
5
+ * and bundles each into a browser-compatible ES module.
6
+ *
7
+ * Output:
8
+ * docs/themes/{name}.js — bundled theme module
9
+ * docs/themes/manifest.json — theme metadata registry
10
+ *
11
+ * Usage:
12
+ * node scripts/bundle-themes.js # Bundle popular themes
13
+ * node scripts/bundle-themes.js --all # Discover + bundle all from npm
14
+ * node scripts/bundle-themes.js --themes elegant,even,kendall # Specific themes
15
+ */
16
+
17
+ import { build } from 'esbuild';
18
+ import { resolve, dirname } from 'path';
19
+ import { fileURLToPath } from 'url';
20
+ import { execSync } from 'child_process';
21
+ import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'fs';
22
+
23
+ const __dirname = dirname(fileURLToPath(import.meta.url));
24
+ const THEMES_DIR = resolve(__dirname, '../docs/themes');
25
+
26
+ // Popular themes that are known to work in the browser
27
+ const POPULAR_THEMES = [
28
+ 'stackoverflow',
29
+ 'elegant',
30
+ 'even',
31
+ 'kendall',
32
+ 'flat',
33
+ 'macchiato',
34
+ 'class',
35
+ 'paper',
36
+ 'spartan',
37
+ 'short',
38
+ 'caffeine',
39
+ 'onepage',
40
+ 'kwan',
41
+ 'autumn',
42
+ 'relaxed',
43
+ 'compact',
44
+ ];
45
+
46
+ async function discoverThemes() {
47
+ const themes = [];
48
+ let from = 0;
49
+ const size = 250;
50
+
51
+ console.log('🔍 Discovering themes from npm...');
52
+
53
+ // npm search API has pagination, fetch up to 1000
54
+ for (let page = 0; page < 4; page++) {
55
+ const url = `https://registry.npmjs.org/-/v1/search?text=jsonresume-theme&size=${size}&from=${from}`;
56
+ const res = await fetch(url);
57
+ if (!res.ok) break;
58
+ const data = await res.json();
59
+
60
+ for (const pkg of data.objects) {
61
+ const name = pkg.package.name;
62
+ if (!name.startsWith('jsonresume-theme-')) continue;
63
+ const shortName = name.replace('jsonresume-theme-', '');
64
+
65
+ // Skip themes with 0 downloads or very old
66
+ if (pkg.score?.detail?.popularity === 0) continue;
67
+
68
+ themes.push({
69
+ name: shortName,
70
+ packageName: name,
71
+ description: pkg.package.description || '',
72
+ version: pkg.package.version,
73
+ });
74
+ }
75
+
76
+ if (data.objects.length < size) break;
77
+ from += size;
78
+ }
79
+
80
+ console.log(` Found ${themes.length} themes`);
81
+ return themes;
82
+ }
83
+
84
+ async function bundleTheme(shortName, packageName) {
85
+ const entryContent = `
86
+ import theme from '${packageName}';
87
+ export const render = theme.render || theme.default?.render;
88
+ export const pdfRenderOptions = theme.pdfRenderOptions || theme.default?.pdfRenderOptions;
89
+ `;
90
+
91
+ const entryFile = resolve(THEMES_DIR, `_entry_${shortName}.js`);
92
+ writeFileSync(entryFile, entryContent);
93
+
94
+ try {
95
+ await build({
96
+ entryPoints: [entryFile],
97
+ bundle: true,
98
+ minify: true,
99
+ format: 'esm',
100
+ target: 'es2022',
101
+ platform: 'browser',
102
+ outfile: resolve(THEMES_DIR, `${shortName}.js`),
103
+ define: {
104
+ 'process.env.NODE_ENV': '"production"',
105
+ 'global': 'globalThis',
106
+ },
107
+ // Polyfill Node.js built-ins as no-ops for browser
108
+ alias: {
109
+ 'path': resolve(__dirname, 'shims/path.js'),
110
+ 'fs': resolve(__dirname, 'shims/fs.js'),
111
+ },
112
+ logLevel: 'silent',
113
+ });
114
+
115
+ // Clean up entry file
116
+ execSync(`rm -f "${entryFile}"`);
117
+ return true;
118
+ } catch (e) {
119
+ execSync(`rm -f "${entryFile}"`);
120
+ return false;
121
+ }
122
+ }
123
+
124
+ async function main() {
125
+ const args = process.argv.slice(2);
126
+ const doAll = args.includes('--all');
127
+ const themesArg = args.find(a => a.startsWith('--themes='));
128
+ const specificThemes = themesArg ? themesArg.split('=')[1].split(',') : null;
129
+
130
+ mkdirSync(THEMES_DIR, { recursive: true });
131
+
132
+ // Create shims directory
133
+ const shimsDir = resolve(__dirname, 'shims');
134
+ mkdirSync(shimsDir, { recursive: true });
135
+ writeFileSync(resolve(shimsDir, 'path.js'), `
136
+ export const join = (...parts) => parts.join('/');
137
+ export const resolve = (...parts) => parts.join('/');
138
+ export const dirname = (p) => p.split('/').slice(0, -1).join('/');
139
+ export const basename = (p) => p.split('/').pop();
140
+ export const extname = (p) => { const m = p.match(/\\.[^.]+$/); return m ? m[0] : ''; };
141
+ export default { join, resolve, dirname, basename, extname };
142
+ `);
143
+ writeFileSync(resolve(shimsDir, 'fs.js'), `
144
+ export const readFileSync = () => '';
145
+ export const existsSync = () => false;
146
+ export default { readFileSync, existsSync };
147
+ `);
148
+
149
+ let themes;
150
+ if (specificThemes) {
151
+ themes = specificThemes.map(name => ({
152
+ name,
153
+ packageName: `jsonresume-theme-${name}`,
154
+ description: '',
155
+ version: '',
156
+ }));
157
+ } else if (doAll) {
158
+ themes = await discoverThemes();
159
+ } else {
160
+ themes = POPULAR_THEMES.map(name => ({
161
+ name,
162
+ packageName: `jsonresume-theme-${name}`,
163
+ description: '',
164
+ version: '',
165
+ }));
166
+ }
167
+
168
+ console.log(`📦 Bundling ${themes.length} themes...\n`);
169
+
170
+ const manifest = [];
171
+
172
+ for (const theme of themes) {
173
+ process.stdout.write(` ${theme.name}... `);
174
+
175
+ // Install the theme
176
+ try {
177
+ execSync(`npm install --no-save ${theme.packageName} 2>/dev/null`, {
178
+ cwd: resolve(__dirname, '..'),
179
+ timeout: 30000,
180
+ });
181
+ } catch {
182
+ console.log('❌ install failed');
183
+ continue;
184
+ }
185
+
186
+ // Try to get description from installed package
187
+ try {
188
+ const pkgPath = resolve(__dirname, `../node_modules/${theme.packageName}/package.json`);
189
+ if (existsSync(pkgPath)) {
190
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
191
+ theme.description = theme.description || pkg.description || '';
192
+ theme.version = theme.version || pkg.version || '';
193
+ }
194
+ } catch {}
195
+
196
+ // Bundle it
197
+ const success = await bundleTheme(theme.name, theme.packageName);
198
+
199
+ if (success) {
200
+ const outFile = resolve(THEMES_DIR, `${theme.name}.js`);
201
+ const stat = existsSync(outFile) ? readFileSync(outFile).length : 0;
202
+ manifest.push({
203
+ name: theme.name,
204
+ displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
205
+ description: theme.description,
206
+ browserCompatible: true,
207
+ fileSize: stat,
208
+ });
209
+ console.log(`✅ (${(stat / 1024).toFixed(0)}KB)`);
210
+ } else {
211
+ manifest.push({
212
+ name: theme.name,
213
+ displayName: theme.name.charAt(0).toUpperCase() + theme.name.slice(1).replace(/-/g, ' '),
214
+ description: theme.description,
215
+ browserCompatible: false,
216
+ fileSize: 0,
217
+ });
218
+ console.log('⚠️ browser incompatible');
219
+ }
220
+ }
221
+
222
+ // Write manifest
223
+ writeFileSync(resolve(THEMES_DIR, 'manifest.json'), JSON.stringify(manifest, null, 2));
224
+
225
+ const successful = manifest.filter(t => t.browserCompatible).length;
226
+ console.log(`\n✅ Done! ${successful}/${manifest.length} themes bundled for the browser`);
227
+ console.log(`📁 Output: docs/themes/`);
228
+
229
+ // Clean up shims
230
+ execSync(`rm -rf "${shimsDir}"`);
231
+ }
232
+
233
+ main().catch(console.error);