@yamlresume/node 0.15.1 → 0.15.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 +24 -24
- package/dist/index.d.ts +12 -12
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -16,10 +16,10 @@ npm install @yamlresume/node
|
|
|
16
16
|
## Usage
|
|
17
17
|
|
|
18
18
|
```typescript
|
|
19
|
-
import {
|
|
19
|
+
import { buildResumeFile, readResumeFile } from '@yamlresume/node'
|
|
20
20
|
|
|
21
|
-
const { resume, validated } =
|
|
22
|
-
const { outputs } = await
|
|
21
|
+
const { resume, validated } = readResumeFile('resume.yaml')
|
|
22
|
+
const { outputs } = await buildResumeFile('resume.yaml')
|
|
23
23
|
```
|
|
24
24
|
|
|
25
25
|
For command-line usage, see the
|
|
@@ -27,12 +27,12 @@ For command-line usage, see the
|
|
|
27
27
|
|
|
28
28
|
## API
|
|
29
29
|
|
|
30
|
-
### `
|
|
30
|
+
### `readResumeFile`
|
|
31
31
|
|
|
32
32
|
```typescript
|
|
33
|
-
function
|
|
33
|
+
function readResumeFile(
|
|
34
34
|
resumePath: string,
|
|
35
|
-
options?:
|
|
35
|
+
options?: ReadResumeFileOptions
|
|
36
36
|
): ReadResumeResult
|
|
37
37
|
```
|
|
38
38
|
|
|
@@ -42,7 +42,7 @@ validation status (`'success' | 'failed' | 'unknown'`), and positional errors
|
|
|
42
42
|
with line and column numbers if validation failed.
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
const { resume, validated, errors } =
|
|
45
|
+
const { resume, validated, errors } = readResumeFile('resume.yaml')
|
|
46
46
|
|
|
47
47
|
if (validated === 'failed') {
|
|
48
48
|
for (const error of errors ?? []) {
|
|
@@ -63,12 +63,12 @@ function validateResume(
|
|
|
63
63
|
Validate a raw YAML string against the resume schema. Returns positional
|
|
64
64
|
errors sorted by line number, or an empty array if validation succeeds.
|
|
65
65
|
|
|
66
|
-
### `
|
|
66
|
+
### `buildResumeFile`
|
|
67
67
|
|
|
68
68
|
```typescript
|
|
69
|
-
function
|
|
69
|
+
function buildResumeFile(
|
|
70
70
|
resumePath: string,
|
|
71
|
-
options?:
|
|
71
|
+
options?: BuildResumeFileOptions
|
|
72
72
|
): Promise<BuildResumeResult>
|
|
73
73
|
```
|
|
74
74
|
|
|
@@ -79,37 +79,37 @@ LaTeX compilation timeout, and an optional logger. Returns the list of
|
|
|
79
79
|
generated file paths.
|
|
80
80
|
|
|
81
81
|
```typescript
|
|
82
|
-
const { outputs } = await
|
|
82
|
+
const { outputs } = await buildResumeFile('resume.yaml', {
|
|
83
83
|
pdf: true,
|
|
84
84
|
output: 'dist',
|
|
85
85
|
})
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
### `
|
|
88
|
+
### `newResumeFile`
|
|
89
89
|
|
|
90
90
|
```typescript
|
|
91
|
-
function
|
|
92
|
-
|
|
91
|
+
function newResumeFile(
|
|
92
|
+
resumePath: string,
|
|
93
93
|
sampleId: string,
|
|
94
94
|
language: LocaleLanguage,
|
|
95
|
-
options?:
|
|
95
|
+
options?: NewResumeFileOptions
|
|
96
96
|
): void
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
Create a new resume file from a curated sample resume.
|
|
100
100
|
|
|
101
101
|
```typescript
|
|
102
|
-
|
|
102
|
+
newResumeFile('resume.yaml', 'software-engineer', 'en')
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
### `
|
|
105
|
+
### `generateResumeFile`
|
|
106
106
|
|
|
107
107
|
```typescript
|
|
108
|
-
async function
|
|
109
|
-
|
|
108
|
+
async function generateResumeFile(
|
|
109
|
+
resumePath: string,
|
|
110
110
|
position: string,
|
|
111
111
|
language: string,
|
|
112
|
-
options?:
|
|
112
|
+
options?: GenerateResumeFileOptions
|
|
113
113
|
): Promise<void>
|
|
114
114
|
```
|
|
115
115
|
|
|
@@ -117,12 +117,12 @@ Generate a new resume file with AI for a given position and language.
|
|
|
117
117
|
Supports model selection, retries, streaming chunks via callback, and an
|
|
118
118
|
optional logger.
|
|
119
119
|
|
|
120
|
-
### `
|
|
120
|
+
### `watchResumeFile`
|
|
121
121
|
|
|
122
122
|
```typescript
|
|
123
|
-
function
|
|
123
|
+
function watchResumeFile(
|
|
124
124
|
resumePath: string,
|
|
125
|
-
options?:
|
|
125
|
+
options?: BuildResumeFileOptions
|
|
126
126
|
): chokidar.Watcher
|
|
127
127
|
```
|
|
128
128
|
|
|
@@ -138,7 +138,7 @@ so you can catch and inspect them uniformly:
|
|
|
138
138
|
import { YAMLResumeError } from '@yamlresume/core'
|
|
139
139
|
|
|
140
140
|
try {
|
|
141
|
-
await
|
|
141
|
+
await buildResumeFile('missing.yaml')
|
|
142
142
|
} catch (error) {
|
|
143
143
|
if (error instanceof YAMLResumeError) {
|
|
144
144
|
console.error(error.code, error.message)
|
package/dist/index.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ import * as chokidar from 'chokidar';
|
|
|
28
28
|
/**
|
|
29
29
|
* Options for building resume outputs.
|
|
30
30
|
*/
|
|
31
|
-
interface
|
|
31
|
+
interface BuildResumeFileOptions {
|
|
32
32
|
pdf?: boolean;
|
|
33
33
|
validate?: boolean;
|
|
34
34
|
output?: string;
|
|
@@ -52,7 +52,7 @@ interface BuildResumeResult {
|
|
|
52
52
|
* output directory, and LaTeX compilation timeout.
|
|
53
53
|
* @returns The list of generated output file paths.
|
|
54
54
|
*/
|
|
55
|
-
declare function
|
|
55
|
+
declare function buildResumeFile(resumePath: string, options?: BuildResumeFileOptions): Promise<BuildResumeResult>;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* MIT License
|
|
@@ -81,7 +81,7 @@ declare function buildResume(resumePath: string, options?: BuildResumeOptions):
|
|
|
81
81
|
/**
|
|
82
82
|
* Options for generating a resume with AI.
|
|
83
83
|
*/
|
|
84
|
-
interface
|
|
84
|
+
interface GenerateResumeFileOptions {
|
|
85
85
|
model?: string;
|
|
86
86
|
baseURL?: string;
|
|
87
87
|
maxRetries?: number;
|
|
@@ -91,13 +91,13 @@ interface GenerateResumeOptions {
|
|
|
91
91
|
/**
|
|
92
92
|
* Generate a new resume file with AI for a given position and language.
|
|
93
93
|
*
|
|
94
|
-
* @param
|
|
94
|
+
* @param resumePath - The output resume file path.
|
|
95
95
|
* @param position - The target position or job title.
|
|
96
96
|
* @param language - The target locale language.
|
|
97
97
|
* @param options - Optional model, base URL, retry and callback settings.
|
|
98
98
|
* @throws {YAMLResumeError} When the file already exists or writing fails.
|
|
99
99
|
*/
|
|
100
|
-
declare function
|
|
100
|
+
declare function generateResumeFile(resumePath: string, position: string, language: string, options?: GenerateResumeFileOptions): Promise<void>;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* MIT License
|
|
@@ -126,14 +126,14 @@ declare function generateResume(filename: string, position: string, language: st
|
|
|
126
126
|
/**
|
|
127
127
|
* Options for creating a new resume from a sample.
|
|
128
128
|
*/
|
|
129
|
-
interface
|
|
129
|
+
interface NewResumeFileOptions {
|
|
130
130
|
showSampleSource?: boolean;
|
|
131
131
|
logger?: Logger;
|
|
132
132
|
}
|
|
133
133
|
/**
|
|
134
134
|
* Creates a new resume file from a curated sample resume.
|
|
135
135
|
*
|
|
136
|
-
* @param
|
|
136
|
+
* @param resumePath - The path of the resume file to create.
|
|
137
137
|
* @param sampleId - The identifier of the sample resume to use.
|
|
138
138
|
* @param language - The locale language of the sample resume.
|
|
139
139
|
* @param options - Optional settings.
|
|
@@ -141,7 +141,7 @@ interface NewResumeOptions {
|
|
|
141
141
|
* - FILE_CONFLICT: When the file already exists
|
|
142
142
|
* - FILE_WRITE_ERROR: When there is an error writing the file
|
|
143
143
|
*/
|
|
144
|
-
declare function
|
|
144
|
+
declare function newResumeFile(resumePath: string, sampleId: string, language: LocaleLanguage, options?: NewResumeFileOptions): void;
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* MIT License
|
|
@@ -179,7 +179,7 @@ interface PositionalError {
|
|
|
179
179
|
/**
|
|
180
180
|
* Options for reading a resume file.
|
|
181
181
|
*/
|
|
182
|
-
interface
|
|
182
|
+
interface ReadResumeFileOptions {
|
|
183
183
|
validate?: boolean;
|
|
184
184
|
}
|
|
185
185
|
/**
|
|
@@ -214,7 +214,7 @@ declare function validateResume(yamlStr: string, schema: typeof ResumeSchema): P
|
|
|
214
214
|
* @returns The resume object.
|
|
215
215
|
* @throws {Error} If the source file cannot be read or is invalid.
|
|
216
216
|
*/
|
|
217
|
-
declare function
|
|
217
|
+
declare function readResumeFile(resumePath: string, options?: ReadResumeFileOptions): ReadResumeResult;
|
|
218
218
|
|
|
219
219
|
/**
|
|
220
220
|
* MIT License
|
|
@@ -257,6 +257,6 @@ declare const LATEX_COMPILE_TIMEOUT = 30;
|
|
|
257
257
|
* @param options - Build and watch options
|
|
258
258
|
* @returns Chokidar watcher instance
|
|
259
259
|
*/
|
|
260
|
-
declare function
|
|
260
|
+
declare function watchResumeFile(resumePath: string, options?: BuildResumeFileOptions): chokidar.FSWatcher;
|
|
261
261
|
|
|
262
|
-
export { type
|
|
262
|
+
export { type BuildResumeFileOptions, type BuildResumeResult, type GenerateResumeFileOptions, LATEX_COMPILE_TIMEOUT, type NewResumeFileOptions, type PositionalError, type ReadResumeFileOptions, type ReadResumeResult, buildResumeFile, generateResumeFile, newResumeFile, readResumeFile, validateResume, watchResumeFile };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import
|
|
1
|
+
import F from'fs';import p from'path';import {YAMLResumeError,getErrorMessage,ResumeSchema,joinNonEmptyString,DEFAULT_RESUME_LAYOUTS,toCodeBlock,getResumeRenderer,LOCALE_LANGUAGE_OPTIONS}from'@yamlresume/core';import V,{LineCounter,parseDocument,isNode}from'yaml';import {execa}from'execa';import H from'which';import {generateResume,getModelFromEnv}from'@yamlresume/ai';import {getSampleResume}from'@yamlresume/samples';import pe from'chokidar';import {coalesce}from'coalescifn';function I(e,n){let a=new LineCounter,t=parseDocument(e,{lineCounter:a,keepSourceTokens:true}),i=n.safeParse(t.toJS());if(i.success)return [];let{error:{issues:r}}=i;return r.map(o=>{let u=o.path,c=t.getIn(u,true),l=1,m=1;if(isNode(c)&&c.range){let d=c.range[0],g=a.linePos(d);l=g.line,m=g.col;}return {message:o.message,line:l,column:m,path:u}}).sort((o,u)=>o.line-u.line)}function h(e,n={}){let{validate:a=true}=n,t;try{t=F.readFileSync(e,"utf8");}catch{throw new YAMLResumeError("FILE_READ_ERROR",{path:e})}let i;try{i=V.parse(t);}catch(r){throw new YAMLResumeError("INVALID_YAML",{error:getErrorMessage(r)})}if(a){let r=I(t,ResumeSchema);return r.length>0?{resume:i,validated:"failed",errors:r}:{resume:i,validated:"success"}}return {resume:i,validated:"unknown"}}function v(e){try{return !!H.sync(e)}catch{return false}}function K(){if(v("xelatex"))return "xelatex";if(v("tectonic"))return "tectonic";throw new YAMLResumeError("LATEX_NOT_FOUND",{})}function L(e){return e.replace(/\.tex$/,".pdf")}function k(e,n){let a=p.extname(e);if(e.endsWith(".yaml")||e.endsWith(".yml")||e.endsWith(".json")){let t=p.basename(e.replace(/\.(yaml|yml|json)$/,".tex"));return n?p.join(n,t):e.replace(/\.(yaml|yml|json)$/,".tex")}throw new YAMLResumeError("INVALID_EXTNAME",{extname:a})}function Q(e,n){let a=K(),t=e.endsWith(".tex")?e:k(e,n),i="",r=[];switch(a){case "xelatex":i="xelatex",r=["-halt-on-error",p.basename(t)];break;case "tectonic":i="tectonic",r=[p.basename(t)];break}let o=n?p.resolve(n):p.dirname(p.resolve(t));return {command:i,args:r,cwd:o}}var f=30;function C(e,n){let a=n?p.resolve(n):p.dirname(p.resolve(e));return p.join(a,`${p.basename(e,".tex")}.aux`)}function S(e){try{return F.readFileSync(e,"utf8")}catch{return null}}async function O(e,n,a=f,t){let{command:i,args:r,cwd:o}=Q(e,n),u=C(e,n);t?.start(`Generating resume pdf file with command: \`${i} ${r.join(" ")}\`...`);let c=a===0?void 0:a*1e3,l=S(u),m=2;for(let d=0;d<m;d++){t?.debug(`Running LaTeX pass ${d+1}/${m}`);try{let s=await execa(i,r,{cwd:o,encoding:"utf8",timeout:c});t?.debug(joinNonEmptyString(["stdout: ",toCodeBlock(s.stdout)]));}catch(s){throw s.timedOut?(s.stdout&&(t?.info("LaTeX output before timeout:"),t?.log(s.stdout)),s.stderr&&(t?.info("LaTeX error output:"),t?.log(s.stderr)),new YAMLResumeError("LATEX_COMPILE_TIMEOUT",{timeout:String(a)})):(t?.debug(joinNonEmptyString(["stdout: ",toCodeBlock(s.stdout)])),t?.debug(joinNonEmptyString(["stderr: ",toCodeBlock(s.stderr)])),new YAMLResumeError("LATEX_COMPILE_ERROR",{error:s.message}))}let g=S(u);if(l===g){t?.debug(`LaTeX compilation stabilized after ${d+1} pass(es)`);break}t?.debug("Auxiliary file changed, running LaTeX again..."),l=g;}t?.success(`Generated resume pdf file successfully: ${L(e)}`);}function re(e,n,a,t,i){let r=p.basename(e.replace(/\.(yaml|yml|json)$/,"")),o=t>1?`${r}.${a}${n}`:`${r}${n}`;return i?p.join(i,o):p.join(p.dirname(e),o)}function ne(e){switch(e){case ".docx":return "docx";case ".html":return "html";case ".md":return "markdown";case ".tex":return "tex";default:return e.replace(".","")}}async function w(e,n,a,t,i,r,o,u){let c=re(e,r,a,t,i),l=p.dirname(c);F.existsSync(l)||F.mkdirSync(l,{recursive:true});let d=await getResumeRenderer(n,o).render();try{F.writeFileSync(c,d),u?.success(joinNonEmptyString([`Generated resume ${ne(r)} file successfully:`,c]," "));}catch{throw new YAMLResumeError("FILE_WRITE_ERROR",{path:c})}return c}async function T(e,n={}){let{pdf:a=true,validate:t=true,timeout:i=f,logger:r}=n,{resume:o,validated:u,errors:c}=h(e,{validate:t});if(u==="failed"&&c){r?.warn(joinNonEmptyString(["Resume schema validation failed for",e,"continuing to build anyway."]," "));for(let s of c)r?.warn(`${s.path.join(".")}: ${s.message}`);}let l=o.layouts??DEFAULT_RESUME_LAYOUTS;o.layouts||(o.layouts=l);let m={docx:l.filter(s=>s.engine==="docx").length,html:l.filter(s=>s.engine==="html").length,latex:l.filter(s=>s.engine==="latex").length,markdown:l.filter(s=>s.engine==="markdown").length},d={docx:0,html:0,latex:0,markdown:0},g=[];for(let s=0;s<l.length;s++)switch(l[s].engine){case "docx":{g.push(await w(e,o,d.docx++,m.docx,n.output,".docx",s,r));break}case "html":{g.push(await w(e,o,d.html++,m.html,n.output,".html",s,r));break}case "latex":{let y=await w(e,o,d.latex++,m.latex,n.output,".tex",s,r);g.push(y),a===true&&(await O(y,n.output,i,r),g.push(L(y)));break}case "markdown":{g.push(await w(e,o,d.markdown++,m.markdown,n.output,".md",s,r));break}}return {outputs:g}}function ae(e){if(!LOCALE_LANGUAGE_OPTIONS.includes(e))throw new YAMLResumeError("INVALID_LANGUAGE",{language:e})}async function ue(e,n,a,t={}){if(F.existsSync(e))throw new YAMLResumeError("FILE_CONFLICT",{path:e});ae(a);let{model:i,baseURL:r,maxRetries:o,onChunk:u,logger:c}=t;c?.start("Generating resume...");let l;try{l=await generateResume(n,a,{model:getModelFromEnv({...i&&{model:i},...r&&{baseURL:r}}),...o!==void 0&&{maxRetries:o},...u&&{onChunk:u}});}catch(m){throw c?.debug(joinNonEmptyString(["Error generating resume: ",toCodeBlock(getErrorMessage(m))])),m}try{F.writeFileSync(e,l),c?.success(`Generated ${e} successfully.`);}catch(m){throw c?.debug(joinNonEmptyString(["Error writing resume file: ",toCodeBlock(getErrorMessage(m))])),new YAMLResumeError("FILE_WRITE_ERROR",{path:e})}}function ge(e,n,a,t={}){let{showSampleSource:i=false,logger:r}=t;if(F.existsSync(e))throw new YAMLResumeError("FILE_CONFLICT",{path:e});let o=getSampleResume(n,a,{withLayouts:true,withComments:true});try{F.writeFileSync(e,o);let u=i?`Created ${e} from sample "${n}" successfully.`:`Created ${e} successfully.`;r?.success(u);}catch(u){throw r?.debug(joinNonEmptyString(["Error creating resume: ",toCodeBlock(getErrorMessage(u))])),new YAMLResumeError("FILE_WRITE_ERROR",{path:e})}}function Re(e,n={pdf:true,validate:true}){let{pdf:a,validate:t,output:i,logger:r}=n,o=coalesce(()=>T(e,{pdf:a,validate:t,output:i,logger:r}));o(),r?.start(`Watching file changes: ${e}...`);let u=pe.watch(e,{awaitWriteFinish:{stabilityThreshold:200,pollInterval:200},ignoreInitial:true});return u.on("change",()=>o()),u.on("add",()=>o()),u}export{f as LATEX_COMPILE_TIMEOUT,T as buildResumeFile,ue as generateResumeFile,ge as newResumeFile,h as readResumeFile,I as validateResume,Re as watchResumeFile};//# sourceMappingURL=index.js.map
|
|
2
2
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/read.ts","../src/utils/latex.ts","../src/build.ts","../src/generate.ts","../src/new.ts","../src/watch.ts"],"names":["validateResume","yamlStr","schema","lineCounter","LineCounter","resumeCST","parseDocument","validationResult","issues","issue","path","node","line","column","isNode","startOffset","pos","a","b","readResume","resumePath","options","validate","resumeStr","fs","YAMLResumeError","resume","yaml","error","getErrorMessage","errors","ResumeSchema","isCommandAvailable","command","which","inferLaTeXEnvironment","getPdfPath","texPath","inferOutput","outputDir","extname","baseName","inferLaTeXCommand","resumePathOrTexFile","environment","texFile","args","cwd","LATEX_COMPILE_TIMEOUT","getAuxPath","readAuxFile","auxPath","compileLaTeX","timeout","logger","execaTimeout","previousAux","maxRuns","run","result","execa","joinNonEmptyString","toCodeBlock","currentAux","getOutputPath","extension","index","total","fileName","normalizeExtension","generateOutput","layoutIndex","outputFile","dir","content","getResumeRenderer","buildResume","pdf","validated","allLayouts","DEFAULT_RESUME_LAYOUTS","totals","l","indices","outputs","validateLocaleLanguage","language","LOCALE_LANGUAGE_OPTIONS","generateResume","filename","position","model","baseURL","maxRetries","onChunk","generateResumeWithAI","getModelFromEnv","newResume","sampleId","showSampleSource","sampleContent","getSampleResume","doc","appendResumeLayouts","contentWithLayoutsAndComments","injectResumeComments","successMessage","watchResume","output","exclusiveBuild","coalesce","watcher","chokidar"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport {\n getErrorMessage,\n type Resume,\n ResumeSchema,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport yaml, { isNode, LineCounter, parseDocument } from 'yaml'\n\n/**\n * A positional error with line number, column number, and path.\n */\nexport interface PositionalError {\n // The error message.\n message: string\n // The line number where the error occurred (1-based).\n line: number\n // The column number where the error occurred (1-based).\n column: number\n // The path to the property in the object where the error occurred.\n path: (string | number | symbol)[]\n}\n\n/**\n * Options for reading a resume file.\n */\nexport interface ReadResumeOptions {\n // Optional flag to validate the resume against the schema. Defaults to true.\n validate?: boolean\n}\n\n/**\n * The result of reading a resume file, including the resume object, validation\n * status, and any validation errors.\n */\nexport interface ReadResumeResult {\n // The resume object read from the file.\n resume: Resume\n // The validation status: 'success', 'failed', or 'unknown'.\n validated: 'success' | 'failed' | 'unknown'\n // An array of positional errors if validation failed, otherwise undefined.\n errors?: PositionalError[]\n}\n\n/**\n * Validates a YAML string against a Zod schema and returns errors.\n *\n * @param yamlStr The YAML string to validate.\n * @param schema The Zod schema to validate against.\n * @returns A list of positional errors, or an empty array if validation is\n * successful.\n */\nexport function validateResume(\n yamlStr: string,\n schema: typeof ResumeSchema\n): PositionalError[] {\n const lineCounter = new LineCounter()\n\n // CST: Concrete Syntax Tree\n const resumeCST = parseDocument(yamlStr, {\n lineCounter,\n keepSourceTokens: true,\n })\n\n const validationResult = schema.safeParse(resumeCST.toJS())\n\n if (validationResult.success) {\n return []\n }\n\n const {\n error: { issues },\n } = validationResult\n\n return issues\n .map((issue) => {\n const path = issue.path\n const node = resumeCST.getIn(path, true)\n\n let line = 1\n let column = 1\n\n if (isNode(node) && node.range) {\n const startOffset = node.range[0]\n const pos = lineCounter.linePos(startOffset)\n line = pos.line\n column = pos.col\n }\n\n return {\n message: issue.message,\n line,\n column,\n path,\n }\n })\n .sort((a, b) => a.line - b.line)\n}\n\n/**\n * Read the resume from the source file and validate it on request.\n *\n * Steps:\n *\n * 1. read the resume from the source file\n * 2. validate the resume with `yaml.parse`\n * 3. if `validate` is true, validate the resume with `ResumeSchema`\n *\n * @param resumePath - The source resume file path (YAML, YML, or JSON).\n * @param options - Options for reading and validating the resume.\n * @returns The resume object.\n * @throws {Error} If the source file cannot be read or is invalid.\n */\nexport function readResume(\n resumePath: string,\n options: ReadResumeOptions = {}\n): ReadResumeResult {\n const { validate = true } = options\n\n let resumeStr: string\n\n try {\n resumeStr = fs.readFileSync(resumePath, 'utf8')\n } catch (_error) {\n throw new YAMLResumeError('FILE_READ_ERROR', { path: resumePath })\n }\n\n let resume: Resume\n\n try {\n resume = yaml.parse(resumeStr) as Resume\n } catch (error) {\n throw new YAMLResumeError('INVALID_YAML', {\n error: getErrorMessage(error),\n })\n }\n\n if (validate) {\n const errors = validateResume(resumeStr, ResumeSchema)\n\n if (errors.length > 0) {\n return { resume, validated: 'failed', errors }\n }\n\n return { resume, validated: 'success' }\n }\n\n return { resume, validated: 'unknown' }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport {\n joinNonEmptyString,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { execa } from 'execa'\nimport which from 'which'\n\ntype LaTeXEnvironment = 'xelatex' | 'tectonic'\n\n/**\n * Check if a command is available\n *\n * @param command - The command to check\n * @returns True if the command is available, false otherwise\n */\nexport function isCommandAvailable(command: string): boolean {\n try {\n return !!which.sync(command)\n } catch {\n return false\n }\n}\n\n/**\n * Infer the LaTeX environment to use\n *\n * We support xelatex and tectonic, if both are installed we will prioritize\n * xelatex.\n *\n * @returns The LaTeX environment PATH.\n * @throws {Error} If neither 'xelatex' nor 'tectonic' is found in system PATH.\n */\nexport function inferLaTeXEnvironment(): LaTeXEnvironment {\n if (isCommandAvailable('xelatex')) {\n return 'xelatex'\n }\n\n if (isCommandAvailable('tectonic')) {\n return 'tectonic'\n }\n\n throw new YAMLResumeError('LATEX_NOT_FOUND', {})\n}\n\n/**\n * Get the PDF output path from a tex file path\n *\n * @param texPath - The tex file path\n * @returns The PDF file path\n */\nexport function getPdfPath(texPath: string): string {\n return texPath.replace(/\\.tex$/, '.pdf')\n}\n\n/**\n * Infer the output file name from the source file name\n *\n * For now we support yaml, yml and json file extensions, and the output file\n * will have a `.tex` extension based on the `layouts` config in the resume. The\n * output file will be placed in the same directory as the source file, or in\n * the specified output directory if provided.\n *\n * @param resumePath - The source resume file\n * @param outputDir - Optional output directory to place the file in\n * @returns The output file name\n * @throws {Error} If the source file has an unsupported extension.\n */\nexport function inferOutput(resumePath: string, outputDir?: string): string {\n const extname = path.extname(resumePath)\n\n if (\n resumePath.endsWith('.yaml') ||\n resumePath.endsWith('.yml') ||\n resumePath.endsWith('.json')\n ) {\n const baseName = path.basename(\n resumePath.replace(/\\.(yaml|yml|json)$/, '.tex')\n )\n if (outputDir) {\n return path.join(outputDir, baseName)\n }\n return resumePath.replace(/\\.(yaml|yml|json)$/, '.tex')\n }\n\n throw new YAMLResumeError('INVALID_EXTNAME', { extname })\n}\n\n/**\n * Infer the LaTeX command to use based on the LaTeX environment\n *\n * @param resumePathOrTexFile - The source resume file OR the target .tex file\n * @param outputDir - Optional output directory\n * @returns The LaTeX command\n * @throws {Error} If the LaTeX environment cannot be inferred or the source\n * file extension is unsupported.\n */\nexport function inferLaTeXCommand(\n resumePathOrTexFile: string,\n outputDir?: string\n): { command: string; args: string[]; cwd: string } {\n const environment = inferLaTeXEnvironment()\n\n // If the input is already a .tex file, use it directly; otherwise infer from .yaml/.json\n const texFile = resumePathOrTexFile.endsWith('.tex')\n ? resumePathOrTexFile\n : inferOutput(resumePathOrTexFile, outputDir)\n\n let command = ''\n let args: string[] = []\n\n switch (environment) {\n case 'xelatex':\n command = 'xelatex'\n args = ['-halt-on-error', path.basename(texFile)]\n break\n case 'tectonic':\n command = 'tectonic'\n args = [path.basename(texFile)]\n break\n }\n\n const cwd = outputDir\n ? path.resolve(outputDir)\n : path.dirname(path.resolve(texFile))\n\n return { command, args, cwd }\n}\n\n/**\n * Default timeout for LaTeX compilation in seconds\n */\nexport const LATEX_COMPILE_TIMEOUT = 30\n\n/**\n * Get the auxiliary file path for a tex file\n *\n * @param texFile - The TeX file path\n * @param outputDir - Optional output directory\n * @returns The auxiliary file path\n */\nexport function getAuxPath(texFile: string, outputDir?: string): string {\n const cwd = outputDir\n ? path.resolve(outputDir)\n : path.dirname(path.resolve(texFile))\n return path.join(cwd, `${path.basename(texFile, '.tex')}.aux`)\n}\n\n/**\n * Read the content of an auxiliary file\n *\n * @param auxPath - The auxiliary file path\n * @returns The file content, or null if the file does not exist\n */\nfunction readAuxFile(auxPath: string): string | null {\n try {\n return fs.readFileSync(auxPath, 'utf8')\n } catch {\n return null\n }\n}\n\n/**\n * Compile a TeX file to PDF\n *\n * Runs the LaTeX compiler repeatedly until auxiliary files stabilize, ensuring\n * correct page numbers and cross-references.\n *\n * @param texFile - The TeX file to compile.\n * @param outputDir - Optional output directory.\n * @param timeout - Timeout in seconds. 0 means no timeout.\n * @param logger - Optional logger for progress messages.\n */\nexport async function compileLaTeX(\n texFile: string,\n outputDir?: string,\n timeout: number = LATEX_COMPILE_TIMEOUT,\n logger?: Logger\n) {\n const { command, args, cwd } = inferLaTeXCommand(texFile, outputDir)\n const auxPath = getAuxPath(texFile, outputDir)\n\n logger?.start(\n `Generating resume pdf file with command: \\`${command} ${args.join(' ')}\\`...`\n )\n\n // When timeout is 0, disable timeout by setting it to undefined\n const execaTimeout = timeout === 0 ? undefined : timeout * 1000\n\n let previousAux = readAuxFile(auxPath)\n const maxRuns = 2\n\n for (let run = 0; run < maxRuns; run++) {\n logger?.debug(`Running LaTeX pass ${run + 1}/${maxRuns}`)\n\n try {\n const result = await execa(command, args, {\n cwd,\n encoding: 'utf8',\n timeout: execaTimeout,\n })\n logger?.debug(\n joinNonEmptyString(['stdout: ', toCodeBlock(result.stdout)])\n )\n } catch (error) {\n // Check if it's a timeout error\n if (error.timedOut) {\n // Show raw logs to help users diagnose the issue\n if (error.stdout) {\n logger?.info('LaTeX output before timeout:')\n logger?.log(error.stdout)\n }\n if (error.stderr) {\n logger?.info('LaTeX error output:')\n logger?.log(error.stderr)\n }\n throw new YAMLResumeError('LATEX_COMPILE_TIMEOUT', {\n timeout: String(timeout),\n })\n }\n\n logger?.debug(joinNonEmptyString(['stdout: ', toCodeBlock(error.stdout)]))\n logger?.debug(joinNonEmptyString(['stderr: ', toCodeBlock(error.stderr)]))\n throw new YAMLResumeError('LATEX_COMPILE_ERROR', { error: error.message })\n }\n\n const currentAux = readAuxFile(auxPath)\n if (previousAux === currentAux) {\n logger?.debug(`LaTeX compilation stabilized after ${run + 1} pass(es)`)\n break\n }\n\n logger?.debug('Auxiliary file changed, running LaTeX again...')\n previousAux = currentAux\n }\n\n logger?.success(\n `Generated resume pdf file successfully: ${getPdfPath(texFile)}`\n )\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport {\n DEFAULT_RESUME_LAYOUTS,\n getResumeRenderer,\n joinNonEmptyString,\n type Logger,\n type Resume,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { readResume } from './read'\nimport { compileLaTeX, getPdfPath, LATEX_COMPILE_TIMEOUT } from './utils'\n\n/**\n * Options for building resume outputs.\n */\nexport interface BuildResumeOptions {\n // Whether to generate PDF output from LaTeX. Defaults to true.\n pdf?: boolean\n // Whether to validate the resume against the schema before building. Defaults\n // to true.\n validate?: boolean\n // Optional output directory for generated files. If not specified, outputs to\n // current working directory.\n output?: string\n // Timeout in seconds for LaTeX compilation. Defaults to 30 seconds. Set to 0\n // to disable timeout.\n timeout?: number\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Result of building resume outputs.\n */\nexport interface BuildResumeResult {\n outputs: string[]\n}\n\n/**\n * Get the output file path with support for multiple outputs and custom extension\n *\n * @param resumePath - The source resume file path\n * @param extension - The target file extension (e.g., '.tex', '.md')\n * @param index - The index of the current layout\n * @param total - The total number of layouts for this engine\n * @param outputDir - Optional output directory\n * @returns The determined output file path\n */\nfunction getOutputPath(\n resumePath: string,\n extension: string,\n index: number,\n total: number,\n outputDir?: string\n): string {\n const baseName = path.basename(resumePath.replace(/\\.(yaml|yml|json)$/, ''))\n\n // If there are multiple layouts, append the index to the filename\n // e.g., resume.0.tex, resume.1.tex\n // Otherwise, use the base filename\n // e.g., resume.tex\n const fileName =\n total > 1 ? `${baseName}.${index}${extension}` : `${baseName}${extension}`\n\n if (outputDir) {\n return path.join(outputDir, fileName)\n }\n return path.join(path.dirname(resumePath), fileName)\n}\n\n/**\n * Normalize the file extension that can be used in the output file name\n *\n * @param extension - file extension\n * @returns\n */\nexport function normalizeExtension(extension: string): string {\n switch (extension) {\n case '.docx':\n return 'docx'\n case '.html':\n return 'html'\n case '.md':\n return 'markdown'\n case '.tex':\n return 'tex'\n default:\n return extension.replace('.', '')\n }\n}\n\n/**\n * Shared helper to generate output file from a layout\n */\nasync function generateOutput(\n resumePath: string,\n resume: Resume,\n index: number,\n total: number,\n outputDir: string | undefined,\n extension: string,\n layoutIndex: number,\n logger?: Logger\n): Promise<string> {\n const outputFile = getOutputPath(\n resumePath,\n extension,\n index,\n total,\n outputDir\n )\n\n const dir = path.dirname(outputFile)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n const renderer = getResumeRenderer(resume, layoutIndex)\n const content = await renderer.render()\n\n try {\n fs.writeFileSync(outputFile, content)\n logger?.success(\n joinNonEmptyString(\n [\n `Generated resume ${normalizeExtension(extension)} file successfully:`,\n outputFile,\n ],\n ' '\n )\n )\n } catch (_error) {\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: outputFile })\n }\n\n return outputFile\n}\n\n/**\n * Build a YAML resume to LaTeX & PDF and/or Markdown\n *\n * It first validates the resume against the schema (unless validation is\n * disabled), then iterates through configured layouts to generate outputs.\n *\n * @param resumePath - The source resume file path (YAML, YML, or JSON).\n * @param options - Build options including validation, PDF generation flags,\n * output directory, and LaTeX compilation timeout.\n * @returns The list of generated output file paths.\n */\nexport async function buildResume(\n resumePath: string,\n options: BuildResumeOptions = {}\n): Promise<BuildResumeResult> {\n const {\n pdf = true,\n validate = true,\n timeout = LATEX_COMPILE_TIMEOUT,\n logger,\n } = options\n\n const { resume, validated, errors } = readResume(resumePath, { validate })\n\n if (validated === 'failed' && errors) {\n logger?.warn(\n joinNonEmptyString(\n [\n 'Resume schema validation failed for',\n resumePath,\n 'continuing to build anyway.',\n ],\n ' '\n )\n )\n for (const error of errors) {\n logger?.warn(`${error.path.join('.')}: ${error.message}`)\n }\n }\n\n // Fallback to default layout if none provided\n const allLayouts = resume.layouts ?? DEFAULT_RESUME_LAYOUTS\n // Ensure resume has layouts for the renderer to use\n if (!resume.layouts) {\n resume.layouts = allLayouts\n }\n\n // Count totals for each engine to determine file naming strategy\n // (e.g. resume.0.tex vs resume.tex)\n const totals = {\n docx: allLayouts.filter((l) => l.engine === 'docx').length,\n html: allLayouts.filter((l) => l.engine === 'html').length,\n latex: allLayouts.filter((l) => l.engine === 'latex').length,\n markdown: allLayouts.filter((l) => l.engine === 'markdown').length,\n }\n\n // Track current index for each engine\n const indices = {\n docx: 0,\n html: 0,\n latex: 0,\n markdown: 0,\n }\n\n const outputs: string[] = []\n\n for (let layoutIndex = 0; layoutIndex < allLayouts.length; layoutIndex++) {\n const layout = allLayouts[layoutIndex]\n\n switch (layout.engine) {\n case 'docx': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.docx++,\n totals.docx,\n options.output,\n '.docx',\n layoutIndex,\n logger\n )\n )\n break\n }\n case 'html': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.html++,\n totals.html,\n options.output,\n '.html',\n layoutIndex,\n logger\n )\n )\n break\n }\n case 'latex': {\n const texFile = await generateOutput(\n resumePath,\n resume,\n indices.latex++,\n totals.latex,\n options.output,\n '.tex',\n layoutIndex,\n logger\n )\n outputs.push(texFile)\n\n if (pdf === true) {\n await compileLaTeX(texFile, options.output, timeout, logger)\n outputs.push(getPdfPath(texFile))\n }\n break\n }\n case 'markdown': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.markdown++,\n totals.markdown,\n options.output,\n '.md',\n layoutIndex,\n logger\n )\n )\n break\n }\n }\n }\n\n return { outputs }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport {\n generateResume as generateResumeWithAI,\n getModelFromEnv,\n} from '@yamlresume/ai'\nimport {\n getErrorMessage,\n joinNonEmptyString,\n LOCALE_LANGUAGE_OPTIONS,\n type LocaleLanguage,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\n\n/**\n * Options for generating a resume with AI.\n */\nexport interface GenerateResumeOptions {\n // Optional settings for generating a resume with AI.\n model?: string\n // Optional base URL for the AI service.\n baseURL?: string\n // Optional maximum number of retries for AI generation.\n maxRetries?: number\n // Optional callback function to handle chunks of generated content.\n onChunk?: (chunk: string) => void\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Validate that a locale language is supported by YAMLResume.\n *\n * @param language - The language code to validate.\n * @throws {YAMLResumeError} When the language is not supported.\n */\nexport function validateLocaleLanguage(\n language: string\n): asserts language is LocaleLanguage {\n if (\n !LOCALE_LANGUAGE_OPTIONS.includes(\n language as (typeof LOCALE_LANGUAGE_OPTIONS)[number]\n )\n ) {\n throw new YAMLResumeError('INVALID_LANGUAGE', { language })\n }\n}\n\n/**\n * Generate a new resume file with AI for a given position and language.\n *\n * @param filename - The output resume file path.\n * @param position - The target position or job title.\n * @param language - The target locale language.\n * @param options - Optional model, base URL, retry and callback settings.\n * @throws {YAMLResumeError} When the file already exists or writing fails.\n */\nexport async function generateResume(\n filename: string,\n position: string,\n language: string,\n options: GenerateResumeOptions = {}\n): Promise<void> {\n if (fs.existsSync(filename)) {\n throw new YAMLResumeError('FILE_CONFLICT', { path: filename })\n }\n\n validateLocaleLanguage(language)\n\n const { model, baseURL, maxRetries, onChunk, logger } = options\n\n logger?.start('Generating resume...')\n\n let content: string\n try {\n content = await generateResumeWithAI({\n position,\n language,\n model: getModelFromEnv({\n ...(model && { model }),\n ...(baseURL && { baseURL }),\n }),\n ...(maxRetries !== undefined && { maxRetries }),\n ...(onChunk && { onChunk }),\n })\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error generating resume: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw error\n }\n\n try {\n fs.writeFileSync(filename, content)\n logger?.success(`Generated ${filename} successfully.`)\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error writing resume file: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: filename })\n }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport {\n appendResumeLayouts,\n getErrorMessage,\n injectResumeComments,\n joinNonEmptyString,\n type LocaleLanguage,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { getSampleResume } from '@yamlresume/samples'\nimport yaml from 'yaml'\n\n/**\n * Options for creating a new resume from a sample.\n */\nexport interface NewResumeOptions {\n // Optional flag to show the source of the sample resume in the success\n // message.\n showSampleSource?: boolean\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Creates a new resume file from a curated sample resume.\n *\n * @param filename - The name of the resume file to create.\n * @param sampleId - The identifier of the sample resume to use.\n * @param language - The locale language of the sample resume.\n * @param options - Optional settings.\n * @throws {YAMLResumeError} When there are file-related errors:\n * - FILE_CONFLICT: When the file already exists\n * - FILE_WRITE_ERROR: When there is an error writing the file\n */\nexport function newResume(\n filename: string,\n sampleId: string,\n language: LocaleLanguage,\n options: NewResumeOptions = {}\n) {\n const { showSampleSource = false, logger } = options\n\n if (fs.existsSync(filename)) {\n throw new YAMLResumeError('FILE_CONFLICT', { path: filename })\n }\n\n const sampleContent = getSampleResume(sampleId, language)\n const doc = yaml.parseDocument(sampleContent)\n appendResumeLayouts(doc)\n const contentWithLayoutsAndComments = injectResumeComments(doc)\n\n try {\n fs.writeFileSync(filename, contentWithLayoutsAndComments)\n\n const successMessage = showSampleSource\n ? `Created ${filename} from sample \"${sampleId}\" successfully.`\n : `Created ${filename} successfully.`\n\n logger?.success(successMessage)\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error creating resume: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: filename })\n }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport chokidar from 'chokidar'\nimport { coalesce } from 'coalescifn'\n\nimport { type BuildResumeOptions, buildResume } from './build'\n\n/**\n * Watch a resume source file and rebuild on changes.\n *\n * - Only one build runs at a time.\n * - If multiple events arrive during a build, run exactly one more build after\n * it finishes (coalesce bursts).\n * - Uses chokidar for robust file watching that handles editor operations.\n *\n * @param resumePath - The resume file to watch\n * @param options - Build and watch options\n * @returns Chokidar watcher instance\n */\nexport function watchResume(\n resumePath: string,\n options: BuildResumeOptions = { pdf: true, validate: true }\n) {\n const { pdf, validate, output, logger } = options\n\n // there should be only one build running at a time\n const exclusiveBuild = coalesce(() =>\n buildResume(resumePath, { pdf, validate, output, logger })\n )\n\n // initial build\n exclusiveBuild()\n\n logger?.start(`Watching file changes: ${resumePath}...`)\n\n // use chokidar for robust file watching that handles vim and other editors\n // properly.\n //\n // vim will save the file in a single atomic operation, that being said, it\n // will first create a temporary file (the '.swp' file), then rename it to the\n // final file.\n //\n // Node.js `fs.watch` has trouble with this, so we use chokidar instead.\n const watcher = chokidar.watch(resumePath, {\n awaitWriteFinish: {\n stabilityThreshold: 200, // wait 200ms after file stops changing\n pollInterval: 200, // check every 200ms\n },\n ignoreInitial: true, // don't trigger on initial file discovery\n })\n\n // handle file changes - chokidar's awaitWriteFinish already handles\n // debouncing\n watcher.on('change', () => exclusiveBuild())\n\n // handle file additions (in case file gets recreated)\n watcher.on('add', () => exclusiveBuild())\n\n return watcher\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/read.ts","../src/utils/latex.ts","../src/build.ts","../src/generate.ts","../src/new.ts","../src/watch.ts"],"names":["validateResume","yamlStr","schema","lineCounter","LineCounter","resumeCST","parseDocument","validationResult","issues","issue","path","node","line","column","isNode","startOffset","pos","a","b","readResumeFile","resumePath","options","validate","resumeStr","fs","YAMLResumeError","resume","yaml","error","getErrorMessage","errors","ResumeSchema","isCommandAvailable","command","which","inferLaTeXEnvironment","getPdfPath","texPath","inferOutput","outputDir","extname","baseName","inferLaTeXCommand","resumePathOrTexFile","environment","texFile","args","cwd","LATEX_COMPILE_TIMEOUT","getAuxPath","readAuxFile","auxPath","compileLaTeX","timeout","logger","execaTimeout","previousAux","maxRuns","run","result","execa","joinNonEmptyString","toCodeBlock","currentAux","getOutputPath","extension","index","total","fileName","normalizeExtension","generateOutput","layoutIndex","outputFile","dir","content","getResumeRenderer","buildResumeFile","pdf","validated","allLayouts","DEFAULT_RESUME_LAYOUTS","totals","l","indices","outputs","validateLocaleLanguage","language","LOCALE_LANGUAGE_OPTIONS","generateResumeFile","position","model","baseURL","maxRetries","onChunk","generateResume","getModelFromEnv","newResumeFile","sampleId","showSampleSource","sampleContent","getSampleResume","successMessage","watchResumeFile","output","exclusiveBuild","coalesce","watcher","chokidar"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport {\n getErrorMessage,\n type Resume,\n ResumeSchema,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport yaml, { isNode, LineCounter, parseDocument } from 'yaml'\n\n/**\n * A positional error with line number, column number, and path.\n */\nexport interface PositionalError {\n // The error message.\n message: string\n // The line number where the error occurred (1-based).\n line: number\n // The column number where the error occurred (1-based).\n column: number\n // The path to the property in the object where the error occurred.\n path: (string | number | symbol)[]\n}\n\n/**\n * Options for reading a resume file.\n */\nexport interface ReadResumeFileOptions {\n // Optional flag to validate the resume against the schema. Defaults to true.\n validate?: boolean\n}\n\n/**\n * The result of reading a resume file, including the resume object, validation\n * status, and any validation errors.\n */\nexport interface ReadResumeResult {\n // The resume object read from the file.\n resume: Resume\n // The validation status: 'success', 'failed', or 'unknown'.\n validated: 'success' | 'failed' | 'unknown'\n // An array of positional errors if validation failed, otherwise undefined.\n errors?: PositionalError[]\n}\n\n/**\n * Validates a YAML string against a Zod schema and returns errors.\n *\n * @param yamlStr The YAML string to validate.\n * @param schema The Zod schema to validate against.\n * @returns A list of positional errors, or an empty array if validation is\n * successful.\n */\nexport function validateResume(\n yamlStr: string,\n schema: typeof ResumeSchema\n): PositionalError[] {\n const lineCounter = new LineCounter()\n\n // CST: Concrete Syntax Tree\n const resumeCST = parseDocument(yamlStr, {\n lineCounter,\n keepSourceTokens: true,\n })\n\n const validationResult = schema.safeParse(resumeCST.toJS())\n\n if (validationResult.success) {\n return []\n }\n\n const {\n error: { issues },\n } = validationResult\n\n return issues\n .map((issue) => {\n const path = issue.path\n const node = resumeCST.getIn(path, true)\n\n let line = 1\n let column = 1\n\n if (isNode(node) && node.range) {\n const startOffset = node.range[0]\n const pos = lineCounter.linePos(startOffset)\n line = pos.line\n column = pos.col\n }\n\n return {\n message: issue.message,\n line,\n column,\n path,\n }\n })\n .sort((a, b) => a.line - b.line)\n}\n\n/**\n * Read the resume from the source file and validate it on request.\n *\n * Steps:\n *\n * 1. read the resume from the source file\n * 2. validate the resume with `yaml.parse`\n * 3. if `validate` is true, validate the resume with `ResumeSchema`\n *\n * @param resumePath - The source resume file path (YAML, YML, or JSON).\n * @param options - Options for reading and validating the resume.\n * @returns The resume object.\n * @throws {Error} If the source file cannot be read or is invalid.\n */\nexport function readResumeFile(\n resumePath: string,\n options: ReadResumeFileOptions = {}\n): ReadResumeResult {\n const { validate = true } = options\n\n let resumeStr: string\n\n try {\n resumeStr = fs.readFileSync(resumePath, 'utf8')\n } catch (_error) {\n throw new YAMLResumeError('FILE_READ_ERROR', { path: resumePath })\n }\n\n let resume: Resume\n\n try {\n resume = yaml.parse(resumeStr) as Resume\n } catch (error) {\n throw new YAMLResumeError('INVALID_YAML', {\n error: getErrorMessage(error),\n })\n }\n\n if (validate) {\n const errors = validateResume(resumeStr, ResumeSchema)\n\n if (errors.length > 0) {\n return { resume, validated: 'failed', errors }\n }\n\n return { resume, validated: 'success' }\n }\n\n return { resume, validated: 'unknown' }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport {\n joinNonEmptyString,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { execa } from 'execa'\nimport which from 'which'\n\ntype LaTeXEnvironment = 'xelatex' | 'tectonic'\n\n/**\n * Check if a command is available\n *\n * @param command - The command to check\n * @returns True if the command is available, false otherwise\n */\nexport function isCommandAvailable(command: string): boolean {\n try {\n return !!which.sync(command)\n } catch {\n return false\n }\n}\n\n/**\n * Infer the LaTeX environment to use\n *\n * We support xelatex and tectonic, if both are installed we will prioritize\n * xelatex.\n *\n * @returns The LaTeX environment PATH.\n * @throws {Error} If neither 'xelatex' nor 'tectonic' is found in system PATH.\n */\nexport function inferLaTeXEnvironment(): LaTeXEnvironment {\n if (isCommandAvailable('xelatex')) {\n return 'xelatex'\n }\n\n if (isCommandAvailable('tectonic')) {\n return 'tectonic'\n }\n\n throw new YAMLResumeError('LATEX_NOT_FOUND', {})\n}\n\n/**\n * Get the PDF output path from a tex file path\n *\n * @param texPath - The tex file path\n * @returns The PDF file path\n */\nexport function getPdfPath(texPath: string): string {\n return texPath.replace(/\\.tex$/, '.pdf')\n}\n\n/**\n * Infer the output file name from the source file name\n *\n * For now we support yaml, yml and json file extensions, and the output file\n * will have a `.tex` extension based on the `layouts` config in the resume. The\n * output file will be placed in the same directory as the source file, or in\n * the specified output directory if provided.\n *\n * @param resumePath - The source resume file\n * @param outputDir - Optional output directory to place the file in\n * @returns The output file name\n * @throws {Error} If the source file has an unsupported extension.\n */\nexport function inferOutput(resumePath: string, outputDir?: string): string {\n const extname = path.extname(resumePath)\n\n if (\n resumePath.endsWith('.yaml') ||\n resumePath.endsWith('.yml') ||\n resumePath.endsWith('.json')\n ) {\n const baseName = path.basename(\n resumePath.replace(/\\.(yaml|yml|json)$/, '.tex')\n )\n if (outputDir) {\n return path.join(outputDir, baseName)\n }\n return resumePath.replace(/\\.(yaml|yml|json)$/, '.tex')\n }\n\n throw new YAMLResumeError('INVALID_EXTNAME', { extname })\n}\n\n/**\n * Infer the LaTeX command to use based on the LaTeX environment\n *\n * @param resumePathOrTexFile - The source resume file OR the target .tex file\n * @param outputDir - Optional output directory\n * @returns The LaTeX command\n * @throws {Error} If the LaTeX environment cannot be inferred or the source\n * file extension is unsupported.\n */\nexport function inferLaTeXCommand(\n resumePathOrTexFile: string,\n outputDir?: string\n): { command: string; args: string[]; cwd: string } {\n const environment = inferLaTeXEnvironment()\n\n // If the input is already a .tex file, use it directly; otherwise infer from .yaml/.json\n const texFile = resumePathOrTexFile.endsWith('.tex')\n ? resumePathOrTexFile\n : inferOutput(resumePathOrTexFile, outputDir)\n\n let command = ''\n let args: string[] = []\n\n switch (environment) {\n case 'xelatex':\n command = 'xelatex'\n args = ['-halt-on-error', path.basename(texFile)]\n break\n case 'tectonic':\n command = 'tectonic'\n args = [path.basename(texFile)]\n break\n }\n\n const cwd = outputDir\n ? path.resolve(outputDir)\n : path.dirname(path.resolve(texFile))\n\n return { command, args, cwd }\n}\n\n/**\n * Default timeout for LaTeX compilation in seconds\n */\nexport const LATEX_COMPILE_TIMEOUT = 30\n\n/**\n * Get the auxiliary file path for a tex file\n *\n * @param texFile - The TeX file path\n * @param outputDir - Optional output directory\n * @returns The auxiliary file path\n */\nexport function getAuxPath(texFile: string, outputDir?: string): string {\n const cwd = outputDir\n ? path.resolve(outputDir)\n : path.dirname(path.resolve(texFile))\n return path.join(cwd, `${path.basename(texFile, '.tex')}.aux`)\n}\n\n/**\n * Read the content of an auxiliary file\n *\n * @param auxPath - The auxiliary file path\n * @returns The file content, or null if the file does not exist\n */\nfunction readAuxFile(auxPath: string): string | null {\n try {\n return fs.readFileSync(auxPath, 'utf8')\n } catch {\n return null\n }\n}\n\n/**\n * Compile a TeX file to PDF\n *\n * Runs the LaTeX compiler repeatedly until auxiliary files stabilize, ensuring\n * correct page numbers and cross-references.\n *\n * @param texFile - The TeX file to compile.\n * @param outputDir - Optional output directory.\n * @param timeout - Timeout in seconds. 0 means no timeout.\n * @param logger - Optional logger for progress messages.\n */\nexport async function compileLaTeX(\n texFile: string,\n outputDir?: string,\n timeout: number = LATEX_COMPILE_TIMEOUT,\n logger?: Logger\n) {\n const { command, args, cwd } = inferLaTeXCommand(texFile, outputDir)\n const auxPath = getAuxPath(texFile, outputDir)\n\n logger?.start(\n `Generating resume pdf file with command: \\`${command} ${args.join(' ')}\\`...`\n )\n\n // When timeout is 0, disable timeout by setting it to undefined\n const execaTimeout = timeout === 0 ? undefined : timeout * 1000\n\n let previousAux = readAuxFile(auxPath)\n const maxRuns = 2\n\n for (let run = 0; run < maxRuns; run++) {\n logger?.debug(`Running LaTeX pass ${run + 1}/${maxRuns}`)\n\n try {\n const result = await execa(command, args, {\n cwd,\n encoding: 'utf8',\n timeout: execaTimeout,\n })\n logger?.debug(\n joinNonEmptyString(['stdout: ', toCodeBlock(result.stdout)])\n )\n } catch (error) {\n // Check if it's a timeout error\n if (error.timedOut) {\n // Show raw logs to help users diagnose the issue\n if (error.stdout) {\n logger?.info('LaTeX output before timeout:')\n logger?.log(error.stdout)\n }\n if (error.stderr) {\n logger?.info('LaTeX error output:')\n logger?.log(error.stderr)\n }\n throw new YAMLResumeError('LATEX_COMPILE_TIMEOUT', {\n timeout: String(timeout),\n })\n }\n\n logger?.debug(joinNonEmptyString(['stdout: ', toCodeBlock(error.stdout)]))\n logger?.debug(joinNonEmptyString(['stderr: ', toCodeBlock(error.stderr)]))\n throw new YAMLResumeError('LATEX_COMPILE_ERROR', { error: error.message })\n }\n\n const currentAux = readAuxFile(auxPath)\n if (previousAux === currentAux) {\n logger?.debug(`LaTeX compilation stabilized after ${run + 1} pass(es)`)\n break\n }\n\n logger?.debug('Auxiliary file changed, running LaTeX again...')\n previousAux = currentAux\n }\n\n logger?.success(\n `Generated resume pdf file successfully: ${getPdfPath(texFile)}`\n )\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport {\n DEFAULT_RESUME_LAYOUTS,\n getResumeRenderer,\n joinNonEmptyString,\n type Logger,\n type Resume,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { readResumeFile } from './read'\nimport { compileLaTeX, getPdfPath, LATEX_COMPILE_TIMEOUT } from './utils'\n\n/**\n * Options for building resume outputs.\n */\nexport interface BuildResumeFileOptions {\n // Whether to generate PDF output from LaTeX. Defaults to true.\n pdf?: boolean\n // Whether to validate the resume against the schema before building. Defaults\n // to true.\n validate?: boolean\n // Optional output directory for generated files. If not specified, outputs to\n // current working directory.\n output?: string\n // Timeout in seconds for LaTeX compilation. Defaults to 30 seconds. Set to 0\n // to disable timeout.\n timeout?: number\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Result of building resume outputs.\n */\nexport interface BuildResumeResult {\n outputs: string[]\n}\n\n/**\n * Get the output file path with support for multiple outputs and custom extension\n *\n * @param resumePath - The source resume file path\n * @param extension - The target file extension (e.g., '.tex', '.md')\n * @param index - The index of the current layout\n * @param total - The total number of layouts for this engine\n * @param outputDir - Optional output directory\n * @returns The determined output file path\n */\nfunction getOutputPath(\n resumePath: string,\n extension: string,\n index: number,\n total: number,\n outputDir?: string\n): string {\n const baseName = path.basename(resumePath.replace(/\\.(yaml|yml|json)$/, ''))\n\n // If there are multiple layouts, append the index to the filename\n // e.g., resume.0.tex, resume.1.tex\n // Otherwise, use the base filename\n // e.g., resume.tex\n const fileName =\n total > 1 ? `${baseName}.${index}${extension}` : `${baseName}${extension}`\n\n if (outputDir) {\n return path.join(outputDir, fileName)\n }\n return path.join(path.dirname(resumePath), fileName)\n}\n\n/**\n * Normalize the file extension that can be used in the output file name\n *\n * @param extension - file extension\n * @returns\n */\nexport function normalizeExtension(extension: string): string {\n switch (extension) {\n case '.docx':\n return 'docx'\n case '.html':\n return 'html'\n case '.md':\n return 'markdown'\n case '.tex':\n return 'tex'\n default:\n return extension.replace('.', '')\n }\n}\n\n/**\n * Shared helper to generate output file from a layout\n */\nasync function generateOutput(\n resumePath: string,\n resume: Resume,\n index: number,\n total: number,\n outputDir: string | undefined,\n extension: string,\n layoutIndex: number,\n logger?: Logger\n): Promise<string> {\n const outputFile = getOutputPath(\n resumePath,\n extension,\n index,\n total,\n outputDir\n )\n\n const dir = path.dirname(outputFile)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n const renderer = getResumeRenderer(resume, layoutIndex)\n const content = await renderer.render()\n\n try {\n fs.writeFileSync(outputFile, content)\n logger?.success(\n joinNonEmptyString(\n [\n `Generated resume ${normalizeExtension(extension)} file successfully:`,\n outputFile,\n ],\n ' '\n )\n )\n } catch (_error) {\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: outputFile })\n }\n\n return outputFile\n}\n\n/**\n * Build a YAML resume to LaTeX & PDF and/or Markdown\n *\n * It first validates the resume against the schema (unless validation is\n * disabled), then iterates through configured layouts to generate outputs.\n *\n * @param resumePath - The source resume file path (YAML, YML, or JSON).\n * @param options - Build options including validation, PDF generation flags,\n * output directory, and LaTeX compilation timeout.\n * @returns The list of generated output file paths.\n */\nexport async function buildResumeFile(\n resumePath: string,\n options: BuildResumeFileOptions = {}\n): Promise<BuildResumeResult> {\n const {\n pdf = true,\n validate = true,\n timeout = LATEX_COMPILE_TIMEOUT,\n logger,\n } = options\n\n const { resume, validated, errors } = readResumeFile(resumePath, { validate })\n\n if (validated === 'failed' && errors) {\n logger?.warn(\n joinNonEmptyString(\n [\n 'Resume schema validation failed for',\n resumePath,\n 'continuing to build anyway.',\n ],\n ' '\n )\n )\n for (const error of errors) {\n logger?.warn(`${error.path.join('.')}: ${error.message}`)\n }\n }\n\n // Fallback to default layout if none provided\n const allLayouts = resume.layouts ?? DEFAULT_RESUME_LAYOUTS\n // Ensure resume has layouts for the renderer to use\n if (!resume.layouts) {\n resume.layouts = allLayouts\n }\n\n // Count totals for each engine to determine file naming strategy\n // (e.g. resume.0.tex vs resume.tex)\n const totals = {\n docx: allLayouts.filter((l) => l.engine === 'docx').length,\n html: allLayouts.filter((l) => l.engine === 'html').length,\n latex: allLayouts.filter((l) => l.engine === 'latex').length,\n markdown: allLayouts.filter((l) => l.engine === 'markdown').length,\n }\n\n // Track current index for each engine\n const indices = {\n docx: 0,\n html: 0,\n latex: 0,\n markdown: 0,\n }\n\n const outputs: string[] = []\n\n for (let layoutIndex = 0; layoutIndex < allLayouts.length; layoutIndex++) {\n const layout = allLayouts[layoutIndex]\n\n switch (layout.engine) {\n case 'docx': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.docx++,\n totals.docx,\n options.output,\n '.docx',\n layoutIndex,\n logger\n )\n )\n break\n }\n case 'html': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.html++,\n totals.html,\n options.output,\n '.html',\n layoutIndex,\n logger\n )\n )\n break\n }\n case 'latex': {\n const texFile = await generateOutput(\n resumePath,\n resume,\n indices.latex++,\n totals.latex,\n options.output,\n '.tex',\n layoutIndex,\n logger\n )\n outputs.push(texFile)\n\n if (pdf === true) {\n await compileLaTeX(texFile, options.output, timeout, logger)\n outputs.push(getPdfPath(texFile))\n }\n break\n }\n case 'markdown': {\n outputs.push(\n await generateOutput(\n resumePath,\n resume,\n indices.markdown++,\n totals.markdown,\n options.output,\n '.md',\n layoutIndex,\n logger\n )\n )\n break\n }\n }\n }\n\n return { outputs }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport { generateResume, getModelFromEnv } from '@yamlresume/ai'\nimport {\n getErrorMessage,\n joinNonEmptyString,\n LOCALE_LANGUAGE_OPTIONS,\n type LocaleLanguage,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\n\n/**\n * Options for generating a resume with AI.\n */\nexport interface GenerateResumeFileOptions {\n // Optional settings for generating a resume with AI.\n model?: string\n // Optional base URL for the AI service.\n baseURL?: string\n // Optional maximum number of retries for AI generation.\n maxRetries?: number\n // Optional callback function to handle chunks of generated content.\n onChunk?: (chunk: string) => void\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Validate that a locale language is supported by YAMLResume.\n *\n * @param language - The language code to validate.\n * @throws {YAMLResumeError} When the language is not supported.\n */\nexport function validateLocaleLanguage(\n language: string\n): asserts language is LocaleLanguage {\n if (\n !LOCALE_LANGUAGE_OPTIONS.includes(\n language as (typeof LOCALE_LANGUAGE_OPTIONS)[number]\n )\n ) {\n throw new YAMLResumeError('INVALID_LANGUAGE', { language })\n }\n}\n\n/**\n * Generate a new resume file with AI for a given position and language.\n *\n * @param resumePath - The output resume file path.\n * @param position - The target position or job title.\n * @param language - The target locale language.\n * @param options - Optional model, base URL, retry and callback settings.\n * @throws {YAMLResumeError} When the file already exists or writing fails.\n */\nexport async function generateResumeFile(\n resumePath: string,\n position: string,\n language: string,\n options: GenerateResumeFileOptions = {}\n): Promise<void> {\n if (fs.existsSync(resumePath)) {\n throw new YAMLResumeError('FILE_CONFLICT', { path: resumePath })\n }\n\n validateLocaleLanguage(language)\n\n const { model, baseURL, maxRetries, onChunk, logger } = options\n\n logger?.start('Generating resume...')\n\n let content: string\n try {\n content = await generateResume(position, language as LocaleLanguage, {\n model: getModelFromEnv({\n ...(model && { model }),\n ...(baseURL && { baseURL }),\n }),\n ...(maxRetries !== undefined && { maxRetries }),\n ...(onChunk && { onChunk }),\n })\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error generating resume: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw error\n }\n\n try {\n fs.writeFileSync(resumePath, content)\n logger?.success(`Generated ${resumePath} successfully.`)\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error writing resume file: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: resumePath })\n }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport fs from 'node:fs'\nimport {\n getErrorMessage,\n joinNonEmptyString,\n type LocaleLanguage,\n type Logger,\n toCodeBlock,\n YAMLResumeError,\n} from '@yamlresume/core'\nimport { getSampleResume } from '@yamlresume/samples'\n\n/**\n * Options for creating a new resume from a sample.\n */\nexport interface NewResumeFileOptions {\n // Optional flag to show the source of the sample resume in the success\n // message.\n showSampleSource?: boolean\n // Optional logger for progress messages. If not provided, no logs will be\n // shown.\n logger?: Logger\n}\n\n/**\n * Creates a new resume file from a curated sample resume.\n *\n * @param resumePath - The path of the resume file to create.\n * @param sampleId - The identifier of the sample resume to use.\n * @param language - The locale language of the sample resume.\n * @param options - Optional settings.\n * @throws {YAMLResumeError} When there are file-related errors:\n * - FILE_CONFLICT: When the file already exists\n * - FILE_WRITE_ERROR: When there is an error writing the file\n */\nexport function newResumeFile(\n resumePath: string,\n sampleId: string,\n language: LocaleLanguage,\n options: NewResumeFileOptions = {}\n) {\n const { showSampleSource = false, logger } = options\n\n if (fs.existsSync(resumePath)) {\n throw new YAMLResumeError('FILE_CONFLICT', { path: resumePath })\n }\n\n const sampleContent = getSampleResume(sampleId, language, {\n withLayouts: true,\n withComments: true,\n })\n\n try {\n fs.writeFileSync(resumePath, sampleContent)\n\n const successMessage = showSampleSource\n ? `Created ${resumePath} from sample \"${sampleId}\" successfully.`\n : `Created ${resumePath} successfully.`\n\n logger?.success(successMessage)\n } catch (error) {\n logger?.debug(\n joinNonEmptyString([\n 'Error creating resume: ',\n toCodeBlock(getErrorMessage(error)),\n ])\n )\n throw new YAMLResumeError('FILE_WRITE_ERROR', { path: resumePath })\n }\n}\n","/**\n * MIT License\n *\n * Copyright (c) 2023–Present PPResume (https://ppresume.com)\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport chokidar from 'chokidar'\nimport { coalesce } from 'coalescifn'\n\nimport { type BuildResumeFileOptions, buildResumeFile } from './build'\n\n/**\n * Watch a resume source file and rebuild on changes.\n *\n * - Only one build runs at a time.\n * - If multiple events arrive during a build, run exactly one more build after\n * it finishes (coalesce bursts).\n * - Uses chokidar for robust file watching that handles editor operations.\n *\n * @param resumePath - The resume file to watch\n * @param options - Build and watch options\n * @returns Chokidar watcher instance\n */\nexport function watchResumeFile(\n resumePath: string,\n options: BuildResumeFileOptions = { pdf: true, validate: true }\n) {\n const { pdf, validate, output, logger } = options\n\n // there should be only one build running at a time\n const exclusiveBuild = coalesce(() =>\n buildResumeFile(resumePath, { pdf, validate, output, logger })\n )\n\n // initial build\n exclusiveBuild()\n\n logger?.start(`Watching file changes: ${resumePath}...`)\n\n // use chokidar for robust file watching that handles vim and other editors\n // properly.\n //\n // vim will save the file in a single atomic operation, that being said, it\n // will first create a temporary file (the '.swp' file), then rename it to the\n // final file.\n //\n // Node.js `fs.watch` has trouble with this, so we use chokidar instead.\n const watcher = chokidar.watch(resumePath, {\n awaitWriteFinish: {\n stabilityThreshold: 200, // wait 200ms after file stops changing\n pollInterval: 200, // check every 200ms\n },\n ignoreInitial: true, // don't trigger on initial file discovery\n })\n\n // handle file changes - chokidar's awaitWriteFinish already handles\n // debouncing\n watcher.on('change', () => exclusiveBuild())\n\n // handle file additions (in case file gets recreated)\n watcher.on('add', () => exclusiveBuild())\n\n return watcher\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yamlresume/node",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "Node.js runtime support for YAMLResume",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"execa": "^10.0.0",
|
|
40
40
|
"which": "^7.0.0",
|
|
41
41
|
"yaml": "^2.9.0",
|
|
42
|
-
"@yamlresume/ai": "0.15.
|
|
43
|
-
"@yamlresume/
|
|
44
|
-
"@yamlresume/
|
|
42
|
+
"@yamlresume/ai": "0.15.2",
|
|
43
|
+
"@yamlresume/core": "0.15.2",
|
|
44
|
+
"@yamlresume/samples": "0.15.2"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/node": "^26.1.1",
|
|
48
48
|
"@types/which": "^3.0.4",
|
|
49
|
-
"@yamlresume/testing": "0.15.
|
|
49
|
+
"@yamlresume/testing": "0.15.2"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|