@lakutata/builder 3.0.0-beta.2 → 3.0.0-beta.4
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 +4 -1
- package/dist/esm/lib/Bundle.js +76 -34
- package/dist/esm/lib/Executable.js +2 -1
- package/dist/esm/lib/Native.js +28 -9
- package/dist/esm/lib/NativeRuntime.js +29 -2
- package/dist/esm/lib/Workers.js +476 -0
- package/dist/esm/lib/electron/Electron.js +2 -0
- package/dist/types/lib/Bundle.d.ts +8 -0
- package/dist/types/lib/Executable.d.ts +4 -0
- package/dist/types/lib/Native.d.ts +6 -2
- package/dist/types/lib/NativeRuntime.d.ts +5 -0
- package/dist/types/lib/Workers.d.ts +96 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -16,7 +16,10 @@ await buildInstaller({project: process.cwd()})
|
|
|
16
16
|
```
|
|
17
17
|
|
|
18
18
|
The application is compiled first (`tsc`, which emits the metadata of the decorators). The bundle is one CommonJS file
|
|
19
|
-
(esbuild); the native addons, which cannot be bundled, are gathered with what they load at run time into a native tree
|
|
19
|
+
(esbuild); the native addons, which cannot be bundled, are gathered with what they load at run time into a native tree,
|
|
20
|
+
with the scripts run on their own by their paths, which the bundler does not follow (the workers, the pools of workers,
|
|
21
|
+
`fork()`, `require(path.join(__dirname, 'x.js'))`: the paths given from their modules are found, the scripts bundled,
|
|
22
|
+
the paths rewritten; the workers whose scripts cannot be followed are in the `warnings` of the result);
|
|
20
23
|
the resources of the application (the `lakutata.resources` of its `package.json`) are found at run time by
|
|
21
24
|
`resourcesPath()` of lakutata. The installers are configured by the `lakutata.installer` of the `package.json`. See the
|
|
22
25
|
documentation of `lakutata package bundle`, `lakutata package exe` and `lakutata package installer` in `@lakutata/cli`.
|
package/dist/esm/lib/Bundle.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { chmod, cp, mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { hashNativeTree, NativeCollector, writeNativeTree } from './Native.js';
|
|
5
5
|
import { EMBEDDED_RESOURCES, nativeBanner, packagedBanner } from './NativeRuntime.js';
|
|
6
|
+
import { WORKERS_DIRECTORY, WorkerCollector } from './Workers.js';
|
|
6
7
|
/**
|
|
7
8
|
* The name of the program of a package: its first bin, or the name of the package without its scope
|
|
8
9
|
* @param manifest
|
|
@@ -85,43 +86,77 @@ export async function bundleProject(options) {
|
|
|
85
86
|
throw new Error(`The entry ${path.relative(process.cwd(), entry) || entry} does not exist: build the project first`);
|
|
86
87
|
const outfile = path.resolve(project, options.outfile ?? path.join('dist', `${name}.cjs`));
|
|
87
88
|
const collector = new NativeCollector(project, options.runtime);
|
|
89
|
+
const workers = new WorkerCollector(project);
|
|
88
90
|
//esbuild is loaded by the commands which need it
|
|
89
91
|
const { build } = await import('esbuild');
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
92
|
+
//The application, and each script of its workers (the bundler does not follow new Worker()): their native packages
|
|
93
|
+
//gathered into one tree, the workers they start found too
|
|
94
|
+
const bundle = async (input, output) => {
|
|
95
|
+
const result = await build({
|
|
96
|
+
entryPoints: [input],
|
|
97
|
+
outfile: output,
|
|
98
|
+
write: false,
|
|
99
|
+
metafile: true,
|
|
100
|
+
bundle: true,
|
|
101
|
+
platform: 'node',
|
|
102
|
+
target: `node${process.versions.node.split('.')[0]}`,
|
|
103
|
+
format: 'cjs',
|
|
104
|
+
minify: options.minify ?? true,
|
|
105
|
+
//The names of the classes are the ones of the objects of the container and of the errors
|
|
106
|
+
keepNames: true,
|
|
107
|
+
external: options.external ?? [],
|
|
108
|
+
//The dynamic imports of the modules left out are requires (the require of the bundle loads the native ones)
|
|
109
|
+
supported: { 'dynamic-import': false },
|
|
110
|
+
banner: { js: `var ${IMPORT_META_URL}=require('node:url').pathToFileURL(__filename).href;` },
|
|
111
|
+
define: {
|
|
112
|
+
'import.meta.url': IMPORT_META_URL,
|
|
113
|
+
//Bun replaces process.env.NODE_ENV with the mode of its build: the one of the run is read
|
|
114
|
+
...options.runtime === 'bun' ? { 'process.env.NODE_ENV': 'globalThis.process.env.NODE_ENV' } : {}
|
|
115
|
+
},
|
|
116
|
+
plugins: [collector.plugin([(source, file) => workers.transform(source, file)])],
|
|
117
|
+
logLevel: 'silent'
|
|
118
|
+
}).catch((error) => {
|
|
119
|
+
const messages = (error.errors ?? []).map(({ text, location }) => location ? `${location.file}:${location.line}: ${text}` : text);
|
|
120
|
+
//The bundles are CommonJS, which the executables of Node.js run
|
|
121
|
+
const hint = messages.some((message) => message.includes('Top-level await')) ? '\nThe bundles are CommonJS modules, without top-level await: await in an async function' : '';
|
|
122
|
+
throw new Error(`The bundle failed:\n ${messages.length ? messages.join('\n ') : error.message}${hint}`);
|
|
123
|
+
});
|
|
124
|
+
//The files bundled (their real paths; the modules of the plugins are in their namespaces)
|
|
125
|
+
const inputs = new Set(Object.keys(result.metafile.inputs).filter((input) => !/^[a-z-]+:/.test(input)).map((input) => {
|
|
126
|
+
const absolute = path.resolve(input);
|
|
127
|
+
return existsSync(absolute) ? realpathSync(absolute) : absolute;
|
|
128
|
+
}));
|
|
129
|
+
return { code: result.outputFiles[0].text, inputs: inputs };
|
|
130
|
+
};
|
|
131
|
+
const application = await bundle(entry, outfile);
|
|
132
|
+
let code = application.code;
|
|
122
133
|
const base = path.basename(outfile, path.extname(outfile));
|
|
123
134
|
const nativeDirectory = path.resolve(path.dirname(outfile), options.directories?.native ?? `${base}.native`);
|
|
124
135
|
const resourcesDirectory = path.resolve(path.dirname(outfile), options.directories?.resources ?? `${base}.resources`);
|
|
136
|
+
//The scripts of the workers, until the ones they start are bundled too
|
|
137
|
+
const scripts = new Map();
|
|
138
|
+
for (let pending = [...workers.scripts]; pending.length; pending = [...workers.scripts].filter(([at]) => !scripts.has(at))) {
|
|
139
|
+
for (const [at, script] of pending)
|
|
140
|
+
scripts.set(at, (await bundle(script, path.join(nativeDirectory, ...at.split('/')))).code);
|
|
141
|
+
}
|
|
142
|
+
workers.warnBundled(application.inputs);
|
|
143
|
+
const embedded = options.native === 'embedded';
|
|
144
|
+
let native = await collector.collect();
|
|
145
|
+
const resources = resourceFiles(project, manifest);
|
|
146
|
+
if (scripts.size) {
|
|
147
|
+
//The scripts run from the tree: their require loads the native packages from it, their resources are the ones of
|
|
148
|
+
//the application (an executable gives them from the tree it extracted)
|
|
149
|
+
const modules = native?.modules ?? [];
|
|
150
|
+
const files = [{ path: `${WORKERS_DIRECTORY}/package.json`, source: manifestFile, mode: 0o644, content: Buffer.from('{"type": "commonjs"}\n') }];
|
|
151
|
+
for (const [at, script] of [...scripts].sort(([a], [b]) => a.localeCompare(b))) {
|
|
152
|
+
const directory = path.dirname(path.join(nativeDirectory, ...at.split('/')));
|
|
153
|
+
const banner = packagedBanner(path.relative(directory, resourcesDirectory).split(path.sep).join('/')) +
|
|
154
|
+
nativeBanner({ name: name, hash: '', directory: '', modules: modules, embedded: embedded, root: path.relative(directory, nativeDirectory).split(path.sep).join('/') });
|
|
155
|
+
files.push({ path: at, source: workers.scripts.get(at), mode: 0o644, content: Buffer.from(prepend(script, banner)) });
|
|
156
|
+
}
|
|
157
|
+
const all = [...native?.files ?? [], ...files];
|
|
158
|
+
native = { modules: modules, files: all, links: native?.links ?? [], hash: hashNativeTree(all, native?.links ?? []) };
|
|
159
|
+
}
|
|
125
160
|
await rm(nativeDirectory, { recursive: true, force: true });
|
|
126
161
|
await rm(resourcesDirectory, { recursive: true, force: true });
|
|
127
162
|
//The resources of an executable are extracted with its native tree
|
|
@@ -129,6 +164,11 @@ export async function bundleProject(options) {
|
|
|
129
164
|
const files = [...native?.files ?? [], ...resources.files.map((file) => ({ ...file, path: `${EMBEDDED_RESOURCES}/${file.path}` }))];
|
|
130
165
|
native = { modules: native?.modules ?? [], files: files, links: native?.links ?? [], hash: hashNativeTree(files, native?.links ?? []) };
|
|
131
166
|
}
|
|
167
|
+
if (native && embedded) {
|
|
168
|
+
//An executable runs the scripts of its tree it is started for (fork()) instead of the application
|
|
169
|
+
const hashbang = /^#![^\n]*\n/.exec(code)?.[0] ?? '';
|
|
170
|
+
code = `${hashbang}if (!require.lakutataScript()) {\n${code.slice(hashbang.length)}\n}\n`;
|
|
171
|
+
}
|
|
132
172
|
if (native) {
|
|
133
173
|
//The require loading the native packages is the first code, after the hashbang
|
|
134
174
|
code = prepend(code, nativeBanner({ name: name, hash: native.hash, directory: path.basename(nativeDirectory), modules: native.modules, embedded: embedded }));
|
|
@@ -157,6 +197,8 @@ export async function bundleProject(options) {
|
|
|
157
197
|
native: native,
|
|
158
198
|
nativeDirectory: native && !embedded ? nativeDirectory : undefined,
|
|
159
199
|
resources: resources.declared,
|
|
160
|
-
resourcesDirectory: resources.files.length && !embedded ? resourcesDirectory : undefined
|
|
200
|
+
resourcesDirectory: resources.files.length && !embedded ? resourcesDirectory : undefined,
|
|
201
|
+
workers: [...new Set([...scripts.keys()].map((at) => workers.nameOf(workers.scripts.get(at))))].sort(),
|
|
202
|
+
warnings: workers.warnings
|
|
161
203
|
};
|
|
162
204
|
}
|
|
@@ -196,7 +196,7 @@ export async function buildExecutable(options) {
|
|
|
196
196
|
//ignores the case), whose signature it writes beside the executable (_CodeSignature)
|
|
197
197
|
const made = path.join(work, 'executable', path.basename(file));
|
|
198
198
|
await mkdir(path.dirname(made), { recursive: true });
|
|
199
|
-
const warnings = [];
|
|
199
|
+
const warnings = [...bundle.warnings];
|
|
200
200
|
//The icon of the application: the one of the executables of Windows (the ones of macOS and Linux have none)
|
|
201
201
|
const application = applicationIcon(project, manifest);
|
|
202
202
|
const icon = application && process.platform === 'win32' ? icoFile(application.image) : undefined;
|
|
@@ -209,6 +209,7 @@ export async function buildExecutable(options) {
|
|
|
209
209
|
platform: `${process.platform === 'win32' ? 'win' : process.platform}-${process.arch}`,
|
|
210
210
|
size: (await stat(file)).size,
|
|
211
211
|
native: bundle.native?.modules ?? [],
|
|
212
|
+
workers: bundle.workers,
|
|
212
213
|
resources: bundle.resources,
|
|
213
214
|
directory: directory ? target : undefined,
|
|
214
215
|
runtime: runtime,
|
package/dist/esm/lib/Native.js
CHANGED
|
@@ -4,6 +4,13 @@ import { mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
|
|
|
4
4
|
import { createRequire, isBuiltin } from 'node:module';
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { gzipSync } from 'node:zlib';
|
|
7
|
+
/**
|
|
8
|
+
* The content of a file of the native tree
|
|
9
|
+
* @param file
|
|
10
|
+
*/
|
|
11
|
+
function contentOf(file) {
|
|
12
|
+
return file.content ?? readFileSync(file.source);
|
|
13
|
+
}
|
|
7
14
|
/**
|
|
8
15
|
* The shared libraries: the addons, and the libraries they are linked with (sharp and libvips)
|
|
9
16
|
*/
|
|
@@ -227,8 +234,10 @@ export class NativeCollector {
|
|
|
227
234
|
}
|
|
228
235
|
/**
|
|
229
236
|
* The plugin of the bundle: the native packages are external, the addons of the application are loaded from the tree
|
|
237
|
+
* @param transforms the transformations of the sources of the modules (the paths of the workers), giving their new
|
|
238
|
+
* source or undefined
|
|
230
239
|
*/
|
|
231
|
-
plugin() {
|
|
240
|
+
plugin(transforms = []) {
|
|
232
241
|
return {
|
|
233
242
|
name: 'lakutata-native',
|
|
234
243
|
setup: (build) => {
|
|
@@ -263,11 +272,21 @@ export class NativeCollector {
|
|
|
263
272
|
});
|
|
264
273
|
//The requires of the ES modules are the one of the bundle: esbuild follows what they load (the addons)
|
|
265
274
|
build.onLoad({ filter: /\.(c|m)?js$/ }, async (args) => {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
275
|
+
let source = await readFile(args.path, 'utf8');
|
|
276
|
+
let changed = false;
|
|
277
|
+
if (MODULE_REQUIRE.test(source)) {
|
|
278
|
+
MODULE_REQUIRE.lastIndex = 0;
|
|
279
|
+
source = source.replace(MODULE_REQUIRE, 'require');
|
|
280
|
+
changed = true;
|
|
281
|
+
}
|
|
282
|
+
for (const transform of transforms) {
|
|
283
|
+
const transformed = transform(source, args.path);
|
|
284
|
+
if (transformed === undefined)
|
|
285
|
+
continue;
|
|
286
|
+
source = transformed;
|
|
287
|
+
changed = true;
|
|
288
|
+
}
|
|
289
|
+
return changed ? { contents: source, loader: 'js', resolveDir: path.dirname(args.path) } : undefined;
|
|
271
290
|
});
|
|
272
291
|
//The addons of the application are loaded by the require of the bundle, from the extracted tree
|
|
273
292
|
build.onLoad({ filter: /.*/, namespace: 'lakutata-native' }, (args) => ({
|
|
@@ -487,7 +506,7 @@ export class NativeCollector {
|
|
|
487
506
|
export function hashNativeTree(files, links) {
|
|
488
507
|
const hash = createHash('sha256');
|
|
489
508
|
for (const file of files)
|
|
490
|
-
hash.update(`${file.path}\0${file.mode}\0`).update(
|
|
509
|
+
hash.update(`${file.path}\0${file.mode}\0`).update(contentOf(file));
|
|
491
510
|
for (const link of links)
|
|
492
511
|
hash.update(`${link.path}\0${link.target}\0`);
|
|
493
512
|
return hash.digest('hex').slice(0, 16);
|
|
@@ -502,7 +521,7 @@ export async function writeNativeTree(tree, directory) {
|
|
|
502
521
|
for (const file of tree.files) {
|
|
503
522
|
const target = path.join(directory, ...file.path.split('/'));
|
|
504
523
|
await mkdir(path.dirname(target), { recursive: true });
|
|
505
|
-
await writeFile(target,
|
|
524
|
+
await writeFile(target, contentOf(file), { mode: file.mode });
|
|
506
525
|
}
|
|
507
526
|
for (const link of tree.links) {
|
|
508
527
|
const at = path.join(directory, ...link.path.split('/'));
|
|
@@ -524,7 +543,7 @@ export function packNativeTree(tree) {
|
|
|
524
543
|
const files = [];
|
|
525
544
|
let offset = 0;
|
|
526
545
|
for (const file of tree.files) {
|
|
527
|
-
const content =
|
|
546
|
+
const content = contentOf(file);
|
|
528
547
|
files.push({ path: file.path, offset: offset, size: content.length, mode: file.mode });
|
|
529
548
|
chunks.push(content);
|
|
530
549
|
offset += content.length;
|
|
@@ -97,7 +97,10 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
97
97
|
const locate = () => {
|
|
98
98
|
if (root)
|
|
99
99
|
return root;
|
|
100
|
-
if (config.
|
|
100
|
+
if (config.root !== undefined) {
|
|
101
|
+
root = path.resolve(__dirname, config.root);
|
|
102
|
+
}
|
|
103
|
+
else if (config.embedded) {
|
|
101
104
|
try {
|
|
102
105
|
root = extract(cacheDirectory());
|
|
103
106
|
}
|
|
@@ -121,7 +124,9 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
121
124
|
return modules.has(id.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]);
|
|
122
125
|
};
|
|
123
126
|
const tree = () => treeRequire ??= createRequire(path.join(locate(), 'index.js'));
|
|
124
|
-
|
|
127
|
+
//The files loaded by their paths (the scripts of the tree): the require of an executable of Node.js loads the modules of
|
|
128
|
+
//node only
|
|
129
|
+
const require = ((id) => fromTree(id) ? tree()(id) : path.isAbsolute(id) ? createRequire(id)(id) : nodeRequire(id));
|
|
125
130
|
require.resolve = ((id, options) => fromTree(id) ? tree().resolve(id, options) : nodeRequire.resolve(id, options));
|
|
126
131
|
require.cache = nodeRequire.cache;
|
|
127
132
|
require.main = nodeRequire.main;
|
|
@@ -132,6 +137,28 @@ export function lakutataNativeRequire(nodeRequire, config) {
|
|
|
132
137
|
process.dlopen(addon, path.join(locate(), ...key.split('/')));
|
|
133
138
|
return addon.exports;
|
|
134
139
|
};
|
|
140
|
+
//The scripts of the workers of the application, bundled into the tree (WORKERS_DIRECTORY): the directory of a key, its URL
|
|
141
|
+
const workers = (key) => path.join(locate(), '.lakutata', 'workers', ...key.split('/'));
|
|
142
|
+
require.lakutataWorker = workers;
|
|
143
|
+
require.lakutataWorkerURL = (key) => nodeRequire('node:url').pathToFileURL(workers(key) + path.sep).href;
|
|
144
|
+
//An executable started to run a script of its tree (fork(), spawn(process.execPath, [script]): process.execPath is the
|
|
145
|
+
//executable): it runs the script as node runs it, the application not started
|
|
146
|
+
require.lakutataScript = () => {
|
|
147
|
+
if (!config.embedded || config.root !== undefined)
|
|
148
|
+
return false;
|
|
149
|
+
const scripts = path.join(locate(), '.lakutata', 'workers') + path.sep;
|
|
150
|
+
const at = [1, 2].find((index) => !!process.argv[index] && path.resolve(process.argv[index]).startsWith(scripts)) ?? -1;
|
|
151
|
+
if (at < 0)
|
|
152
|
+
return false;
|
|
153
|
+
const script = path.resolve(process.argv[at]);
|
|
154
|
+
process.argv.splice(1, at - 1);
|
|
155
|
+
//The main module of node (require.main); Bun loads it by a require (its Module._load does nothing)
|
|
156
|
+
if (process.versions.bun)
|
|
157
|
+
createRequire(script)(script);
|
|
158
|
+
else
|
|
159
|
+
nodeRequire('node:module')._load(script, null, true);
|
|
160
|
+
return true;
|
|
161
|
+
};
|
|
135
162
|
//The resources of an executable are extracted with the tree (PACKAGED, EMBEDDED_RESOURCES)
|
|
136
163
|
if (config.embedded)
|
|
137
164
|
globalThis[Symbol.for('lakutata.packaged')] = { resources: () => path.join(locate(), '.lakutata', 'resources') };
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { parse } from 'acorn';
|
|
6
|
+
/**
|
|
7
|
+
* The directory of the scripts run on their own in the native tree
|
|
8
|
+
*/
|
|
9
|
+
export const WORKERS_DIRECTORY = '.lakutata/workers';
|
|
10
|
+
/**
|
|
11
|
+
* The scripts run on their own: the CommonJS scripts, which they are bundled as
|
|
12
|
+
*/
|
|
13
|
+
const SCRIPT = /\.c?js$/;
|
|
14
|
+
/**
|
|
15
|
+
* The modules of the worker threads
|
|
16
|
+
*/
|
|
17
|
+
const WORKER_THREADS = new Set(['worker_threads', 'node:worker_threads']);
|
|
18
|
+
/**
|
|
19
|
+
* The name of a function called: the identifier, the property (path.join, node_path_1.join), the last expression of a
|
|
20
|
+
* sequence (the calls of tsc: (0, node_path_1.join)(...))
|
|
21
|
+
* @param node
|
|
22
|
+
*/
|
|
23
|
+
function calleeName(node) {
|
|
24
|
+
if (node?.type === 'Identifier')
|
|
25
|
+
return node.name;
|
|
26
|
+
if (node?.type === 'MemberExpression')
|
|
27
|
+
return node.computed ? (typeof node.property.value === 'string' ? node.property.value : undefined) : node.property.name;
|
|
28
|
+
if (node?.type === 'SequenceExpression')
|
|
29
|
+
return calleeName(node.expressions[node.expressions.length - 1]);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether a node is import.meta.<name>
|
|
34
|
+
* @param node
|
|
35
|
+
* @param name
|
|
36
|
+
*/
|
|
37
|
+
function isImportMeta(node, name) {
|
|
38
|
+
return node?.type === 'MemberExpression' && !node.computed && node.object.type === 'MetaProperty' && node.object.meta.name === 'import' && node.property.name === name;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether a node is the URL of the file of the module: import.meta.url, pathToFileURL(__filename) (and its href)
|
|
42
|
+
* @param node
|
|
43
|
+
*/
|
|
44
|
+
function isFileURL(node) {
|
|
45
|
+
if (isImportMeta(node, 'url'))
|
|
46
|
+
return true;
|
|
47
|
+
if (node?.type === 'CallExpression' && calleeName(node.callee) === 'pathToFileURL')
|
|
48
|
+
return isFilename(node.arguments[0]);
|
|
49
|
+
if (node?.type === 'MemberExpression' && !node.computed && node.property.name === 'href')
|
|
50
|
+
return isFileURL(node.object);
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whether a node is the file of the module: __filename, import.meta.filename, fileURLToPath(import.meta.url)
|
|
55
|
+
* @param node
|
|
56
|
+
*/
|
|
57
|
+
function isFilename(node) {
|
|
58
|
+
if (node?.type === 'Identifier')
|
|
59
|
+
return node.name === '__filename';
|
|
60
|
+
if (isImportMeta(node, 'filename'))
|
|
61
|
+
return true;
|
|
62
|
+
return node?.type === 'CallExpression' && calleeName(node.callee) === 'fileURLToPath' && isImportMeta(node.arguments[0], 'url');
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether a node is the directory of the module: __dirname, import.meta.dirname, dirname(<its file>),
|
|
66
|
+
* fileURLToPath(new URL('.', <its URL>))
|
|
67
|
+
* @param node
|
|
68
|
+
*/
|
|
69
|
+
function isDirname(node) {
|
|
70
|
+
if (node?.type === 'Identifier')
|
|
71
|
+
return node.name === '__dirname';
|
|
72
|
+
if (isImportMeta(node, 'dirname'))
|
|
73
|
+
return true;
|
|
74
|
+
if (node?.type !== 'CallExpression')
|
|
75
|
+
return false;
|
|
76
|
+
const name = calleeName(node.callee);
|
|
77
|
+
if (name === 'dirname')
|
|
78
|
+
return isFilename(node.arguments[0]);
|
|
79
|
+
if (name !== 'fileURLToPath')
|
|
80
|
+
return false;
|
|
81
|
+
const url = node.arguments[0];
|
|
82
|
+
return url?.type === 'NewExpression' && calleeName(url.callee) === 'URL' && url.arguments[0]?.type === 'Literal' && ['.', './'].includes(url.arguments[0].value) && isFileURL(url.arguments[1]);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Whether a node is require('<module>') of one of the modules
|
|
86
|
+
* @param node
|
|
87
|
+
* @param modules
|
|
88
|
+
*/
|
|
89
|
+
function isRequireOf(node, modules) {
|
|
90
|
+
return node?.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'require' && node.arguments[0]?.type === 'Literal' && modules.has(node.arguments[0].value);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The pieces of a string expression: its literal texts, the expressions known at run time only
|
|
94
|
+
* @param node
|
|
95
|
+
* @param source
|
|
96
|
+
*/
|
|
97
|
+
function partsOf(node, source) {
|
|
98
|
+
if (node?.type === 'Literal')
|
|
99
|
+
return typeof node.value === 'string' ? [{ text: node.value }] : undefined;
|
|
100
|
+
if (node?.type === 'TemplateLiteral') {
|
|
101
|
+
const parts = [];
|
|
102
|
+
node.quasis.forEach((quasi, index) => {
|
|
103
|
+
if (quasi.value.cooked)
|
|
104
|
+
parts.push({ text: quasi.value.cooked });
|
|
105
|
+
if (index < node.expressions.length)
|
|
106
|
+
parts.push({ expression: source.slice(node.expressions[index].start, node.expressions[index].end) });
|
|
107
|
+
});
|
|
108
|
+
return parts;
|
|
109
|
+
}
|
|
110
|
+
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
|
111
|
+
const left = partsOf(node.left, source);
|
|
112
|
+
const right = partsOf(node.right, source);
|
|
113
|
+
return left && right ? [...left, ...right] : undefined;
|
|
114
|
+
}
|
|
115
|
+
if (node?.type === 'SpreadElement')
|
|
116
|
+
return undefined;
|
|
117
|
+
return [{ expression: source.slice(node.start, node.end) }];
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The operands of a chain of concatenations, from the first one
|
|
121
|
+
* @param node
|
|
122
|
+
*/
|
|
123
|
+
function operandsOf(node) {
|
|
124
|
+
return node?.type === 'BinaryExpression' && node.operator === '+' ? [...operandsOf(node.left), node.right] : [node];
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* The parts of the operands following a base
|
|
128
|
+
* @param operands
|
|
129
|
+
* @param source
|
|
130
|
+
*/
|
|
131
|
+
function partsOfOperands(operands, source) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
for (const operand of operands) {
|
|
134
|
+
const pieces = partsOf(operand, source);
|
|
135
|
+
if (!pieces)
|
|
136
|
+
return undefined;
|
|
137
|
+
parts.push(...pieces);
|
|
138
|
+
}
|
|
139
|
+
return parts;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The text of parts known when the application is bundled, undefined when one is known at run time only
|
|
143
|
+
* @param parts
|
|
144
|
+
*/
|
|
145
|
+
function textOf(parts) {
|
|
146
|
+
return parts.every((part) => 'text' in part) ? parts.map((part) => part.text).join('') : undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The path to a script an expression gives from its module, undefined when it gives none
|
|
150
|
+
* @param node
|
|
151
|
+
* @param source
|
|
152
|
+
* @param file the module
|
|
153
|
+
*/
|
|
154
|
+
function scriptPathOf(node, source, file) {
|
|
155
|
+
//new URL('./worker.js', import.meta.url), new URL('./worker.js', pathToFileURL(__filename))
|
|
156
|
+
if (node?.type === 'NewExpression' && calleeName(node.callee) === 'URL' && node.arguments.length === 2 && isFileURL(node.arguments[1])) {
|
|
157
|
+
const parts = partsOf(node.arguments[0], source);
|
|
158
|
+
const first = parts?.[0];
|
|
159
|
+
//A path relative to the module
|
|
160
|
+
if (!parts || !first || !('text' in first) || /^([a-z][a-z0-9+.-]*:|\/)/i.test(first.text))
|
|
161
|
+
return undefined;
|
|
162
|
+
return { kind: 'url', parts: parts };
|
|
163
|
+
}
|
|
164
|
+
if (node?.type === 'CallExpression') {
|
|
165
|
+
const name = calleeName(node.callee);
|
|
166
|
+
//path.join(__dirname, 'worker.js'), path.resolve(__dirname, 'workers', `${name}.js`)
|
|
167
|
+
if ((name === 'join' || name === 'resolve') && isDirname(node.arguments[0])) {
|
|
168
|
+
const parts = [];
|
|
169
|
+
for (const argument of node.arguments.slice(1)) {
|
|
170
|
+
const pieces = partsOf(argument, source);
|
|
171
|
+
if (!pieces)
|
|
172
|
+
return undefined;
|
|
173
|
+
parts.push({ text: '/' }, ...pieces);
|
|
174
|
+
}
|
|
175
|
+
return { kind: 'path', parts: parts };
|
|
176
|
+
}
|
|
177
|
+
//require.resolve('./worker'), require.resolve(path.resolve(__dirname, '../lib/worker')): the extension of node found
|
|
178
|
+
if (name === 'resolve' && node.callee.type === 'MemberExpression' && node.callee.object.type === 'Identifier' && node.callee.object.name === 'require' && node.arguments.length === 1) {
|
|
179
|
+
const argument = node.arguments[0];
|
|
180
|
+
let request;
|
|
181
|
+
if (argument.type === 'Literal' && typeof argument.value === 'string' && /^\.\.?\//.test(argument.value))
|
|
182
|
+
request = argument.value;
|
|
183
|
+
const inner = request === undefined ? scriptPathOf(argument, source, file) : undefined;
|
|
184
|
+
const text = inner?.kind === 'path' ? textOf(inner.parts) : undefined;
|
|
185
|
+
if (text !== undefined)
|
|
186
|
+
request = path.join(inner.base ?? path.dirname(file), text);
|
|
187
|
+
if (request === undefined)
|
|
188
|
+
return undefined;
|
|
189
|
+
try {
|
|
190
|
+
const resolved = createRequire(file).resolve(request);
|
|
191
|
+
return { kind: 'path', parts: [{ text: path.basename(resolved) }], base: path.dirname(resolved) };
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (node?.type === 'BinaryExpression' && node.operator === '+') {
|
|
199
|
+
const [base, ...rest] = operandsOf(node);
|
|
200
|
+
//__dirname + '/worker.js'
|
|
201
|
+
if (isDirname(base)) {
|
|
202
|
+
const parts = partsOfOperands(rest, source);
|
|
203
|
+
return parts ? { kind: 'path', parts: parts } : undefined;
|
|
204
|
+
}
|
|
205
|
+
//import.meta.url + '/../entry/worker.js': the URL of the module, as a directory
|
|
206
|
+
if (isFileURL(base)) {
|
|
207
|
+
const parts = partsOfOperands(rest, source);
|
|
208
|
+
const first = parts?.[0];
|
|
209
|
+
return parts && first && 'text' in first && first.text.startsWith('/') ? { kind: 'url-text', parts: parts, base: file } : undefined;
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
//`${__dirname}/worker.js`
|
|
214
|
+
if (node?.type === 'TemplateLiteral' && node.expressions.length && !node.quasis[0].value.cooked && isDirname(node.expressions[0])) {
|
|
215
|
+
const parts = [];
|
|
216
|
+
node.quasis.forEach((quasi, index) => {
|
|
217
|
+
if (quasi.value.cooked)
|
|
218
|
+
parts.push({ text: quasi.value.cooked });
|
|
219
|
+
if (index > 0 && index < node.expressions.length)
|
|
220
|
+
parts.push({ expression: source.slice(node.expressions[index].start, node.expressions[index].end) });
|
|
221
|
+
});
|
|
222
|
+
return { kind: 'path', parts: parts };
|
|
223
|
+
}
|
|
224
|
+
return undefined;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Visit the nodes of a syntax tree, depth first: the children of a node are not visited when the visitor returns false
|
|
228
|
+
* @param node
|
|
229
|
+
* @param visit
|
|
230
|
+
*/
|
|
231
|
+
function walk(node, visit) {
|
|
232
|
+
if (!node || typeof node.type !== 'string')
|
|
233
|
+
return;
|
|
234
|
+
if (visit(node) === false)
|
|
235
|
+
return;
|
|
236
|
+
for (const key of Object.keys(node)) {
|
|
237
|
+
const value = node[key];
|
|
238
|
+
if (Array.isArray(value))
|
|
239
|
+
for (const child of value)
|
|
240
|
+
walk(child, visit);
|
|
241
|
+
else if (value && typeof value === 'object' && typeof value.type === 'string')
|
|
242
|
+
walk(value, visit);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* A text in a template literal
|
|
247
|
+
* @param text
|
|
248
|
+
*/
|
|
249
|
+
function templateText(text) {
|
|
250
|
+
return text.replace(/[`\\]|\$\{/g, (matched) => `\\${matched}`);
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* The scripts an application runs on its own, by their paths, which the bundler does not follow: the scripts of its
|
|
254
|
+
* workers (new Worker(), the pools of workers given a file), of its child processes (fork()), the modules it loads by
|
|
255
|
+
* their paths (require(path.join(__dirname, 'x.js'))), in its modules and in the ones of its packages. A path to a script
|
|
256
|
+
* given from its module, wherever it is written, is found when the module is bundled: the script is bundled on its own
|
|
257
|
+
* into the native tree (WORKERS_DIRECTORY, the directories mirrored), and the expression rewritten to it. A path known at
|
|
258
|
+
* run time only in part (a template, a concatenation, an argument of path.join: `${name}.js`) gives every script of its
|
|
259
|
+
* directory its pattern matches, the expression of the part kept. The scripts of the workers of node (worker_threads)
|
|
260
|
+
* whose paths cannot be followed are warned of, in the modules of the application (the packages start workers from the
|
|
261
|
+
* paths their users give)
|
|
262
|
+
*/
|
|
263
|
+
export class WorkerCollector {
|
|
264
|
+
/**
|
|
265
|
+
* @param project The directory of the project: the scripts are named from it (as the modules the bundler gives, its
|
|
266
|
+
* real path)
|
|
267
|
+
*/
|
|
268
|
+
constructor(project) {
|
|
269
|
+
/**
|
|
270
|
+
* The scripts: their paths in the native tree, and their files
|
|
271
|
+
*/
|
|
272
|
+
this.scripts = new Map();
|
|
273
|
+
this.warnings = [];
|
|
274
|
+
this.project = realpathSync(project);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* The key of a directory in the tree ("/" separated): its path from the project; outside of the project (the
|
|
278
|
+
* packages of a workspace, linked), its path from the node_modules holding it, else a hash of its path (the paths of
|
|
279
|
+
* the machine bundling it are not shipped)
|
|
280
|
+
* @param directory
|
|
281
|
+
*/
|
|
282
|
+
keyOf(directory) {
|
|
283
|
+
const relative = path.relative(this.project, directory);
|
|
284
|
+
if (!relative)
|
|
285
|
+
return '_';
|
|
286
|
+
if (!relative.startsWith('..'))
|
|
287
|
+
return relative.split(path.sep).join('/');
|
|
288
|
+
const segments = directory.split(path.sep);
|
|
289
|
+
const modules = segments.indexOf('node_modules');
|
|
290
|
+
return modules >= 0 ? segments.slice(modules).join('/') : `_outside/${createHash('sha256').update(directory).digest('hex').slice(0, 16)}`;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The name of a script: its path from the project ("/" separated), or from the node_modules holding it outside of the
|
|
294
|
+
* project (the packages of a workspace)
|
|
295
|
+
* @param script
|
|
296
|
+
*/
|
|
297
|
+
nameOf(script) {
|
|
298
|
+
const relative = path.relative(this.project, script);
|
|
299
|
+
const segments = script.split(path.sep);
|
|
300
|
+
const modules = segments.indexOf('node_modules');
|
|
301
|
+
return relative.startsWith('..') && modules >= 0 ? segments.slice(modules).join('/') : relative.split(path.sep).join('/');
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* The scripts a path gives: the file, or the ones its pattern matches; their directory and the parts of their paths
|
|
305
|
+
* from it
|
|
306
|
+
* @param found
|
|
307
|
+
* @param file the module
|
|
308
|
+
*/
|
|
309
|
+
scriptsOf(found, file) {
|
|
310
|
+
//The directory: the texts before the first expression, up to their last separator
|
|
311
|
+
const firstExpression = found.parts.findIndex((part) => 'expression' in part);
|
|
312
|
+
const leading = firstExpression < 0 ? found.parts : found.parts.slice(0, firstExpression);
|
|
313
|
+
const prefix = leading.map((part) => part.text).join('');
|
|
314
|
+
const cut = prefix.lastIndexOf('/');
|
|
315
|
+
const directory = path.resolve(path.join(found.base ?? path.dirname(file), prefix.slice(0, cut + 1)));
|
|
316
|
+
const rest = [...prefix.slice(cut + 1) ? [{ text: prefix.slice(cut + 1) }] : [], ...found.parts.slice(leading.length)];
|
|
317
|
+
if (firstExpression < 0) {
|
|
318
|
+
const script = path.join(directory, prefix.slice(cut + 1));
|
|
319
|
+
return { directory: directory, rest: rest, files: SCRIPT.test(script) && existsSync(script) && statSync(script).isFile() ? [script] : [] };
|
|
320
|
+
}
|
|
321
|
+
//The pattern: an expression matches a part of a segment
|
|
322
|
+
const pattern = new RegExp(`^${rest.map((part) => 'text' in part ? part.text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') : '[^/]*').join('')}$`);
|
|
323
|
+
const depth = rest.map((part) => 'text' in part ? part.text : '').join('').split('/').length;
|
|
324
|
+
const files = [];
|
|
325
|
+
const search = (at, relative, level) => {
|
|
326
|
+
if (level > depth || !existsSync(at))
|
|
327
|
+
return;
|
|
328
|
+
for (const entry of readdirSync(at, { withFileTypes: true })) {
|
|
329
|
+
const name = relative ? `${relative}/${entry.name}` : entry.name;
|
|
330
|
+
if (entry.isDirectory() && entry.name !== 'node_modules')
|
|
331
|
+
search(path.join(at, entry.name), name, level + 1);
|
|
332
|
+
else if (entry.isFile() && SCRIPT.test(entry.name) && pattern.test(name))
|
|
333
|
+
files.push(path.join(at, entry.name));
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
search(directory, '', 1);
|
|
337
|
+
return { directory: directory, rest: rest, files: files.sort() };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Warn of a worker in a module of the application
|
|
341
|
+
* @param file
|
|
342
|
+
* @param line
|
|
343
|
+
* @param message
|
|
344
|
+
*/
|
|
345
|
+
warn(file, line, message) {
|
|
346
|
+
if (file.split(path.sep).includes('node_modules'))
|
|
347
|
+
return;
|
|
348
|
+
this.warnings.push(`${path.relative(this.project, file)}:${line}: ${message}`);
|
|
349
|
+
}
|
|
350
|
+
/**
|
|
351
|
+
* Warn of the scripts bundled with the application and run on their own too: two copies of their modules
|
|
352
|
+
* @param bundled the files of the bundle of the application (their real paths)
|
|
353
|
+
*/
|
|
354
|
+
warnBundled(bundled) {
|
|
355
|
+
for (const script of new Set(this.scripts.values())) {
|
|
356
|
+
if (!bundled.has(script) || script.split(path.sep).includes('node_modules'))
|
|
357
|
+
continue;
|
|
358
|
+
this.warnings.push(`${path.relative(this.project, script)}: is bundled with the application and run on its own by its path (a worker, a child process, a module loaded by its path): two copies of its module, their state and their classes apart — load it by its path only, or import it only`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* The source of a module with its paths to scripts rewritten to the scripts in the native tree; undefined when it
|
|
363
|
+
* gives none
|
|
364
|
+
* @param source
|
|
365
|
+
* @param file
|
|
366
|
+
*/
|
|
367
|
+
transform(source, file) {
|
|
368
|
+
if (!/__dirname|__filename|import\.meta|require\.resolve|\bWorker\b/.test(source))
|
|
369
|
+
return undefined;
|
|
370
|
+
let program;
|
|
371
|
+
for (const sourceType of ['module', 'script']) {
|
|
372
|
+
try {
|
|
373
|
+
program = parse(source, { ecmaVersion: 'latest', sourceType: sourceType, locations: true, allowHashBang: true, allowReturnOutsideFunction: true, allowAwaitOutsideFunction: true, allowImportExportEverywhere: true });
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
program = undefined;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
if (!program)
|
|
381
|
+
return undefined;
|
|
382
|
+
//The values of the variables (const script = ...) and of the properties of the classes (workerPath = ..., this.workerPath
|
|
383
|
+
//= ...) declared once; the constructors of the workers of node and their modules (Worker, worker_threads_1)
|
|
384
|
+
const constants = new Map();
|
|
385
|
+
const members = new Map();
|
|
386
|
+
const constructors = new Set();
|
|
387
|
+
const namespaces = new Set();
|
|
388
|
+
const remember = (values, name, value) => values.set(name, values.has(name) ? null : value);
|
|
389
|
+
walk(program, (node) => {
|
|
390
|
+
if (node.type === 'ImportDeclaration' && WORKER_THREADS.has(node.source.value)) {
|
|
391
|
+
for (const specifier of node.specifiers) {
|
|
392
|
+
if (specifier.type === 'ImportSpecifier' && (specifier.imported.name ?? specifier.imported.value) === 'Worker')
|
|
393
|
+
constructors.add(specifier.local.name);
|
|
394
|
+
else if (specifier.type !== 'ImportSpecifier')
|
|
395
|
+
namespaces.add(specifier.local.name);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (node.type === 'VariableDeclaration') {
|
|
399
|
+
for (const declarator of node.declarations) {
|
|
400
|
+
const init = declarator.init?.type === 'CallExpression' && isRequireOf(declarator.init.arguments[0], WORKER_THREADS) ? declarator.init.arguments[0] : declarator.init;
|
|
401
|
+
if (declarator.id.type === 'Identifier') {
|
|
402
|
+
remember(constants, declarator.id.name, declarator.init);
|
|
403
|
+
if (isRequireOf(init, WORKER_THREADS))
|
|
404
|
+
namespaces.add(declarator.id.name);
|
|
405
|
+
if (init?.type === 'MemberExpression' && isRequireOf(init.object, WORKER_THREADS) && calleeName(init) === 'Worker')
|
|
406
|
+
constructors.add(declarator.id.name);
|
|
407
|
+
}
|
|
408
|
+
else if (declarator.id.type === 'ObjectPattern' && isRequireOf(init, WORKER_THREADS)) {
|
|
409
|
+
for (const property of declarator.id.properties)
|
|
410
|
+
if (property.key?.name === 'Worker' && property.value?.type === 'Identifier')
|
|
411
|
+
constructors.add(property.value.name);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (node.type === 'PropertyDefinition' && !node.computed && node.key && node.value)
|
|
416
|
+
remember(members, node.key.name, node.value);
|
|
417
|
+
if (node.type === 'AssignmentExpression' && node.operator === '=' && node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression' && !node.left.computed)
|
|
418
|
+
remember(members, node.left.property.name, node.right);
|
|
419
|
+
});
|
|
420
|
+
//The paths to scripts, wherever they are written
|
|
421
|
+
const edits = [];
|
|
422
|
+
const followed = new Set();
|
|
423
|
+
walk(program, (node) => {
|
|
424
|
+
const found = scriptPathOf(node, source, file);
|
|
425
|
+
if (!found)
|
|
426
|
+
return true;
|
|
427
|
+
const { directory, rest, files } = this.scriptsOf(found, file);
|
|
428
|
+
//A path to another file (a directory, a data file, a file not found): left as it is
|
|
429
|
+
if (!files.length)
|
|
430
|
+
return true;
|
|
431
|
+
const key = this.keyOf(directory);
|
|
432
|
+
for (const script of files)
|
|
433
|
+
this.scripts.set(`${WORKERS_DIRECTORY}/${key}/${path.relative(directory, script).split(path.sep).join('/')}`, script);
|
|
434
|
+
const relative = `\`${rest.map((part) => 'text' in part ? templateText(part.text) : `\${${part.expression}}`).join('')}\``;
|
|
435
|
+
const text = found.kind === 'url' ? `new URL(${relative}, require.lakutataWorkerURL(${JSON.stringify(key)}))` :
|
|
436
|
+
found.kind === 'url-text' ? `(require.lakutataWorkerURL(${JSON.stringify(key)}) + ${relative})` :
|
|
437
|
+
`require("node:path").join(require.lakutataWorker(${JSON.stringify(key)}), ${relative})`;
|
|
438
|
+
followed.add(node);
|
|
439
|
+
edits.push({ start: node.start, end: node.end, text: text });
|
|
440
|
+
return false;
|
|
441
|
+
});
|
|
442
|
+
//The workers of node whose scripts are not followed
|
|
443
|
+
walk(program, (node) => {
|
|
444
|
+
if (node.type !== 'NewExpression' || !node.arguments.length)
|
|
445
|
+
return;
|
|
446
|
+
const callee = node.callee;
|
|
447
|
+
const isWorker = callee.type === 'Identifier' ? constructors.has(callee.name) :
|
|
448
|
+
calleeName(callee) === 'Worker' && callee.type === 'MemberExpression' && ((callee.object.type === 'Identifier' && namespaces.has(callee.object.name)) || isRequireOf(callee.object, WORKER_THREADS));
|
|
449
|
+
if (!isWorker)
|
|
450
|
+
return;
|
|
451
|
+
//A worker running a source (eval: true)
|
|
452
|
+
const options = node.arguments[1];
|
|
453
|
+
if (options?.type === 'ObjectExpression' && options.properties.some((property) => property.key?.name === 'eval' && property.value?.value === true))
|
|
454
|
+
return;
|
|
455
|
+
const argument = node.arguments[0];
|
|
456
|
+
const value = argument.type === 'Identifier' && constants.get(argument.name) ? constants.get(argument.name) :
|
|
457
|
+
argument.type === 'MemberExpression' && argument.object.type === 'ThisExpression' && !argument.computed && members.get(argument.property.name) ? members.get(argument.property.name) : argument;
|
|
458
|
+
if (followed.has(value))
|
|
459
|
+
return;
|
|
460
|
+
//As written: the warnings name it
|
|
461
|
+
const written = source.slice(argument.start, argument.end);
|
|
462
|
+
const line = node.loc.start.line;
|
|
463
|
+
if (scriptPathOf(value, source, file))
|
|
464
|
+
return this.warn(file, line, `the script of the worker (${written}) is not found: a .js or .cjs file from the module, built before the bundle`);
|
|
465
|
+
if (value.type === 'Literal' && typeof value.value === 'string')
|
|
466
|
+
return this.warn(file, line, `the script of the worker (${value.value}) is a path from the current directory of the process, not the bundle: give it from the directory of the module, path.join(__dirname, ${JSON.stringify(value.value)}) or new URL(${JSON.stringify(value.value)}, import.meta.url)`);
|
|
467
|
+
this.warn(file, line, `the script of the worker cannot be followed (${written}): give its path from the directory of the module (path.join(__dirname, 'worker.js'), new URL('./worker.js', import.meta.url)), or ship it as a resource (lakutata.resources)`);
|
|
468
|
+
});
|
|
469
|
+
if (!edits.length)
|
|
470
|
+
return undefined;
|
|
471
|
+
let result = source;
|
|
472
|
+
for (const edit of edits.sort((a, b) => b.start - a.start))
|
|
473
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
474
|
+
return result;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
@@ -236,6 +236,7 @@ export async function buildElectronInstaller(project, manifest, config, options)
|
|
|
236
236
|
outfile: path.join(application, 'main.cjs'),
|
|
237
237
|
directories: { native: path.join(work, 'main.native'), resources: path.join(work, 'main.resources') }
|
|
238
238
|
});
|
|
239
|
+
warnings.push(...bundle.warnings);
|
|
239
240
|
const maintainer = platform === 'linux' ? packageMaintainer(author, name, warnings) : author ?? name;
|
|
240
241
|
//The packages of fpm need a homepage
|
|
241
242
|
let homepage = config.homepage ?? (typeof manifest.homepage === 'string' ? manifest.homepage : undefined);
|
|
@@ -354,6 +355,7 @@ export async function buildElectronInstaller(project, manifest, config, options)
|
|
|
354
355
|
platform: `${process.platform}-${process.arch}`,
|
|
355
356
|
size: bundle.size,
|
|
356
357
|
native: bundle.native?.modules ?? [],
|
|
358
|
+
workers: bundle.workers,
|
|
357
359
|
resources: bundle.resources,
|
|
358
360
|
runtime: `Electron v${electronVersion}`,
|
|
359
361
|
warnings: []
|
|
@@ -70,6 +70,14 @@ export type BundleResult = {
|
|
|
70
70
|
*/
|
|
71
71
|
resources: string[];
|
|
72
72
|
resourcesDirectory?: string;
|
|
73
|
+
/**
|
|
74
|
+
* The scripts of the workers of the application (their files, from the project), bundled into the native tree
|
|
75
|
+
*/
|
|
76
|
+
workers: string[];
|
|
77
|
+
/**
|
|
78
|
+
* What the bundle cannot follow: the scripts of the workers given by paths known at run time only
|
|
79
|
+
*/
|
|
80
|
+
warnings: string[];
|
|
73
81
|
};
|
|
74
82
|
/**
|
|
75
83
|
* The name of the program of a package: its first bin, or the name of the package without its scope
|
|
@@ -23,6 +23,10 @@ export type ExecutableResult = {
|
|
|
23
23
|
* The packages of native addons embedded, extracted at the first run, or next to the executable
|
|
24
24
|
*/
|
|
25
25
|
native: string[];
|
|
26
|
+
/**
|
|
27
|
+
* The scripts of the workers of the application (their files, from the project), with the native addons
|
|
28
|
+
*/
|
|
29
|
+
workers: string[];
|
|
26
30
|
/**
|
|
27
31
|
* The resources of the application (their paths in the resources)
|
|
28
32
|
*/
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Plugin, PluginBuild } from 'esbuild';
|
|
2
2
|
/**
|
|
3
|
-
* A file of the native tree: its path in the tree ("/" separated) and where it is read
|
|
3
|
+
* A file of the native tree: its path in the tree ("/" separated) and where it is read, or its content (a script of a
|
|
4
|
+
* worker, bundled)
|
|
4
5
|
*/
|
|
5
6
|
export type NativeFile = {
|
|
6
7
|
path: string;
|
|
7
8
|
source: string;
|
|
8
9
|
mode: number;
|
|
10
|
+
content?: Buffer;
|
|
9
11
|
};
|
|
10
12
|
/**
|
|
11
13
|
* A link of the native tree to a directory of the tree, as the links of the node_modules (pnpm, the workspaces)
|
|
@@ -136,8 +138,10 @@ export declare class NativeCollector {
|
|
|
136
138
|
}): Promise<string | undefined>;
|
|
137
139
|
/**
|
|
138
140
|
* The plugin of the bundle: the native packages are external, the addons of the application are loaded from the tree
|
|
141
|
+
* @param transforms the transformations of the sources of the modules (the paths of the workers), giving their new
|
|
142
|
+
* source or undefined
|
|
139
143
|
*/
|
|
140
|
-
plugin(): Plugin;
|
|
144
|
+
plugin(transforms?: ((source: string, file: string) => string | undefined)[]): Plugin;
|
|
141
145
|
/**
|
|
142
146
|
* The packages a native package loads at run time: the JavaScript it requires (followed by esbuild), the packages of
|
|
143
147
|
* shared libraries it depends on (the libraries linked with its addons), and the native packages it loads
|
|
@@ -24,6 +24,11 @@ export type NativeRuntimeConfig = {
|
|
|
24
24
|
* in its .lakutata/resources), or a directory next to the bundle
|
|
25
25
|
*/
|
|
26
26
|
embedded: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* The tree from the directory of the bundle, for the scripts of the workers, which run from the tree (its
|
|
29
|
+
* .lakutata/workers): the tree is not looked for (nor extracted)
|
|
30
|
+
*/
|
|
31
|
+
root?: string;
|
|
27
32
|
};
|
|
28
33
|
/**
|
|
29
34
|
* The global symbol of the packed applications giving the directory of their resources (resourcesPath of lakutata)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory of the scripts run on their own in the native tree
|
|
3
|
+
*/
|
|
4
|
+
export declare const WORKERS_DIRECTORY: string;
|
|
5
|
+
/**
|
|
6
|
+
* A piece of a path to a script: a text, or an expression known at run time only (the source of the expression)
|
|
7
|
+
*/
|
|
8
|
+
type Part = {
|
|
9
|
+
text: string;
|
|
10
|
+
} | {
|
|
11
|
+
expression: string;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* A path to a script given from a module: from its directory (path.join, path.resolve, a concatenation or a template of
|
|
15
|
+
* __dirname), a URL relative to its file (new URL('./x.js', import.meta.url)), a URL text from its file
|
|
16
|
+
* (import.meta.url + '/../x.js'), or a file resolved when the application is bundled (require.resolve)
|
|
17
|
+
*/
|
|
18
|
+
type ScriptPath = {
|
|
19
|
+
/**
|
|
20
|
+
* What the expression gives: a path, a URL, the text of a URL
|
|
21
|
+
*/
|
|
22
|
+
kind: 'path' | 'url' | 'url-text';
|
|
23
|
+
parts: Part[];
|
|
24
|
+
/**
|
|
25
|
+
* The path the parts are joined to, the directory of the module by default
|
|
26
|
+
*/
|
|
27
|
+
base?: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* The scripts an application runs on its own, by their paths, which the bundler does not follow: the scripts of its
|
|
31
|
+
* workers (new Worker(), the pools of workers given a file), of its child processes (fork()), the modules it loads by
|
|
32
|
+
* their paths (require(path.join(__dirname, 'x.js'))), in its modules and in the ones of its packages. A path to a script
|
|
33
|
+
* given from its module, wherever it is written, is found when the module is bundled: the script is bundled on its own
|
|
34
|
+
* into the native tree (WORKERS_DIRECTORY, the directories mirrored), and the expression rewritten to it. A path known at
|
|
35
|
+
* run time only in part (a template, a concatenation, an argument of path.join: `${name}.js`) gives every script of its
|
|
36
|
+
* directory its pattern matches, the expression of the part kept. The scripts of the workers of node (worker_threads)
|
|
37
|
+
* whose paths cannot be followed are warned of, in the modules of the application (the packages start workers from the
|
|
38
|
+
* paths their users give)
|
|
39
|
+
*/
|
|
40
|
+
export declare class WorkerCollector {
|
|
41
|
+
/**
|
|
42
|
+
* The scripts: their paths in the native tree, and their files
|
|
43
|
+
*/
|
|
44
|
+
readonly scripts: Map<string, string>;
|
|
45
|
+
readonly warnings: string[];
|
|
46
|
+
protected readonly project: string;
|
|
47
|
+
/**
|
|
48
|
+
* @param project The directory of the project: the scripts are named from it (as the modules the bundler gives, its
|
|
49
|
+
* real path)
|
|
50
|
+
*/
|
|
51
|
+
constructor(project: string);
|
|
52
|
+
/**
|
|
53
|
+
* The key of a directory in the tree ("/" separated): its path from the project; outside of the project (the
|
|
54
|
+
* packages of a workspace, linked), its path from the node_modules holding it, else a hash of its path (the paths of
|
|
55
|
+
* the machine bundling it are not shipped)
|
|
56
|
+
* @param directory
|
|
57
|
+
*/
|
|
58
|
+
protected keyOf(directory: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* The name of a script: its path from the project ("/" separated), or from the node_modules holding it outside of the
|
|
61
|
+
* project (the packages of a workspace)
|
|
62
|
+
* @param script
|
|
63
|
+
*/
|
|
64
|
+
nameOf(script: string): string;
|
|
65
|
+
/**
|
|
66
|
+
* The scripts a path gives: the file, or the ones its pattern matches; their directory and the parts of their paths
|
|
67
|
+
* from it
|
|
68
|
+
* @param found
|
|
69
|
+
* @param file the module
|
|
70
|
+
*/
|
|
71
|
+
protected scriptsOf(found: ScriptPath, file: string): {
|
|
72
|
+
directory: string;
|
|
73
|
+
rest: Part[];
|
|
74
|
+
files: string[];
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* Warn of a worker in a module of the application
|
|
78
|
+
* @param file
|
|
79
|
+
* @param line
|
|
80
|
+
* @param message
|
|
81
|
+
*/
|
|
82
|
+
protected warn(file: string, line: number, message: string): void;
|
|
83
|
+
/**
|
|
84
|
+
* Warn of the scripts bundled with the application and run on their own too: two copies of their modules
|
|
85
|
+
* @param bundled the files of the bundle of the application (their real paths)
|
|
86
|
+
*/
|
|
87
|
+
warnBundled(bundled: Set<string>): void;
|
|
88
|
+
/**
|
|
89
|
+
* The source of a module with its paths to scripts rewritten to the scripts in the native tree; undefined when it
|
|
90
|
+
* gives none
|
|
91
|
+
* @param source
|
|
92
|
+
* @param file
|
|
93
|
+
*/
|
|
94
|
+
transform(source: string, file: string): string | undefined;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lakutata/builder",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.4",
|
|
4
4
|
"description": "The builder of the applications of the lakutata framework: their bundles, their executables (Node.js, Bun) with their native addons and their resources.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/esm/index.js",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"test:installer:windows": "npm run compile && node --test dist/esm/tests/InstallerWindows.spec.js"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"acorn": "^8.19.0",
|
|
26
27
|
"esbuild": "^0.28.2",
|
|
27
28
|
"postject": "^1.0.0-alpha.6",
|
|
28
29
|
"resedit": "^1.7.2"
|
|
@@ -47,5 +48,10 @@
|
|
|
47
48
|
"*.md",
|
|
48
49
|
"LICENSE",
|
|
49
50
|
"package.json"
|
|
50
|
-
]
|
|
51
|
+
],
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"piscina": "^5.3.2",
|
|
54
|
+
"tinypool": "^2.2.0",
|
|
55
|
+
"workerpool": "^10.0.3"
|
|
56
|
+
}
|
|
51
57
|
}
|