linkedin-resume 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/dist/linkedin-resume.cjs +135 -96
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ linkedin-resume --render
|
|
|
32
32
|
## Usage
|
|
33
33
|
|
|
34
34
|
```
|
|
35
|
-
Usage: linkedin-resume [options] [command]
|
|
35
|
+
Usage: linkedin-resume [options] [command] [outputFilepath]
|
|
36
36
|
|
|
37
37
|
A CLI tool to generate a LinkedIn resume in PDF format.
|
|
38
38
|
|
|
@@ -43,7 +43,7 @@ Options:
|
|
|
43
43
|
-V, --version output the version number
|
|
44
44
|
--debug enable debug output
|
|
45
45
|
--render skip scraping, only render
|
|
46
|
-
--headless
|
|
46
|
+
--no-headless show scraping browser window
|
|
47
47
|
--keep-open keep browser open after scraping
|
|
48
48
|
-h, --help display help for command
|
|
49
49
|
|
|
@@ -54,11 +54,11 @@ Commands:
|
|
|
54
54
|
### Examples
|
|
55
55
|
|
|
56
56
|
```sh
|
|
57
|
-
# Scrape and generate
|
|
57
|
+
# Scrape and generate in headless mode (default)
|
|
58
58
|
linkedin-resume
|
|
59
59
|
|
|
60
|
-
#
|
|
61
|
-
linkedin-resume --headless
|
|
60
|
+
# Show the browser window during scraping
|
|
61
|
+
linkedin-resume --no-headless
|
|
62
62
|
|
|
63
63
|
# Skip scraping, re-render from previously scraped data
|
|
64
64
|
linkedin-resume --render
|
|
@@ -112,11 +112,11 @@ linkedin-resume config
|
|
|
112
112
|
"ignore": {
|
|
113
113
|
// Set to true to hide the entire section, or an array to hide specific entries
|
|
114
114
|
"work": [{ "name": "Company Inc.", "position": "Intern" }],
|
|
115
|
-
"education":
|
|
115
|
+
"education": true,
|
|
116
116
|
"projects": [{ "name": "Old Project" }],
|
|
117
117
|
"skills": [{ "name": "Microsoft Word" }],
|
|
118
|
-
"languages":
|
|
119
|
-
"recommendations":
|
|
118
|
+
"languages": true,
|
|
119
|
+
"recommendations": true,
|
|
120
120
|
},
|
|
121
121
|
}
|
|
122
122
|
```
|
package/dist/linkedin-resume.cjs
CHANGED
|
@@ -69,6 +69,108 @@ async function patchEsbuildHelpers(page) {
|
|
|
69
69
|
}
|
|
70
70
|
__name(patchEsbuildHelpers, "patchEsbuildHelpers");
|
|
71
71
|
|
|
72
|
+
// src/linkedin/utils/injectBrowserHelpers.ts
|
|
73
|
+
async function injectBrowserHelpers(page) {
|
|
74
|
+
await page.evaluate(() => {
|
|
75
|
+
;
|
|
76
|
+
globalThis.__getTextWithBreaks = (el) => {
|
|
77
|
+
let text = "";
|
|
78
|
+
for (const node of el.childNodes) {
|
|
79
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
80
|
+
text += node.textContent;
|
|
81
|
+
} else if (node.nodeName === "BR") {
|
|
82
|
+
text += "\n";
|
|
83
|
+
} else {
|
|
84
|
+
text += globalThis.__getTextWithBreaks(node);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return text.trim();
|
|
88
|
+
};
|
|
89
|
+
globalThis.__getVisibleSpans = (el) => {
|
|
90
|
+
return Array.from(el.querySelectorAll("span")).filter((span) => !span.className.includes("visually-hidden") && span.hasAttribute("aria-hidden")).map((span) => globalThis.__getTextWithBreaks(span));
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
__name(injectBrowserHelpers, "injectBrowserHelpers");
|
|
95
|
+
|
|
96
|
+
// ../../node_modules/mimic-function/index.js
|
|
97
|
+
var copyProperty = /* @__PURE__ */ __name((to, from, property, ignoreNonConfigurable) => {
|
|
98
|
+
if (property === "length" || property === "prototype") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (property === "arguments" || property === "caller") {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const toDescriptor = Object.getOwnPropertyDescriptor(to, property);
|
|
105
|
+
const fromDescriptor = Object.getOwnPropertyDescriptor(from, property);
|
|
106
|
+
if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
Object.defineProperty(to, property, fromDescriptor);
|
|
110
|
+
}, "copyProperty");
|
|
111
|
+
var canCopyProperty = /* @__PURE__ */ __name(function(toDescriptor, fromDescriptor) {
|
|
112
|
+
return toDescriptor === void 0 || toDescriptor.configurable || toDescriptor.writable === fromDescriptor.writable && toDescriptor.enumerable === fromDescriptor.enumerable && toDescriptor.configurable === fromDescriptor.configurable && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value);
|
|
113
|
+
}, "canCopyProperty");
|
|
114
|
+
var changePrototype = /* @__PURE__ */ __name((to, from) => {
|
|
115
|
+
const fromPrototype = Object.getPrototypeOf(from);
|
|
116
|
+
if (fromPrototype === Object.getPrototypeOf(to)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
Object.setPrototypeOf(to, fromPrototype);
|
|
120
|
+
}, "changePrototype");
|
|
121
|
+
var wrappedToString = /* @__PURE__ */ __name((withName, fromBody) => `/* Wrapped ${withName}*/
|
|
122
|
+
${fromBody}`, "wrappedToString");
|
|
123
|
+
var toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, "toString");
|
|
124
|
+
var toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, "name");
|
|
125
|
+
var changeToString = /* @__PURE__ */ __name((to, from, name) => {
|
|
126
|
+
const withName = name === "" ? "" : `with ${name.trim()}() `;
|
|
127
|
+
const newToString = wrappedToString.bind(null, withName, from.toString());
|
|
128
|
+
Object.defineProperty(newToString, "name", toStringName);
|
|
129
|
+
const { writable, enumerable, configurable } = toStringDescriptor;
|
|
130
|
+
Object.defineProperty(to, "toString", { value: newToString, writable, enumerable, configurable });
|
|
131
|
+
}, "changeToString");
|
|
132
|
+
function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) {
|
|
133
|
+
const { name } = to;
|
|
134
|
+
for (const property of Reflect.ownKeys(from)) {
|
|
135
|
+
copyProperty(to, from, property, ignoreNonConfigurable);
|
|
136
|
+
}
|
|
137
|
+
changePrototype(to, from);
|
|
138
|
+
changeToString(to, from, name);
|
|
139
|
+
return to;
|
|
140
|
+
}
|
|
141
|
+
__name(mimicFunction, "mimicFunction");
|
|
142
|
+
|
|
143
|
+
// ../../node_modules/onetime/index.js
|
|
144
|
+
var calledFunctions = /* @__PURE__ */ new WeakMap();
|
|
145
|
+
var onetime = /* @__PURE__ */ __name((function_, options = {}) => {
|
|
146
|
+
if (typeof function_ !== "function") {
|
|
147
|
+
throw new TypeError("Expected a function");
|
|
148
|
+
}
|
|
149
|
+
let returnValue;
|
|
150
|
+
let callCount = 0;
|
|
151
|
+
const functionName = function_.displayName || function_.name || "<anonymous>";
|
|
152
|
+
const onetime2 = /* @__PURE__ */ __name(function(...arguments_) {
|
|
153
|
+
calledFunctions.set(onetime2, ++callCount);
|
|
154
|
+
if (callCount === 1) {
|
|
155
|
+
returnValue = function_.apply(this, arguments_);
|
|
156
|
+
function_ = void 0;
|
|
157
|
+
} else if (options.throw === true) {
|
|
158
|
+
throw new Error(`Function \`${functionName}\` can only be called once`);
|
|
159
|
+
}
|
|
160
|
+
return returnValue;
|
|
161
|
+
}, "onetime");
|
|
162
|
+
mimicFunction(onetime2, function_);
|
|
163
|
+
calledFunctions.set(onetime2, callCount);
|
|
164
|
+
return onetime2;
|
|
165
|
+
}, "onetime");
|
|
166
|
+
onetime.callCount = (function_) => {
|
|
167
|
+
if (!calledFunctions.has(function_)) {
|
|
168
|
+
throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`);
|
|
169
|
+
}
|
|
170
|
+
return calledFunctions.get(function_);
|
|
171
|
+
};
|
|
172
|
+
var onetime_default = onetime;
|
|
173
|
+
|
|
72
174
|
// src/loadUserConfig.ts
|
|
73
175
|
var import_promises = require("node:readline/promises");
|
|
74
176
|
|
|
@@ -89,7 +191,7 @@ var JsonFileStrategy = class {
|
|
|
89
191
|
}
|
|
90
192
|
}
|
|
91
193
|
save(config) {
|
|
92
|
-
import_fs_extra.default.outputFileSync(this.filepath, JSON.stringify(config, null, 2));
|
|
194
|
+
import_fs_extra.default.outputFileSync(this.filepath, JSON.stringify(config, null, 2) + "\n");
|
|
93
195
|
}
|
|
94
196
|
};
|
|
95
197
|
|
|
@@ -240,7 +342,7 @@ __name(getAppDataPath, "getAppDataPath");
|
|
|
240
342
|
// src/constants.ts
|
|
241
343
|
var CONFIG_PATH = getAppDataPath("bemoje", "linkedin-resume", "config.json");
|
|
242
344
|
var DIST_PATH = getAppDataPath("bemoje", "linkedin-resume", "dist");
|
|
243
|
-
var
|
|
345
|
+
var CHROME_PROFILE_PATH = getAppDataPath("bemoje", "linkedin-resume", ".chrome-profile");
|
|
244
346
|
import_fs_extra2.default.ensureDirSync(DIST_PATH);
|
|
245
347
|
|
|
246
348
|
// src/userConfigFile.ts
|
|
@@ -402,10 +504,10 @@ async function loadUserConfig() {
|
|
|
402
504
|
__name(loadUserConfig, "loadUserConfig");
|
|
403
505
|
|
|
404
506
|
// src/linkedin/utils/getLinkedInUsername.ts
|
|
405
|
-
var getLinkedInUsername =
|
|
507
|
+
var getLinkedInUsername = onetime_default(async () => {
|
|
406
508
|
const userConfig = await loadUserConfig();
|
|
407
509
|
return userConfig.linkedInUsername;
|
|
408
|
-
}
|
|
510
|
+
});
|
|
409
511
|
|
|
410
512
|
// src/linkedin/scrapeProfile.ts
|
|
411
513
|
async function scrapeProfile(browser, options) {
|
|
@@ -430,6 +532,7 @@ async function scrapeProfile(browser, options) {
|
|
|
430
532
|
await new Promise((r) => setTimeout(r, 250));
|
|
431
533
|
await autoScroll(page);
|
|
432
534
|
await patchEsbuildHelpers(page);
|
|
535
|
+
await injectBrowserHelpers(page);
|
|
433
536
|
if (options.debug) {
|
|
434
537
|
const debugDump = await page.evaluate(() => {
|
|
435
538
|
const result2 = {};
|
|
@@ -462,19 +565,7 @@ async function scrapeProfile(browser, options) {
|
|
|
462
565
|
};
|
|
463
566
|
const imgEl = document.querySelector(".pv-top-card-profile-picture__image--show") || document.querySelector(".pv-top-card-profile-picture__image") || document.querySelector("img.profile-photo-edit__preview") || document.querySelector('img[class*="profile"][width="200"]') || document.querySelector('main img[src*="profile"]');
|
|
464
567
|
const image = imgEl?.src ?? "";
|
|
465
|
-
const getTextWithBreaks =
|
|
466
|
-
let text = "";
|
|
467
|
-
for (const node of el.childNodes) {
|
|
468
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
469
|
-
text += node.textContent;
|
|
470
|
-
} else if (node.nodeName === "BR") {
|
|
471
|
-
text += "\n";
|
|
472
|
-
} else {
|
|
473
|
-
text += getTextWithBreaks(node);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
return text;
|
|
477
|
-
}, "getTextWithBreaks");
|
|
568
|
+
const getTextWithBreaks = globalThis.__getTextWithBreaks;
|
|
478
569
|
let summary = "";
|
|
479
570
|
const aboutAnchor = document.querySelector("#about");
|
|
480
571
|
if (aboutAnchor) {
|
|
@@ -651,26 +742,12 @@ async function scrapeSkills(browser, options) {
|
|
|
651
742
|
await page.waitForSelector(".scaffold-finite-scroll__content", { timeout: 6e4 });
|
|
652
743
|
await autoScroll(page);
|
|
653
744
|
await patchEsbuildHelpers(page);
|
|
745
|
+
await injectBrowserHelpers(page);
|
|
654
746
|
const rawEntries = await page.evaluate(() => {
|
|
655
747
|
const container = document.querySelector(".scaffold-finite-scroll__content");
|
|
656
748
|
if (!container) return [];
|
|
657
749
|
const topLevelItems = Array.from(container.querySelector("ul")?.children ?? []);
|
|
658
|
-
const
|
|
659
|
-
let text = "";
|
|
660
|
-
for (const node of el.childNodes) {
|
|
661
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
662
|
-
text += node.textContent;
|
|
663
|
-
} else if (node.nodeName === "BR") {
|
|
664
|
-
text += "\n";
|
|
665
|
-
} else {
|
|
666
|
-
text += getTextWithBreaks(node);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
return text.trim();
|
|
670
|
-
}, "getTextWithBreaks");
|
|
671
|
-
const getVisibleSpans = /* @__PURE__ */ __name((el) => {
|
|
672
|
-
return Array.from(el.querySelectorAll("span")).filter((span) => !span.className.includes("visually-hidden") && span.hasAttribute("aria-hidden")).map((span) => getTextWithBreaks(span));
|
|
673
|
-
}, "getVisibleSpans");
|
|
750
|
+
const getVisibleSpans = globalThis.__getVisibleSpans;
|
|
674
751
|
return topLevelItems.map((li) => ({
|
|
675
752
|
spans: getVisibleSpans(li),
|
|
676
753
|
logoUrl: li.querySelector("img")?.src ?? ""
|
|
@@ -803,26 +880,12 @@ async function scrapeProjects(browser, options) {
|
|
|
803
880
|
await page.waitForSelector(".scaffold-finite-scroll__content", { timeout: 6e4 });
|
|
804
881
|
await autoScroll(page);
|
|
805
882
|
await patchEsbuildHelpers(page);
|
|
883
|
+
await injectBrowserHelpers(page);
|
|
806
884
|
const rawEntries = await page.evaluate(() => {
|
|
807
885
|
const container = document.querySelector(".scaffold-finite-scroll__content");
|
|
808
886
|
if (!container) return [];
|
|
809
887
|
const topLevelItems = Array.from(container.querySelector("ul")?.children ?? []);
|
|
810
|
-
const
|
|
811
|
-
let text = "";
|
|
812
|
-
for (const node of el.childNodes) {
|
|
813
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
814
|
-
text += node.textContent;
|
|
815
|
-
} else if (node.nodeName === "BR") {
|
|
816
|
-
text += "\n";
|
|
817
|
-
} else {
|
|
818
|
-
text += getTextWithBreaks(node);
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
return text.trim();
|
|
822
|
-
}, "getTextWithBreaks");
|
|
823
|
-
const getVisibleSpans = /* @__PURE__ */ __name((el) => {
|
|
824
|
-
return Array.from(el.querySelectorAll("span")).filter((span) => !span.className.includes("visually-hidden") && span.hasAttribute("aria-hidden")).map((span) => getTextWithBreaks(span));
|
|
825
|
-
}, "getVisibleSpans");
|
|
888
|
+
const getVisibleSpans = globalThis.__getVisibleSpans;
|
|
826
889
|
return topLevelItems.map((li) => {
|
|
827
890
|
const mediaLinks = Array.from(li.querySelectorAll("a[href]")).filter((a) => {
|
|
828
891
|
const href = a.getAttribute("href") ?? "";
|
|
@@ -938,26 +1001,12 @@ async function scrapeEducation(browser, options) {
|
|
|
938
1001
|
await page.waitForSelector(".scaffold-finite-scroll__content", { timeout: 6e4 });
|
|
939
1002
|
await autoScroll(page);
|
|
940
1003
|
await patchEsbuildHelpers(page);
|
|
1004
|
+
await injectBrowserHelpers(page);
|
|
941
1005
|
const rawEntries = await page.evaluate(() => {
|
|
942
1006
|
const container = document.querySelector(".scaffold-finite-scroll__content");
|
|
943
1007
|
if (!container) return [];
|
|
944
1008
|
const topLevelItems = Array.from(container.querySelector("ul")?.children ?? []);
|
|
945
|
-
const
|
|
946
|
-
let text = "";
|
|
947
|
-
for (const node of el.childNodes) {
|
|
948
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
949
|
-
text += node.textContent;
|
|
950
|
-
} else if (node.nodeName === "BR") {
|
|
951
|
-
text += "\n";
|
|
952
|
-
} else {
|
|
953
|
-
text += getTextWithBreaks(node);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
return text.trim();
|
|
957
|
-
}, "getTextWithBreaks");
|
|
958
|
-
const getVisibleSpans = /* @__PURE__ */ __name((el) => {
|
|
959
|
-
return Array.from(el.querySelectorAll("span")).filter((span) => !span.className.includes("visually-hidden") && span.hasAttribute("aria-hidden")).map((span) => getTextWithBreaks(span));
|
|
960
|
-
}, "getVisibleSpans");
|
|
1009
|
+
const getVisibleSpans = globalThis.__getVisibleSpans;
|
|
961
1010
|
return topLevelItems.map((li) => ({
|
|
962
1011
|
spans: getVisibleSpans(li),
|
|
963
1012
|
logoUrl: li.querySelector("img")?.src ?? ""
|
|
@@ -969,12 +1018,8 @@ async function scrapeEducation(browser, options) {
|
|
|
969
1018
|
let parseEducationDate2 = function(d) {
|
|
970
1019
|
if (!d) return "";
|
|
971
1020
|
if (/^\d{4}$/.test(d)) return `${d}-01-01`;
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
return date.toISOString().slice(0, 7) + "-01";
|
|
975
|
-
} catch {
|
|
976
|
-
return d;
|
|
977
|
-
}
|
|
1021
|
+
const parsed = parseDate(d);
|
|
1022
|
+
return parsed && parsed !== d ? parsed + "-01" : d;
|
|
978
1023
|
};
|
|
979
1024
|
var parseEducationDate = parseEducationDate2;
|
|
980
1025
|
__name(parseEducationDate2, "parseEducationDate");
|
|
@@ -1083,26 +1128,12 @@ async function scrapeExperience(browser, options) {
|
|
|
1083
1128
|
await page.waitForSelector(".scaffold-finite-scroll__content", { timeout: 6e4 });
|
|
1084
1129
|
await autoScroll(page);
|
|
1085
1130
|
await patchEsbuildHelpers(page);
|
|
1131
|
+
await injectBrowserHelpers(page);
|
|
1086
1132
|
const rawEntries = await page.evaluate(() => {
|
|
1087
1133
|
const container = document.querySelector(".scaffold-finite-scroll__content");
|
|
1088
1134
|
if (!container) return [];
|
|
1089
1135
|
const topLevelItems = Array.from(container.querySelector("ul")?.children ?? []);
|
|
1090
|
-
const
|
|
1091
|
-
let text = "";
|
|
1092
|
-
for (const node of el.childNodes) {
|
|
1093
|
-
if (node.nodeType === Node.TEXT_NODE) {
|
|
1094
|
-
text += node.textContent;
|
|
1095
|
-
} else if (node.nodeName === "BR") {
|
|
1096
|
-
text += "\n";
|
|
1097
|
-
} else {
|
|
1098
|
-
text += getTextWithBreaks(node);
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
return text.trim();
|
|
1102
|
-
}, "getTextWithBreaks");
|
|
1103
|
-
const getVisibleSpans = /* @__PURE__ */ __name((el) => {
|
|
1104
|
-
return Array.from(el.querySelectorAll("span")).filter((span) => !span.className.includes("visually-hidden") && span.hasAttribute("aria-hidden")).map((span) => getTextWithBreaks(span));
|
|
1105
|
-
}, "getVisibleSpans");
|
|
1136
|
+
const getVisibleSpans = globalThis.__getVisibleSpans;
|
|
1106
1137
|
return topLevelItems.map((li) => ({
|
|
1107
1138
|
spans: getVisibleSpans(li),
|
|
1108
1139
|
logoUrl: li.querySelector("img")?.src ?? ""
|
|
@@ -1168,7 +1199,7 @@ __name(scrapeExperience, "scrapeExperience");
|
|
|
1168
1199
|
async function scrapeLinkedIn(options) {
|
|
1169
1200
|
const browser = await import_puppeteer.default.launch({
|
|
1170
1201
|
headless: options.headless ? "shell" : false,
|
|
1171
|
-
userDataDir:
|
|
1202
|
+
userDataDir: CHROME_PROFILE_PATH,
|
|
1172
1203
|
args: ["--start-maximized"],
|
|
1173
1204
|
defaultViewport: null
|
|
1174
1205
|
});
|
|
@@ -1887,7 +1918,8 @@ function renderLanguages(resume) {
|
|
|
1887
1918
|
}
|
|
1888
1919
|
__name(renderLanguages, "renderLanguages");
|
|
1889
1920
|
function renderRecommendations(resume) {
|
|
1890
|
-
const
|
|
1921
|
+
const linkedInProfile = resume.basics.profiles.find((p) => p.network.toLowerCase() === "linkedin");
|
|
1922
|
+
const username = linkedInProfile?.username ?? "";
|
|
1891
1923
|
const href = `https://www.linkedin.com/in/${username}/details/recommendations`;
|
|
1892
1924
|
if (!resume.recommendations?.length) return "";
|
|
1893
1925
|
return `
|
|
@@ -1936,7 +1968,7 @@ async function loadResume() {
|
|
|
1936
1968
|
}
|
|
1937
1969
|
__name(loadResume, "loadResume");
|
|
1938
1970
|
function esc(s) {
|
|
1939
|
-
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1971
|
+
return (s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1940
1972
|
}
|
|
1941
1973
|
__name(esc, "esc");
|
|
1942
1974
|
function formatDate(d) {
|
|
@@ -2023,12 +2055,20 @@ __name(renderResumeJson, "renderResumeJson");
|
|
|
2023
2055
|
var import_fs_extra11 = __toESM(require("fs-extra"), 1);
|
|
2024
2056
|
var import_upath4 = __toESM(require("upath"), 1);
|
|
2025
2057
|
var import_url = require("url");
|
|
2058
|
+
|
|
2059
|
+
// src/utils/expandEnvVars.ts
|
|
2060
|
+
function expandEnvVars(filepath) {
|
|
2061
|
+
return filepath.replace(/\$\{(\w+)\}|\$(\w+)/g, (_2, a, b) => process.env[a || b] ?? "");
|
|
2062
|
+
}
|
|
2063
|
+
__name(expandEnvVars, "expandEnvVars");
|
|
2064
|
+
|
|
2065
|
+
// src/renderPdfFromHtml.ts
|
|
2026
2066
|
var import_puppeteer2 = __toESM(require("puppeteer"), 1);
|
|
2027
2067
|
async function renderPdfFromHtml(outputFilepath, options) {
|
|
2028
2068
|
const htmlPath = import_upath4.default.join(DIST_PATH, "resume.html");
|
|
2029
2069
|
const pdfPath = import_upath4.default.join(DIST_PATH, "resume.pdf");
|
|
2030
2070
|
const htmlFileUrl = (0, import_url.pathToFileURL)(htmlPath).href;
|
|
2031
|
-
if (!await import_fs_extra11.default.
|
|
2071
|
+
if (!await import_fs_extra11.default.pathExists(htmlPath)) {
|
|
2032
2072
|
throw new Error(`HTML file not found: ${htmlPath}. Run renderResumeHtml first.`);
|
|
2033
2073
|
}
|
|
2034
2074
|
await import_fs_extra11.default.ensureDir(import_upath4.default.dirname(pdfPath));
|
|
@@ -2052,12 +2092,12 @@ async function renderPdfFromHtml(outputFilepath, options) {
|
|
|
2052
2092
|
} finally {
|
|
2053
2093
|
await browser.close();
|
|
2054
2094
|
}
|
|
2055
|
-
if (!import_fs_extra11.default.
|
|
2095
|
+
if (!await import_fs_extra11.default.pathExists(pdfPath)) {
|
|
2056
2096
|
throw new Error(`PDF was not created at ${pdfPath}`);
|
|
2057
2097
|
}
|
|
2058
2098
|
console.log(`output: ${pdfPath}`);
|
|
2059
2099
|
const userConfig = await loadUserConfig();
|
|
2060
|
-
const outputFilepathToUse = outputFilepath || userConfig.outputFilepath;
|
|
2100
|
+
const outputFilepathToUse = expandEnvVars(outputFilepath || userConfig.outputFilepath);
|
|
2061
2101
|
await import_fs_extra11.default.ensureDir(import_upath4.default.dirname(outputFilepathToUse));
|
|
2062
2102
|
await import_fs_extra11.default.copy(pdfPath, outputFilepathToUse);
|
|
2063
2103
|
console.log("PDF:", outputFilepathToUse);
|
|
@@ -2073,7 +2113,7 @@ var import_puppeteer3 = __toESM(require("puppeteer"), 1);
|
|
|
2073
2113
|
async function ensureUserLoggedInToLinkedIn() {
|
|
2074
2114
|
const browser = await import_puppeteer3.default.launch({
|
|
2075
2115
|
headless: false,
|
|
2076
|
-
userDataDir:
|
|
2116
|
+
userDataDir: CHROME_PROFILE_PATH,
|
|
2077
2117
|
args: ["--start-maximized"],
|
|
2078
2118
|
defaultViewport: null
|
|
2079
2119
|
});
|
|
@@ -2109,11 +2149,10 @@ __name(ensureUserLoggedInToLinkedIn, "ensureUserLoggedInToLinkedIn");
|
|
|
2109
2149
|
var description_default = "A CLI tool to generate a LinkedIn resume in PDF format.";
|
|
2110
2150
|
|
|
2111
2151
|
// src/core/version.ts
|
|
2112
|
-
var version_default = `0.
|
|
2152
|
+
var version_default = `0.3.0`;
|
|
2113
2153
|
|
|
2114
2154
|
// src/main.ts
|
|
2115
2155
|
var cli = new import_commander.Command("linkedin-resume").version(version_default).description(description_default).argument("[outputFilepath]", "Optional filepath. Defaults to value set in config file.").option("--debug", "enable debug output").option("--render", "skip scraping, only render").option("--no-headless", "show scraping browser window").option("--keep-open", "keep browser open after scraping").action(async (outputFilepath, options) => {
|
|
2116
|
-
options.headless = options.headless ?? true;
|
|
2117
2156
|
if (options.debug) {
|
|
2118
2157
|
console.log({ argv: process.argv });
|
|
2119
2158
|
console.dir({ config: await loadUserConfig() }, { depth: null });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "linkedin-resume",
|
|
3
3
|
"description": "A CLI tool to generate a LinkedIn resume in PDF format.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"packageManager": "yarn@4.3.1",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/cli.mjs",
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"@sinclair/typebox": "^0.34.37",
|
|
32
32
|
"commander": "^14.0.0",
|
|
33
33
|
"fs-extra": "^11.3.0",
|
|
34
|
+
"onetime": "^7.0.0",
|
|
34
35
|
"puppeteer": "^24.37.3",
|
|
35
36
|
"upath": "^2.0.1"
|
|
36
37
|
},
|