@lakutata/builder 3.0.0-beta.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/LICENSE +23 -0
- package/README.md +27 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/lib/Archive.js +21 -0
- package/dist/esm/lib/Bundle.js +162 -0
- package/dist/esm/lib/Executable.js +221 -0
- package/dist/esm/lib/Icons.js +372 -0
- package/dist/esm/lib/Installer.js +158 -0
- package/dist/esm/lib/Native.js +533 -0
- package/dist/esm/lib/NativeRuntime.js +177 -0
- package/dist/esm/lib/Platform.js +9 -0
- package/dist/esm/lib/Processes.js +42 -0
- package/dist/esm/lib/Scripts.js +161 -0
- package/dist/esm/lib/Tools.js +109 -0
- package/dist/esm/lib/electron/Electron.js +366 -0
- package/dist/esm/lib/linux/Linux.js +361 -0
- package/dist/esm/lib/mac/Mac.js +270 -0
- package/dist/esm/lib/win/Windows.js +525 -0
- package/dist/types/index.d.ts +13 -0
- package/dist/types/lib/Archive.d.ts +14 -0
- package/dist/types/lib/Bundle.d.ts +96 -0
- package/dist/types/lib/Executable.d.ts +50 -0
- package/dist/types/lib/Icons.d.ts +70 -0
- package/dist/types/lib/Installer.d.ts +242 -0
- package/dist/types/lib/Native.d.ts +196 -0
- package/dist/types/lib/NativeRuntime.d.ts +75 -0
- package/dist/types/lib/Platform.d.ts +4 -0
- package/dist/types/lib/Processes.d.ts +21 -0
- package/dist/types/lib/Scripts.d.ts +77 -0
- package/dist/types/lib/Tools.d.ts +32 -0
- package/dist/types/lib/electron/Electron.d.ts +13 -0
- package/dist/types/lib/linux/Linux.d.ts +29 -0
- package/dist/types/lib/mac/Mac.d.ts +19 -0
- package/dist/types/lib/win/Windows.d.ts +20 -0
- package/package.json +51 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { createRequire, isBuiltin } from 'node:module';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { gzipSync } from 'node:zlib';
|
|
7
|
+
/**
|
|
8
|
+
* The shared libraries: the addons, and the libraries they are linked with (sharp and libvips)
|
|
9
|
+
*/
|
|
10
|
+
const ADDON = /\.node$/;
|
|
11
|
+
const LIBRARY = /\.(node|dylib|dll|so)$|\.so\.\d+(\.\d+)*$/;
|
|
12
|
+
const RESOLVING = 'lakutataNativeResolving';
|
|
13
|
+
/**
|
|
14
|
+
* The files of the packages node never loads: their types, their source maps, their sources in TypeScript, their
|
|
15
|
+
* documentation
|
|
16
|
+
*/
|
|
17
|
+
const UNUSED = /\.(d\.[cm]?ts|[cm]?ts|map|md|markdown)$/i;
|
|
18
|
+
/**
|
|
19
|
+
* The JavaScript files, which a package keeps when they are loaded (traced)
|
|
20
|
+
*/
|
|
21
|
+
const SCRIPT = /\.[cm]?js$/;
|
|
22
|
+
/**
|
|
23
|
+
* A require esbuild cannot follow loading a file of its package by its path (require(path.join(__dirname, name)))
|
|
24
|
+
*/
|
|
25
|
+
const OWN_FILE = /__dirname|__filename|import\.meta|['"`]\.\.?\//;
|
|
26
|
+
/**
|
|
27
|
+
* The require of an ES module, createRequire(import.meta.url): in the bundle, it is the require of the bundle
|
|
28
|
+
*/
|
|
29
|
+
const MODULE_REQUIRE = /\b(?:[A-Za-z_$][\w$]*\.)?createRequire\s*\(\s*import\.meta\.url\s*\)/g;
|
|
30
|
+
/**
|
|
31
|
+
* The name of the package of a module specifier ("@scope/name/sub/path" → "@scope/name"), undefined for a path
|
|
32
|
+
* @param specifier
|
|
33
|
+
*/
|
|
34
|
+
export function specifierPackage(specifier) {
|
|
35
|
+
if (specifier.startsWith('.') || path.isAbsolute(specifier))
|
|
36
|
+
return undefined;
|
|
37
|
+
const parts = specifier.split('/');
|
|
38
|
+
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The files of a package directory, without the packages it holds (node_modules)
|
|
42
|
+
* @param directory
|
|
43
|
+
*/
|
|
44
|
+
function packageFiles(directory) {
|
|
45
|
+
const files = [];
|
|
46
|
+
const walk = (current) => {
|
|
47
|
+
for (const entry of readdirSync(current, { withFileTypes: true })) {
|
|
48
|
+
if (entry.name === 'node_modules')
|
|
49
|
+
continue;
|
|
50
|
+
const file = path.join(current, entry.name);
|
|
51
|
+
let stats;
|
|
52
|
+
try {
|
|
53
|
+
stats = statSync(file);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
//A broken link
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (stats.isDirectory())
|
|
60
|
+
walk(file);
|
|
61
|
+
else if (stats.isFile())
|
|
62
|
+
files.push(file);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
walk(directory);
|
|
66
|
+
return files;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The directories node looks a package up in from a directory of the tree, the nearest first
|
|
70
|
+
* @param directory
|
|
71
|
+
* @param name
|
|
72
|
+
*/
|
|
73
|
+
function lookupDirectories(directory, name) {
|
|
74
|
+
const parts = directory.split('/');
|
|
75
|
+
const lookups = [];
|
|
76
|
+
for (let index = parts.length; index >= 0; index--) {
|
|
77
|
+
const prefix = parts.slice(0, index);
|
|
78
|
+
if (prefix.at(-1) === 'node_modules')
|
|
79
|
+
continue;
|
|
80
|
+
lookups.push([...prefix, 'node_modules', name].join('/'));
|
|
81
|
+
}
|
|
82
|
+
return lookups;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Find the native addons of an application while it is bundled: the packages holding addons (and the packages loading
|
|
86
|
+
* them with computed paths, such as sharp) are left out of the bundle and gathered, with the packages they load, into
|
|
87
|
+
* a native tree; the addons of the application itself are loaded from the tree
|
|
88
|
+
*/
|
|
89
|
+
export class NativeCollector {
|
|
90
|
+
/**
|
|
91
|
+
* @param project the directory of the project, whose files are bundled
|
|
92
|
+
* @param runtime the runtime loading the tree: Bun resolves the packages with its condition (bun) first
|
|
93
|
+
*/
|
|
94
|
+
constructor(project, runtime = 'node') {
|
|
95
|
+
this.project = project;
|
|
96
|
+
this.runtime = runtime;
|
|
97
|
+
this.packages = new Map();
|
|
98
|
+
this.roots = new Map();
|
|
99
|
+
/**
|
|
100
|
+
* The packages left out of the bundle, by the name the application loads them with, and the files it loads
|
|
101
|
+
*/
|
|
102
|
+
this.externals = new Map();
|
|
103
|
+
/**
|
|
104
|
+
* The addons of the application itself, by their key in the tree
|
|
105
|
+
*/
|
|
106
|
+
this.addons = new Map();
|
|
107
|
+
/**
|
|
108
|
+
* The files of the packages of the tree which are loaded, by package (traced)
|
|
109
|
+
*/
|
|
110
|
+
this.loaded = new Map();
|
|
111
|
+
/**
|
|
112
|
+
* The packages of the tree kept as a whole: the ones which are not traced, the ones loading their files by paths
|
|
113
|
+
* esbuild cannot follow
|
|
114
|
+
*/
|
|
115
|
+
this.whole = new Set();
|
|
116
|
+
/**
|
|
117
|
+
* The packages each package of the tree requires, by their names (traced)
|
|
118
|
+
*/
|
|
119
|
+
this.required = new Map();
|
|
120
|
+
this.project = realpathSync(project);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* The directory of the package a file belongs to: the nearest directory with a named package.json
|
|
124
|
+
* @param file
|
|
125
|
+
*/
|
|
126
|
+
packageRoot(file) {
|
|
127
|
+
let directory = path.dirname(file);
|
|
128
|
+
const visited = [];
|
|
129
|
+
let found;
|
|
130
|
+
while (true) {
|
|
131
|
+
if (this.roots.has(directory)) {
|
|
132
|
+
found = this.roots.get(directory);
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
visited.push(directory);
|
|
136
|
+
const manifest = path.join(directory, 'package.json');
|
|
137
|
+
if (existsSync(manifest)) {
|
|
138
|
+
try {
|
|
139
|
+
if (JSON.parse(readFileSync(manifest, 'utf8')).name) {
|
|
140
|
+
found = directory;
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
//Not a package.json of a package
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const parent = path.dirname(directory);
|
|
149
|
+
if (parent === directory)
|
|
150
|
+
break;
|
|
151
|
+
directory = parent;
|
|
152
|
+
}
|
|
153
|
+
for (const entry of visited)
|
|
154
|
+
this.roots.set(entry, found);
|
|
155
|
+
return found;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A package: its manifest and whether it holds addons or shared libraries
|
|
159
|
+
* @param root
|
|
160
|
+
*/
|
|
161
|
+
package(root) {
|
|
162
|
+
if (!this.packages.has(root)) {
|
|
163
|
+
let info;
|
|
164
|
+
try {
|
|
165
|
+
const manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
166
|
+
const files = packageFiles(root);
|
|
167
|
+
info = {
|
|
168
|
+
root: root,
|
|
169
|
+
name: manifest.name,
|
|
170
|
+
manifest: manifest,
|
|
171
|
+
addons: files.some((file) => ADDON.test(file)),
|
|
172
|
+
libraries: files.some((file) => LIBRARY.test(file))
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
info = undefined;
|
|
177
|
+
}
|
|
178
|
+
this.packages.set(root, info);
|
|
179
|
+
}
|
|
180
|
+
return this.packages.get(root);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The dependencies of a package, as node finds them from it: the path looked up (a link with pnpm) and the package
|
|
184
|
+
* @param root
|
|
185
|
+
*/
|
|
186
|
+
dependencies(root) {
|
|
187
|
+
const manifest = this.package(root)?.manifest ?? {};
|
|
188
|
+
const names = new Set(Object.keys({ ...manifest.dependencies, ...manifest.optionalDependencies, ...manifest.peerDependencies }));
|
|
189
|
+
const found = [];
|
|
190
|
+
for (const name of names) {
|
|
191
|
+
let directory = root;
|
|
192
|
+
while (true) {
|
|
193
|
+
const lookup = path.join(directory, 'node_modules', name);
|
|
194
|
+
if (existsSync(path.join(lookup, 'package.json'))) {
|
|
195
|
+
found.push({ name: name, lookup: lookup, root: realpathSync(lookup) });
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const parent = path.dirname(directory);
|
|
199
|
+
if (parent === directory)
|
|
200
|
+
break;
|
|
201
|
+
directory = parent;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return found;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Whether a package is left out of the bundle: it holds addons, or one of its dependencies does (it loads them,
|
|
208
|
+
* with computed paths often), and it is not the project
|
|
209
|
+
* @param root
|
|
210
|
+
*/
|
|
211
|
+
isNative(root) {
|
|
212
|
+
if (!root || root === this.project)
|
|
213
|
+
return false;
|
|
214
|
+
const info = this.package(root);
|
|
215
|
+
if (!info)
|
|
216
|
+
return false;
|
|
217
|
+
return info.addons || this.dependencies(root).some(({ root: dependency }) => !!this.package(dependency)?.addons);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Resolve an import as esbuild does
|
|
221
|
+
* @param build
|
|
222
|
+
* @param args
|
|
223
|
+
*/
|
|
224
|
+
async resolve(build, args) {
|
|
225
|
+
const result = await build.resolve(args.path, { kind: args.kind, resolveDir: args.resolveDir, importer: args.importer, pluginData: { [RESOLVING]: true } });
|
|
226
|
+
return result.errors.length || result.external || !result.path ? undefined : realpathSync(result.path);
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* The plugin of the bundle: the native packages are external, the addons of the application are loaded from the tree
|
|
230
|
+
*/
|
|
231
|
+
plugin() {
|
|
232
|
+
return {
|
|
233
|
+
name: 'lakutata-native',
|
|
234
|
+
setup: (build) => {
|
|
235
|
+
build.onResolve({ filter: /.*/ }, async (args) => {
|
|
236
|
+
if (args.pluginData?.[RESOLVING] || isBuiltin(args.path) || args.kind === 'entry-point')
|
|
237
|
+
return undefined;
|
|
238
|
+
const file = await this.resolve(build, args);
|
|
239
|
+
if (!file)
|
|
240
|
+
return undefined;
|
|
241
|
+
const root = this.packageRoot(file);
|
|
242
|
+
if (this.isNative(root)) {
|
|
243
|
+
const name = specifierPackage(args.path);
|
|
244
|
+
if (!name)
|
|
245
|
+
throw new Error(`${args.importer} loads ${args.path} of the native package ${this.package(root).name} by its path: load it by its name`);
|
|
246
|
+
const external = this.externals.get(name) ?? { root: root, entries: new Set() };
|
|
247
|
+
external.entries.add(file);
|
|
248
|
+
//The bundle is CommonJS: the require of the run loads the file of the require condition
|
|
249
|
+
if (args.kind !== 'require-call') {
|
|
250
|
+
const required = await this.resolve(build, { ...args, kind: 'require-call' });
|
|
251
|
+
if (required)
|
|
252
|
+
external.entries.add(required);
|
|
253
|
+
}
|
|
254
|
+
this.externals.set(name, external);
|
|
255
|
+
return { path: args.path, external: true };
|
|
256
|
+
}
|
|
257
|
+
if (ADDON.test(file)) {
|
|
258
|
+
const key = `.lakutata/addons/${createHash('sha256').update(file).digest('hex').slice(0, 16)}-${path.basename(file)}`;
|
|
259
|
+
this.addons.set(key, file);
|
|
260
|
+
return { path: key, namespace: 'lakutata-native' };
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
});
|
|
264
|
+
//The requires of the ES modules are the one of the bundle: esbuild follows what they load (the addons)
|
|
265
|
+
build.onLoad({ filter: /\.(c|m)?js$/ }, async (args) => {
|
|
266
|
+
const source = await readFile(args.path, 'utf8');
|
|
267
|
+
if (!MODULE_REQUIRE.test(source))
|
|
268
|
+
return undefined;
|
|
269
|
+
MODULE_REQUIRE.lastIndex = 0;
|
|
270
|
+
return { contents: source.replace(MODULE_REQUIRE, 'require'), loader: 'js', resolveDir: path.dirname(args.path) };
|
|
271
|
+
});
|
|
272
|
+
//The addons of the application are loaded by the require of the bundle, from the extracted tree
|
|
273
|
+
build.onLoad({ filter: /.*/, namespace: 'lakutata-native' }, (args) => ({
|
|
274
|
+
contents: `module.exports = require.lakutataNative(${JSON.stringify(args.path)})`,
|
|
275
|
+
loader: 'js'
|
|
276
|
+
}));
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The packages a native package loads at run time: the JavaScript it requires (followed by esbuild), the packages of
|
|
282
|
+
* shared libraries it depends on (the libraries linked with its addons), and the native packages it loads
|
|
283
|
+
* @param build the build function of esbuild
|
|
284
|
+
* @param root
|
|
285
|
+
* @param entries the files of the package which are loaded
|
|
286
|
+
* @param included the packages of the tree
|
|
287
|
+
*/
|
|
288
|
+
async trace(build, root, entries, included) {
|
|
289
|
+
const pending = [{ root: root, entries: entries }];
|
|
290
|
+
const traced = new Set();
|
|
291
|
+
while (pending.length) {
|
|
292
|
+
const current = pending.shift();
|
|
293
|
+
const key = `${current.root}\0${[...current.entries].sort().join('\0')}`;
|
|
294
|
+
if (traced.has(key))
|
|
295
|
+
continue;
|
|
296
|
+
traced.add(key);
|
|
297
|
+
included.add(current.root);
|
|
298
|
+
//A package esbuild cannot trace (a glob of its requires matching nothing) is kept as a whole
|
|
299
|
+
const result = await build({
|
|
300
|
+
entryPoints: [...current.entries],
|
|
301
|
+
bundle: true,
|
|
302
|
+
write: false,
|
|
303
|
+
//Not written: a package loaded by several of its files is several entries, which need a directory
|
|
304
|
+
outdir: path.join(current.root, '.lakutata-trace'),
|
|
305
|
+
metafile: true,
|
|
306
|
+
platform: 'node',
|
|
307
|
+
format: 'cjs',
|
|
308
|
+
conditions: this.runtime === 'bun' ? ['bun'] : [],
|
|
309
|
+
logLevel: 'silent',
|
|
310
|
+
//The addons are loaded at run time (the paths of their globs may match nothing: sharp built from its sources)
|
|
311
|
+
external: ['*.node'],
|
|
312
|
+
//Reported in the node_modules too: the requires it cannot follow
|
|
313
|
+
logOverride: { 'unsupported-require-call': 'warning' },
|
|
314
|
+
plugins: [{
|
|
315
|
+
name: 'lakutata-native-trace',
|
|
316
|
+
setup: (trace) => {
|
|
317
|
+
trace.onResolve({ filter: /.*/ }, async (args) => {
|
|
318
|
+
if (args.pluginData?.[RESOLVING] || isBuiltin(args.path) || args.kind === 'entry-point')
|
|
319
|
+
return undefined;
|
|
320
|
+
const file = await this.resolve(trace, args);
|
|
321
|
+
//The requires which cannot be followed (computed paths) are left to the run time
|
|
322
|
+
if (!file || ADDON.test(file))
|
|
323
|
+
return { path: args.path, external: true };
|
|
324
|
+
const dependency = this.packageRoot(file);
|
|
325
|
+
//The package it requires by this name (the ones it does not declare too)
|
|
326
|
+
const name = specifierPackage(args.path);
|
|
327
|
+
const importer = args.importer ? this.packageRoot(realpathSync(args.importer)) : undefined;
|
|
328
|
+
if (name && importer && dependency && importer !== dependency)
|
|
329
|
+
this.requires(importer).set(name, dependency);
|
|
330
|
+
if (dependency !== current.root && this.isNative(dependency)) {
|
|
331
|
+
pending.push({ root: dependency, entries: new Set([file]) });
|
|
332
|
+
return { path: args.path, external: true };
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}]
|
|
338
|
+
}).catch(() => undefined);
|
|
339
|
+
if (!result)
|
|
340
|
+
this.whole.add(current.root);
|
|
341
|
+
for (const input of Object.keys(result?.metafile.inputs ?? {})) {
|
|
342
|
+
const file = realpathSync(path.resolve(input));
|
|
343
|
+
const dependency = this.packageRoot(file);
|
|
344
|
+
if (!dependency || dependency === this.project)
|
|
345
|
+
continue;
|
|
346
|
+
included.add(dependency);
|
|
347
|
+
const files = this.loaded.get(dependency) ?? new Set();
|
|
348
|
+
this.loaded.set(dependency, files.add(file));
|
|
349
|
+
}
|
|
350
|
+
//The packages loading their files by paths esbuild cannot follow are kept as a whole
|
|
351
|
+
for (const warning of result?.warnings ?? []) {
|
|
352
|
+
if (!warning.location || !OWN_FILE.test(warning.location.lineText.slice(warning.location.column, warning.location.column + 200)))
|
|
353
|
+
continue;
|
|
354
|
+
const dependency = this.packageRoot(realpathSync(path.resolve(warning.location.file)));
|
|
355
|
+
if (dependency)
|
|
356
|
+
this.whole.add(dependency);
|
|
357
|
+
}
|
|
358
|
+
//The packages it declares which its code names: the ones it loads by requires esbuild does not follow
|
|
359
|
+
//(createRequire(import.meta.url)('node-gyp-build')), not the ones installing it (node-gyp, prebuild-install)
|
|
360
|
+
const sources = (result ? Object.keys(result.metafile.inputs).map((input) => realpathSync(path.resolve(input))) : packageFiles(current.root))
|
|
361
|
+
.filter((file) => this.packageRoot(file) === current.root && /\.[cm]?[jt]s$/.test(file))
|
|
362
|
+
.map((file) => readFileSync(file, 'utf8'))
|
|
363
|
+
.join('\n');
|
|
364
|
+
for (const dependency of this.dependencies(current.root)) {
|
|
365
|
+
if (dependency.root === this.project || included.has(dependency.root))
|
|
366
|
+
continue;
|
|
367
|
+
if (!new RegExp(`(['"\`])${dependency.name.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')}\\1`).test(sources))
|
|
368
|
+
continue;
|
|
369
|
+
let entry;
|
|
370
|
+
try {
|
|
371
|
+
entry = createRequire(path.join(current.root, 'package.json')).resolve(dependency.name);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
//A package without a CommonJS entry: kept as a whole
|
|
375
|
+
}
|
|
376
|
+
if (entry && !isBuiltin(entry))
|
|
377
|
+
pending.push({ root: dependency.root, entries: new Set([realpathSync(entry)]) });
|
|
378
|
+
else {
|
|
379
|
+
included.add(dependency.root);
|
|
380
|
+
this.whole.add(dependency.root);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
//The packages of the shared libraries (the addons of the platform packages, the libraries they are linked with)
|
|
385
|
+
const libraries = [...included];
|
|
386
|
+
while (libraries.length) {
|
|
387
|
+
for (const { root: dependency } of this.dependencies(libraries.pop())) {
|
|
388
|
+
if (!included.has(dependency) && this.package(dependency)?.libraries) {
|
|
389
|
+
included.add(dependency);
|
|
390
|
+
libraries.push(dependency);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* The packages a package requires, by their names
|
|
397
|
+
* @param root
|
|
398
|
+
*/
|
|
399
|
+
requires(root) {
|
|
400
|
+
const required = this.required.get(root) ?? new Map();
|
|
401
|
+
this.required.set(root, required);
|
|
402
|
+
return required;
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* The layout of the packages of the tree, without links (as npm installs them, the tree is moved, copied, archived
|
|
406
|
+
* and installed as it is): the packages the application loads in node_modules, the packages each package requires
|
|
407
|
+
* (traced) or declares where node finds them from it: shared when node finds the same package, at the top when its
|
|
408
|
+
* name is free there, else in the node_modules of the package
|
|
409
|
+
* @param included the packages of the tree
|
|
410
|
+
* @return the packages by their directories in the tree
|
|
411
|
+
*/
|
|
412
|
+
layout(included) {
|
|
413
|
+
const placed = new Map();
|
|
414
|
+
const queue = [];
|
|
415
|
+
const place = (directory, root) => queue.push(placed.set(directory, root) && directory);
|
|
416
|
+
const drain = () => {
|
|
417
|
+
while (queue.length) {
|
|
418
|
+
const directory = queue.shift();
|
|
419
|
+
const root = placed.get(directory);
|
|
420
|
+
const needed = new Map(this.required.get(root));
|
|
421
|
+
for (const dependency of this.dependencies(root)) {
|
|
422
|
+
if (included.has(dependency.root) && !needed.has(dependency.name))
|
|
423
|
+
needed.set(dependency.name, dependency.root);
|
|
424
|
+
}
|
|
425
|
+
for (const [name, dependency] of [...needed].sort(([a], [b]) => a.localeCompare(b))) {
|
|
426
|
+
if (dependency === root || !included.has(dependency))
|
|
427
|
+
continue;
|
|
428
|
+
const found = lookupDirectories(directory, name).find((lookup) => placed.has(lookup));
|
|
429
|
+
if (found !== undefined && placed.get(found) === dependency)
|
|
430
|
+
continue;
|
|
431
|
+
place(found === undefined ? `node_modules/${name}` : `${directory}/node_modules/${name}`, dependency);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
for (const [name, { root }] of [...this.externals].sort(([a], [b]) => a.localeCompare(b)))
|
|
436
|
+
place(`node_modules/${name}`, root);
|
|
437
|
+
drain();
|
|
438
|
+
//The packages reached otherwise (by the libraries of a package): at the top
|
|
439
|
+
const reached = new Set(placed.values());
|
|
440
|
+
for (const root of [...included].sort()) {
|
|
441
|
+
const name = this.package(root)?.name;
|
|
442
|
+
if (reached.has(root) || !name || placed.has(`node_modules/${name}`))
|
|
443
|
+
continue;
|
|
444
|
+
place(`node_modules/${name}`, root);
|
|
445
|
+
}
|
|
446
|
+
drain();
|
|
447
|
+
return placed;
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* The files of a package of the tree which are loaded at run time, without the ones node never loads (UNUSED): the
|
|
451
|
+
* packages of shared libraries (which find their addons by computed paths), and the ones kept as a whole, have all
|
|
452
|
+
* their files; the others have the JavaScript files they load (traced) and all their other files (the data they read)
|
|
453
|
+
* @param root
|
|
454
|
+
*/
|
|
455
|
+
packageFiles(root) {
|
|
456
|
+
const files = packageFiles(root).filter((file) => !UNUSED.test(file));
|
|
457
|
+
const loaded = this.loaded.get(root);
|
|
458
|
+
if (!loaded || this.whole.has(root) || this.package(root)?.libraries)
|
|
459
|
+
return files;
|
|
460
|
+
return files.filter((file) => !SCRIPT.test(file) || loaded.has(file));
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The native tree of the application, once it is bundled; undefined when it has no native addon
|
|
464
|
+
*/
|
|
465
|
+
async collect() {
|
|
466
|
+
if (!this.externals.size && !this.addons.size)
|
|
467
|
+
return undefined;
|
|
468
|
+
const { build } = await import('esbuild');
|
|
469
|
+
const included = new Set();
|
|
470
|
+
for (const { root, entries } of this.externals.values())
|
|
471
|
+
await this.trace(build, root, entries, included);
|
|
472
|
+
const files = [];
|
|
473
|
+
for (const [directory, root] of [...this.layout(included)].sort(([a], [b]) => a.localeCompare(b))) {
|
|
474
|
+
for (const file of this.packageFiles(root))
|
|
475
|
+
files.push({ path: `${directory}/${path.relative(root, file).split(path.sep).join('/')}`, source: file, mode: statSync(file).mode & 0o777 });
|
|
476
|
+
}
|
|
477
|
+
for (const [key, file] of this.addons)
|
|
478
|
+
files.push({ path: key, source: file, mode: statSync(file).mode & 0o777 });
|
|
479
|
+
return { modules: [...this.externals.keys()].sort(), files: files, links: [], hash: hashNativeTree(files, []) };
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* The hash of a native tree, which names the directory it is extracted into: its files and their content, its links
|
|
484
|
+
* @param files
|
|
485
|
+
* @param links
|
|
486
|
+
*/
|
|
487
|
+
export function hashNativeTree(files, links) {
|
|
488
|
+
const hash = createHash('sha256');
|
|
489
|
+
for (const file of files)
|
|
490
|
+
hash.update(`${file.path}\0${file.mode}\0`).update(readFileSync(file.source));
|
|
491
|
+
for (const link of links)
|
|
492
|
+
hash.update(`${link.path}\0${link.target}\0`);
|
|
493
|
+
return hash.digest('hex').slice(0, 16);
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Write a native tree into a directory (next to a bundle)
|
|
497
|
+
* @param tree
|
|
498
|
+
* @param directory
|
|
499
|
+
*/
|
|
500
|
+
export async function writeNativeTree(tree, directory) {
|
|
501
|
+
await rm(directory, { recursive: true, force: true });
|
|
502
|
+
for (const file of tree.files) {
|
|
503
|
+
const target = path.join(directory, ...file.path.split('/'));
|
|
504
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
505
|
+
await writeFile(target, readFileSync(file.source), { mode: file.mode });
|
|
506
|
+
}
|
|
507
|
+
for (const link of tree.links) {
|
|
508
|
+
const at = path.join(directory, ...link.path.split('/'));
|
|
509
|
+
const target = path.join(directory, ...link.target.split('/'));
|
|
510
|
+
await mkdir(path.dirname(at), { recursive: true });
|
|
511
|
+
//The junctions of Windows need no privilege
|
|
512
|
+
if (process.platform === 'win32')
|
|
513
|
+
await symlink(target, at, 'junction');
|
|
514
|
+
else
|
|
515
|
+
await symlink(path.relative(path.dirname(at), target), at, 'dir');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Pack a native tree into the assets of an executable: its gzipped files and their index
|
|
520
|
+
* @param tree
|
|
521
|
+
*/
|
|
522
|
+
export function packNativeTree(tree) {
|
|
523
|
+
const chunks = [];
|
|
524
|
+
const files = [];
|
|
525
|
+
let offset = 0;
|
|
526
|
+
for (const file of tree.files) {
|
|
527
|
+
const content = readFileSync(file.source);
|
|
528
|
+
files.push({ path: file.path, offset: offset, size: content.length, mode: file.mode });
|
|
529
|
+
chunks.push(content);
|
|
530
|
+
offset += content.length;
|
|
531
|
+
}
|
|
532
|
+
return { index: { hash: tree.hash, files: files, links: tree.links }, data: gzipSync(Buffer.concat(chunks)) };
|
|
533
|
+
}
|