@lakutata/builder 3.1.0 → 3.1.3

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.
@@ -98,7 +98,7 @@ export async function bundleProject(options) {
98
98
  throw new Error(`The entry ${path.relative(process.cwd(), entry) || entry} does not exist: build the project first`);
99
99
  const outfile = path.resolve(project, options.outfile ?? path.join('dist', `${name}.cjs`));
100
100
  const collector = new NativeCollector(project, options.runtime);
101
- const files = new FileCollector(project);
101
+ const files = new FileCollector(project, { packageOf: (file) => collector.addonPackageOf(file), nativeFilesOf: (root, directory) => collector.nativeFilesOf(root, directory) });
102
102
  //esbuild is loaded by the commands which need it
103
103
  const { build } = await import('esbuild');
104
104
  //The application, and each script of its workers (the bundler does not follow new Worker()): their native packages
@@ -147,9 +147,15 @@ export async function bundleProject(options) {
147
147
  const resourcesDirectory = path.resolve(path.dirname(outfile), options.directories?.resources ?? `${base}.resources`);
148
148
  //The scripts of the workers, until the ones they start are bundled too
149
149
  const scripts = new Map();
150
+ //The files of every bundle, the application's and its workers'
151
+ const inputs = new Set(application.inputs);
150
152
  for (let pending = [...files.scripts]; pending.length; pending = [...files.scripts].filter(([at]) => !scripts.has(at))) {
151
- for (const [at, script] of pending)
152
- scripts.set(at, (await bundle(script, path.join(nativeDirectory, ...at.split('/')))).code);
153
+ for (const [at, script] of pending) {
154
+ const bundled = await bundle(script, path.join(nativeDirectory, ...at.split('/')));
155
+ scripts.set(at, bundled.code);
156
+ for (const input of bundled.inputs)
157
+ inputs.add(input);
158
+ }
153
159
  }
154
160
  files.warnBundled(application.inputs);
155
161
  const embedded = options.native === 'embedded';
@@ -221,6 +227,6 @@ export async function bundleProject(options) {
221
227
  resourcesDirectory: resources.files.length && !embedded ? resourcesDirectory : undefined,
222
228
  workers: [...new Set([...scripts.keys()].map((at) => files.nameOf(files.scripts.get(at))))].sort(),
223
229
  files: [...files.roots].filter((root) => !files.scriptPaths().has(root)).map((root) => files.nameOf(root)).sort(),
224
- warnings: files.warnings
230
+ warnings: [...files.warnings, ...collector.unshippedAddons(inputs, files.files.values())]
225
231
  };
226
232
  }
@@ -291,8 +291,10 @@ export class FileCollector {
291
291
  /**
292
292
  * @param project The directory of the project: the scripts are named from it (as the modules the bundler gives, its
293
293
  * real path)
294
+ * @param addons The packages sharing the framework and holding addons
294
295
  */
295
- constructor(project) {
296
+ constructor(project, addons) {
297
+ this.addons = addons;
296
298
  /**
297
299
  * The scripts: their paths in the native tree, and their files
298
300
  */
@@ -354,6 +356,7 @@ export class FileCollector {
354
356
  const directory = path.resolve(path.join(found.base ?? path.dirname(file), prefix.slice(0, cut + 1)));
355
357
  const rest = [...prefix.slice(cut + 1) ? [{ text: prefix.slice(cut + 1) }] : [], ...found.parts.slice(leading.length)];
356
358
  const targets = { directory: directory, rest: rest, scripts: [], files: [], roots: [] };
359
+ const addons = this.addons?.packageOf(file);
357
360
  //A file: bundled when it is a script, copied otherwise; a directory of data: its files copied
358
361
  const take = (at, strict) => {
359
362
  const stats = statSync(at);
@@ -362,8 +365,19 @@ export class FileCollector {
362
365
  targets.roots.push(at);
363
366
  }
364
367
  else if (stats.isDirectory()) {
365
- //A directory holding the module (the application, its packages), or holding code: not shipped
366
- if ((file + path.sep).startsWith(at + path.sep)) {
368
+ //A directory of the package of addons of the module, bundled with the application: its native files
369
+ //(its addons found from it, as node-gyp-build finds them)
370
+ if (addons && (at + path.sep).startsWith(addons + path.sep)) {
371
+ targets.files.push(...this.addons.nativeFilesOf(addons, at));
372
+ targets.roots.push(at);
373
+ //The directory named exactly (the package above the module): the files are named from it
374
+ if (strict) {
375
+ targets.directory = at;
376
+ targets.rest = [];
377
+ }
378
+ }
379
+ else if ((file + path.sep).startsWith(at + path.sep)) {
380
+ //A directory holding the module (the application, its packages), or holding code: not shipped
367
381
  if (strict)
368
382
  targets.refused = 'ancestor';
369
383
  }
@@ -452,8 +466,29 @@ export class FileCollector {
452
466
  const members = new Map();
453
467
  const constructors = new Set();
454
468
  const namespaces = new Set();
469
+ //The names of the properties (a.__dirname, {__dirname: a}, {__dirname}), which are not to be rewritten; whether the
470
+ //module declares a __dirname of its own (a variable, a parameter: the name is not the directory of the module)
471
+ const properties = new Set();
472
+ let declaresDirname = false;
473
+ const names = (pattern) => {
474
+ let found = false;
475
+ walk(pattern, (node) => {
476
+ if (node.type === 'Identifier' && node.name === '__dirname')
477
+ found = true;
478
+ });
479
+ return found;
480
+ };
455
481
  const remember = (values, name, value) => values.set(name, values.has(name) ? null : value);
456
482
  walk(program, (node) => {
483
+ if (node.type === 'MemberExpression' && !node.computed)
484
+ properties.add(node.property);
485
+ if (node.type === 'Property' && !node.computed) {
486
+ properties.add(node.key);
487
+ if (node.shorthand)
488
+ properties.add(node.value);
489
+ }
490
+ if ((node.type === 'VariableDeclarator' && names(node.id)) || (node.type === 'CatchClause' && node.param && names(node.param)) || (/Function/.test(node.type) && node.params.some(names)))
491
+ declaresDirname = true;
457
492
  if (node.type === 'ImportDeclaration' && WORKER_THREADS.has(node.source.value)) {
458
493
  for (const specifier of node.specifiers) {
459
494
  if (specifier.type === 'ImportSpecifier' && (specifier.imported.name ?? specifier.imported.value) === 'Worker')
@@ -484,11 +519,15 @@ export class FileCollector {
484
519
  if (node.type === 'AssignmentExpression' && node.operator === '=' && node.left.type === 'MemberExpression' && node.left.object.type === 'ThisExpression' && !node.left.computed)
485
520
  remember(members, node.left.property.name, node.right);
486
521
  });
487
- //The paths to files, wherever they are written
522
+ //The paths to files, wherever they are written. In a module of a package of addons bundled with the application,
523
+ //the directory of the module itself too (given to node-gyp-build): the native files of the package are there, in
524
+ //the tree (a __dirname the module declares is its own: the value it is given, rewritten when it is the directory)
525
+ const ownDirectory = !!this.addons?.packageOf(file);
526
+ const isOwnDirectory = (node) => ownDirectory && isDirname(node) && !properties.has(node) && !(node.type === 'Identifier' && declaresDirname);
488
527
  const edits = [];
489
528
  const followed = new Set();
490
529
  walk(program, (node) => {
491
- const found = scriptPathOf(node, source, file);
530
+ const found = scriptPathOf(node, source, file) ?? (isOwnDirectory(node) ? { kind: 'path', parts: [] } : undefined);
492
531
  if (!found)
493
532
  return true;
494
533
  const targets = this.targetsOf(found, file);
@@ -227,7 +227,7 @@ export class NativeCollector {
227
227
  }
228
228
  /**
229
229
  * Whether a package is left out of the bundle: it holds addons, or one of its dependencies does (it loads them,
230
- * with computed paths often), and it is not the project
230
+ * with computed paths often), and it is neither the project nor a package sharing the framework
231
231
  * @param root
232
232
  */
233
233
  isNative(root) {
@@ -236,14 +236,68 @@ export class NativeCollector {
236
236
  const info = this.package(root);
237
237
  if (!info)
238
238
  return false;
239
- if (info.addons)
240
- return true;
241
239
  //A package of the framework, or built on it, shares it with the application: bundled with it (one copy of its
242
- //classes, its container), its native dependencies loaded by their names, which the bundler follows
240
+ //classes, its container), its native dependencies loaded by their names, which the bundler follows, and its own
241
+ //addons loaded from the tree (a require of the addon, or a path from its module: the bundler follows both)
243
242
  if (sharesFramework(info.manifest))
244
243
  return false;
244
+ if (info.addons)
245
+ return true;
245
246
  return this.dependencies(root).some(({ root: dependency }) => !!this.package(dependency)?.addons);
246
247
  }
248
+ /**
249
+ * The package sharing the framework and holding addons a file belongs to: bundled with the application, the paths
250
+ * to its own directories ship their native files (its addons, its manifest, its data)
251
+ * @param file
252
+ * @returns its directory, undefined for a file of another package
253
+ */
254
+ addonPackageOf(file) {
255
+ const root = this.packageRoot(file);
256
+ if (!root || root === this.project)
257
+ return undefined;
258
+ const info = this.package(root);
259
+ return info?.addons && sharesFramework(info.manifest) ? root : undefined;
260
+ }
261
+ /**
262
+ * The native files of a directory of a package holding addons: the files node loads at run time (its addons, its
263
+ * libraries, its manifest, its data), without its scripts (bundled) and what node never loads
264
+ * @param root the directory of the package
265
+ * @param directory
266
+ */
267
+ nativeFilesOf(root, directory) {
268
+ const cargo = existsSync(path.join(root, 'Cargo.toml'));
269
+ return packageFiles(directory).filter((file) => {
270
+ const relative = path.relative(root, file).split(path.sep).join('/');
271
+ return !SCRIPT.test(file) && !UNUSED.test(file) && !BUILD_FILES.test(relative) && !(cargo && relative.startsWith('target/'));
272
+ });
273
+ }
274
+ /**
275
+ * The addons of the packages sharing the framework bundled with the application which are not shipped: none of
276
+ * their addons is loaded by a require or a path the bundler follows (they would not be found at run time)
277
+ * @param inputs the files bundled
278
+ * @param shipped the files of the tree (the addons loaded by a require, the files read by a path from a module)
279
+ * @returns the warnings, one by package
280
+ */
281
+ unshippedAddons(inputs, shipped) {
282
+ const files = new Set([...shipped, ...this.addons.values()]);
283
+ const roots = new Set();
284
+ for (const input of inputs) {
285
+ const root = this.packageRoot(input);
286
+ if (root && root !== this.project)
287
+ roots.add(root);
288
+ }
289
+ const warnings = [];
290
+ for (const root of [...roots].sort()) {
291
+ const info = this.package(root);
292
+ if (!info?.addons || !sharesFramework(info.manifest))
293
+ continue;
294
+ const addons = packageFiles(root).filter((file) => ADDON.test(file));
295
+ if (addons.some((addon) => files.has(addon)))
296
+ continue;
297
+ warnings.push(`${info.name}: its native addon (${addons.map((addon) => path.relative(root, addon).split(path.sep).join('/')).join(', ')}) is not shipped: the bundler does not follow how it is loaded. A package built on the framework is bundled with the application: load its addon by a require (require('./build/Release/addon.node')) or by a path from its module (path.join(__dirname, 'prebuilds', \`\${process.platform}-\${process.arch}\`, 'addon.node'), new URL('./addon.node', import.meta.url))`);
298
+ }
299
+ return warnings;
300
+ }
247
301
  /**
248
302
  * Resolve an import as esbuild does
249
303
  * @param build
@@ -27,6 +27,14 @@ type ScriptPath = {
27
27
  */
28
28
  base?: string;
29
29
  };
30
+ /**
31
+ * The packages sharing the framework and holding addons, bundled with the application: the package of a file, the
32
+ * native files of one of its directories (shipped by the paths to its own directories)
33
+ */
34
+ export type AddonPackages = {
35
+ packageOf(file: string): string | undefined;
36
+ nativeFilesOf(root: string, directory: string): string[];
37
+ };
30
38
  /**
31
39
  * What a path from a module gives: the directory of its pattern, the parts from it, the scripts to bundle and the files
32
40
  * to copy (the files of its directories of data), or why it cannot be shipped
@@ -54,6 +62,7 @@ type Targets = {
54
62
  * paths their users give)
55
63
  */
56
64
  export declare class FileCollector {
65
+ protected readonly addons?: AddonPackages | undefined;
57
66
  /**
58
67
  * The scripts: their paths in the native tree, and their files
59
68
  */
@@ -71,8 +80,9 @@ export declare class FileCollector {
71
80
  /**
72
81
  * @param project The directory of the project: the scripts are named from it (as the modules the bundler gives, its
73
82
  * real path)
83
+ * @param addons The packages sharing the framework and holding addons
74
84
  */
75
- constructor(project: string);
85
+ constructor(project: string, addons?: AddonPackages | undefined);
76
86
  /**
77
87
  * The key of a directory in the tree ("/" separated): its path from the project; outside of the project (the
78
88
  * packages of a workspace, linked), its path from the node_modules holding it, else a hash of its path (the paths of
@@ -121,10 +121,32 @@ export declare class NativeCollector {
121
121
  }[];
122
122
  /**
123
123
  * Whether a package is left out of the bundle: it holds addons, or one of its dependencies does (it loads them,
124
- * with computed paths often), and it is not the project
124
+ * with computed paths often), and it is neither the project nor a package sharing the framework
125
125
  * @param root
126
126
  */
127
127
  protected isNative(root: string | undefined): root is string;
128
+ /**
129
+ * The package sharing the framework and holding addons a file belongs to: bundled with the application, the paths
130
+ * to its own directories ship their native files (its addons, its manifest, its data)
131
+ * @param file
132
+ * @returns its directory, undefined for a file of another package
133
+ */
134
+ addonPackageOf(file: string): string | undefined;
135
+ /**
136
+ * The native files of a directory of a package holding addons: the files node loads at run time (its addons, its
137
+ * libraries, its manifest, its data), without its scripts (bundled) and what node never loads
138
+ * @param root the directory of the package
139
+ * @param directory
140
+ */
141
+ nativeFilesOf(root: string, directory: string): string[];
142
+ /**
143
+ * The addons of the packages sharing the framework bundled with the application which are not shipped: none of
144
+ * their addons is loaded by a require or a path the bundler follows (they would not be found at run time)
145
+ * @param inputs the files bundled
146
+ * @param shipped the files of the tree (the addons loaded by a require, the files read by a path from a module)
147
+ * @returns the warnings, one by package
148
+ */
149
+ unshippedAddons(inputs: Iterable<string>, shipped: Iterable<string>): string[];
128
150
  /**
129
151
  * Resolve an import as esbuild does
130
152
  * @param build
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lakutata/builder",
3
- "version": "3.1.0",
3
+ "version": "3.1.3",
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
  "keywords": [
6
6
  "lakutata",