33# - On Windows, if an existing uninstaller fails, gives an option to continue with the installation
44# despite the failure (https://github.com/laurent22/joplin/pull/11541).
55#
6+ diff --git a/out/node-module-collector/nodeModulesCollector.d.ts b/out/node-module-collector/nodeModulesCollector.d.ts
7+ index 601ab15e1c0617eb5782372cbcf7d9854ae308b0..d01cd0fd786923112de84f9cbacf31180a6c7980 100644
8+ --- a/out/node-module-collector/nodeModulesCollector.d.ts
9+ +++ b/out/node-module-collector/nodeModulesCollector.d.ts
10+ @@ -25,6 +25,7 @@ export declare abstract class NodeModulesCollector<ProdDepType extends Dependenc
11+ */
12+ getNodeModules({ packageName }: {
13+ packageName: string;
14+ + workspaceDir: string;
15+ }): Promise<{
16+ nodeModules: NodeModuleInfo[];
17+ logSummary: ModuleManager["logSummary"];
18+ diff --git a/out/node-module-collector/nodeModulesCollector.js b/out/node-module-collector/nodeModulesCollector.js
19+ index 0956d2657279b06ff210e5008583b9c35b533d5f..62044f0ab215635aeb8a358f6e6c2fbcb8cdc502 100644
20+ --- a/out/node-module-collector/nodeModulesCollector.js
21+ +++ b/out/node-module-collector/nodeModulesCollector.js
22+ @@ -45,8 +45,8 @@ class NodeModulesCollector {
23+ * 5. Hoisting the dependencies to their final locations
24+ * 6. Resolving and returning module information
25+ */
26+ - async getNodeModules({ packageName }) {
27+ - const tree = await this.getDependenciesTree(this.installOptions.manager);
28+ + async getNodeModules({ packageName, workspaceDir }) {
29+ + const tree = await this.getDependenciesTree(this.installOptions.manager, workspaceDir);
30+ await this.collectAllDependencies(tree, packageName);
31+ const realTree = this.getTreeFromWorkspaces(tree, packageName);
32+ await this.extractProductionDependencyGraph(realTree, packageName);
33+ @@ -64,7 +64,7 @@ class NodeModulesCollector {
34+ * the output to a temporary file. Includes retry logic to handle transient failures such as
35+ * incomplete JSON output or missing files. Will retry up to 1 time with exponential backoff.
36+ */
37+ - async getDependenciesTree(pm) {
38+ + async getDependenciesTree(pm, _workspaceDir) {
39+ const command = (0, packageManager_1.getPackageManagerCommand)(pm);
40+ const args = this.getArgs();
41+ const tempOutputFile = await this.tempDirManager.getTempFile({
642diff --git a/out/node-module-collector/traversalNodeModulesCollector.js b/out/node-module-collector/traversalNodeModulesCollector.js
7- index 851c5427b676c0c8e65b7b63bcf8eadef68904a3..38c6f85195683584b4bf32d784bc521c065f9d69 100644
43+ index 851c5427b676c0c8e65b7b63bcf8eadef68904a3..6391c1f3be447cf794ebc7bd5c00b46d34ef4adf 100644
844--- a/out/node-module-collector/traversalNodeModulesCollector.js
945+++ b/out/node-module-collector/traversalNodeModulesCollector.js
1046@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
@@ -16,22 +52,41 @@ index 851c5427b676c0c8e65b7b63bcf8eadef68904a3..38c6f85195683584b4bf32d784bc521c
1652 const moduleManager_1 = require("./moduleManager");
1753 const nodeModulesCollector_1 = require("./nodeModulesCollector");
1854 const packageManager_js_1 = require("./packageManager.js");
19- @@ -53,6 +55,34 @@ class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCo
55+ @@ -18,9 +20,9 @@ class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCo
56+ getArgs() {
57+ return [];
58+ }
59+ - getDependenciesTree(_pm) {
60+ + getDependenciesTree(_pm, workspaceDir) {
61+ builder_util_1.log.info(null, "using manual traversal of node_modules to build dependency tree");
62+ - return this.buildNodeModulesTreeManually(this.rootDir, undefined);
63+ + return this.buildNodeModulesTreeManually(this.rootDir, undefined, workspaceDir);
64+ }
65+ async collectAllDependencies(tree, appPackageName) {
66+ for (const [packageKey, value] of Object.entries({ ...tree.dependencies, ...tree.optionalDependencies })) {
67+ @@ -49,10 +51,40 @@ class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCo
68+ * Builds a dependency tree using only package.json dependencies and optionalDependencies.
69+ * This skips devDependencies and uses Node.js module resolution (require.resolve).
70+ */
71+ - async buildNodeModulesTreeManually(baseDir, aliasName) {
72+ + async buildNodeModulesTreeManually(baseDir, aliasName, workspaceDir) {
2073 // Track visited packages by their resolved path to prevent infinite loops
2174 const visited = new Set();
2275 const resolvedBaseDir = await this.cache.realPath[baseDir];
2376+
2477+ // JOPLIN CHANGE: Read the toplevel package.json file in the workspace:
25- + const rootPackageJson = JSON.parse(await fs.readFile(path.join(resolvedBaseDir, '..', '..' , 'package.json')));
78+ + const rootPackageJson = JSON.parse(await fs.readFile(path.join(workspaceDir , 'package.json')));
2679+
2780+ // Checks the "resolutions" field of the toplevel package.json
2881+ const handleResolutions = (depName, depVersion) => {
2982+ const resolutionKeys = [ depName, `${depName}@npm:${depVersion}` ];
3083+ for (const resolutionKey of resolutionKeys) {
3184+ if (rootPackageJson.resolutions && resolutionKey in rootPackageJson.resolutions) {
32- + const resolvedTo = decodeURIComponent(rootPackageJson.resolutions[resolutionKey].split('#')[0]);
85+ + // Resolutions can take the form "patch:package@npm%3A1.2.3#./.yarn/patches/package-npm-4.5.6-d92bace04d.patch"
86+ + // where the "%3A" is a URL-encoded ":"
87+ + const resolvedTo = rootPackageJson.resolutions[resolutionKey].split('#')[0];
88+ + const patchMatch = resolvedTo.match(/^patch:[^@]+@npm%3A(.*)$/);
3389+
34- + const patchMatch = resolvedTo.match(/^patch:[^@]+@npm:(.*)$/);
3590+ let resolution = null;
3691+ if (patchMatch) {
3792+ resolution = patchMatch[1];
@@ -40,7 +95,7 @@ index 851c5427b676c0c8e65b7b63bcf8eadef68904a3..38c6f85195683584b4bf32d784bc521c
4095+ }
4196+
4297+ if (resolution) {
43- + console .log(' Resolving', depName, 'to', resolution);
98+ + builder_util_1 .log.info(` Resolving ${ depName} to ${ resolution}...` );
4499+ return resolution;
45100+ }
46101+ }
@@ -51,32 +106,41 @@ index 851c5427b676c0c8e65b7b63bcf8eadef68904a3..38c6f85195683584b4bf32d784bc521c
51106 /**
52107 * Recursively builds dependency tree starting from a package directory.
53108 * @param packageDir - The directory of the package to process
54- @@ -82,7 +112,10 @@ class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCo
109+ @@ -82,7 +112,9 @@ class TraversalNodeModulesCollector extends nodeModulesCollector_1.NodeModulesCo
55110 visited.add(resolvedPackageDir);
56111 const buildPackage = async (dependencies, nullHandler) => {
57112 const builtPackages = {};
58113- for (const [depName, depVersion] of Object.entries(dependencies || {})) {
59114+ for (let [depName, depVersion] of Object.entries(dependencies || {})) {
60115+ depVersion = handleResolutions(depName, depVersion);
61- +
62116+
63117 const pkg = await this.locatePackageWithVersion({ name: depName, version: depVersion, path: resolvedPackageDir });
64118 const logFields = { parent: moduleName, dependency: depName, version: depVersion };
65119 if (pkg == null) {
66120diff --git a/out/util/appFileCopier.js b/out/util/appFileCopier.js
67- index f762b47b85539eea2b38f6cf76692624a3b7e307..f169d5a4d42259690b31974b6f0bb6f4336ff879 100644
121+ index f762b47b85539eea2b38f6cf76692624a3b7e307..5975683046464777bb296b26f1ed46ae0c87770d 100644
68122--- a/out/util/appFileCopier.js
69123+++ b/out/util/appFileCopier.js
70- @@ -176,7 +176,8 @@ async function collectNodeModulesWithLogging(platformPackager) {
124+ @@ -175,13 +175,16 @@ async function collectNodeModulesWithLogging(platformPackager) {
125+ const packager = platformPackager.info;
71126 const { tempDirManager, appDir, projectDir } = packager;
72127 let deps = undefined;
73- const searchDirectories = Array.from(new Set([appDir, projectDir, await packager.getWorkspaceRoot()])).filter((it) => (0, builder_util_1.isEmptyOrSpaces)(it) === false);
128+ - const searchDirectories = Array.from(new Set([appDir, projectDir, await packager.getWorkspaceRoot()])).filter((it) => (0, builder_util_1.isEmptyOrSpaces)(it) === false);
74129- const pmApproaches = [await packager.getPackageManager(), node_module_collector_1.PM.TRAVERSAL];
130+ + const workspaceDir = await packager.getWorkspaceRoot();
131+ + const searchDirectories = Array.from(new Set([appDir, projectDir, workspaceDir])).filter((it) => (0, builder_util_1.isEmptyOrSpaces)(it) === false);
75132+ // JOPLIN CHANGE: Always use the TRAVERSAL approach: The default yarn approach is unreliable:
76133+ const pmApproaches = [node_module_collector_1.PM.TRAVERSAL];
77134 for (const pm of pmApproaches) {
78135 for (const dir of searchDirectories) {
79136 builder_util_1.log.info({ pm, searchDir: dir }, "searching for node modules");
137+ const collector = (0, node_module_collector_1.getCollectorByPackageManager)(pm, dir, tempDirManager);
138+ - deps = await collector.getNodeModules({ packageName: packager.metadata.name });
139+ + // JOPLIN CHANGE: Always pass "workspaceDir":
140+ + deps = await collector.getNodeModules({ packageName: packager.metadata.name, workspaceDir });
141+ if (deps.nodeModules.length > 0) {
142+ break;
143+ }
80144diff --git a/templates/nsis/include/installUtil.nsh b/templates/nsis/include/installUtil.nsh
81145index 47367741632726ba0886ac516461dbe98b7aea58..e58d38b7b76276e39dd22864ce9754375e342a87 100644
82146--- a/templates/nsis/include/installUtil.nsh
0 commit comments