@lakutata/builder 3.1.1 → 3.1.5

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 CHANGED
@@ -6,7 +6,7 @@ The builder for lakutata applications. The `package bundle`, `package exe` and `
6
6
  `@lakutata/cli` use it, and a project's scripts can call it directly:
7
7
 
8
8
  ```js
9
- import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
9
+ import {buildExecutable, buildInstaller, buildNwjsApp, bundleProject} from '@lakutata/builder'
10
10
 
11
11
  //dist/<name>.cjs, run by node, with its native addons and its resources next to it
12
12
  await bundleProject({project: process.cwd()})
@@ -16,6 +16,8 @@ await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directo
16
16
  //dist/installer: the installers for the current platform (deb, rpm, apk, archlinux, tar.gz on Linux; pkg, tar.gz on
17
17
  //macOS; nsis, msi, zip on Windows), which also register the application's service when it declares one
18
18
  await buildInstaller({project: process.cwd()})
19
+ //An NW.js application (its version in lakutata.nwjs): dist/nwjs/<name>-<build>, the runtime of NW.js named after it
20
+ await buildNwjsApp({project: process.cwd()})
19
21
  ```
20
22
 
21
23
  The builder works on the compiled application: build the project with `tsc` first, since `tsc` emits the decorator
@@ -29,6 +31,14 @@ result. At run time, `resourcesPath()` from lakutata locates the application's r
29
31
  `lakutata package bundle`, `lakutata package exe` and `lakutata package installer` in the `@lakutata/cli`
30
32
  documentation.
31
33
 
34
+ The NW.js applications run in the version of NW.js of their `lakutata.nwjs`. `nwjsReleases()` lists the releases of
35
+ NW.js, `downloadNwjs()` downloads a runtime into the cache (checked against the checksums of its release), and
36
+ `stageNwjsApp()` makes the application for NW.js: its bundle for the Node.js of that NW.js, its native addons made to
37
+ load in it (built again for its architecture, or with its headers when they use the V8 API; on Windows, given the
38
+ Node.js of NW.js), its resources and the manifest of NW.js. `buildNwjsApp()` puts it into the normal flavor of NW.js
39
+ named after the application, and `buildInstaller()` makes its installers with `"runtime": "nwjs"` (the installers of a
40
+ desktop application: dmg on macOS, nsis on Windows, deb on Linux by default).
41
+
32
42
  The Linux installers are tested in containers (Docker): `npm run test:installer` builds them on Debian and Alpine, then
33
43
  installs, upgrades and removes them on Debian and Fedora (systemd) and on Alpine (OpenRC). The macOS installers are
34
44
  installed in a macOS virtual machine (`npm run test:installer:mac`, with tart), and the Windows installers on Windows,
package/README.zh-CN.md CHANGED
@@ -5,7 +5,7 @@
5
5
  lakutata 应用的打包工具。`@lakutata/cli` 的 `package bundle`、`package exe` 和 `package installer` 命令都靠它完成工作,项目的脚本也可以直接调用它:
6
6
 
7
7
  ```js
8
- import {buildExecutable, buildInstaller, bundleProject} from '@lakutata/builder'
8
+ import {buildExecutable, buildInstaller, buildNwjsApp, bundleProject} from '@lakutata/builder'
9
9
 
10
10
  //dist/<name>.cjs,由 node 运行,原生插件和资源放在它旁边
11
11
  await bundleProject({project: process.cwd()})
@@ -15,10 +15,14 @@ await buildExecutable({project: process.cwd(), runtime: 'node', layout: 'directo
15
15
  //dist/installer:当前平台的安装包(Linux 上是 deb、rpm、apk、archlinux、tar.gz;macOS 上是 pkg、tar.gz;
16
16
  //Windows 上是 nsis、msi、zip),应用声明了服务时,安装包还会注册这个服务
17
17
  await buildInstaller({project: process.cwd()})
18
+ //NW.js 应用(版本在 lakutata.nwjs 中):dist/nwjs/<name>-<build>,以应用命名的 NW.js 运行时
19
+ await buildNwjsApp({project: process.cwd()})
18
20
  ```
19
21
 
20
22
  打包工具处理的是编译后的应用:请先用 `tsc` 构建项目,因为装饰器的元数据是由 `tsc` 生成的。单文件包是一个 CommonJS 文件,由 esbuild 生成。原生插件不能打进单文件包,所以它们连同运行时加载的内容会一起收集到一个 native 目录树中。打包器跟踪不到应用按路径读取的文件,这些文件也放在这个目录树中:单独运行的脚本(worker、worker 池、`fork()`、`require(path.join(__dirname, 'x.js'))`)会各自打包,数据文件和目录则直接复制。打包工具会找出由模块位置构造的路径,并改写它们;无法一起发布的内容会列在结果的 `warnings` 中。运行时,lakutata 的 `resourcesPath()` 负责找到应用的资源,这些资源在应用 `package.json` 的 `lakutata.resources` 中声明。安装包则由同一个文件中的 `lakutata.installer` 配置。详见 `@lakutata/cli` 文档中的 `lakutata package bundle`、`lakutata package exe` 和 `lakutata package installer`。
21
23
 
24
+ NW.js 应用运行在它 `lakutata.nwjs` 中指定的 NW.js 版本里。`nwjsReleases()` 列出 NW.js 的发布版本,`downloadNwjs()` 把一个运行时下载到缓存目录(按其发布版本的校验和核对),`stageNwjsApp()` 为 NW.js 准备应用:为这个 NW.js 的 Node.js 打包的代码、处理成能在其中加载的原生插件(为它的架构重新构建,使用 V8 API 的插件用它的头文件重新构建;在 Windows 上让插件加载 NW.js 的 Node.js)、资源和 NW.js 的清单。`buildNwjsApp()` 把应用放进以应用命名的 NW.js normal 版本中,`buildInstaller()` 在 `"runtime": "nwjs"` 时制作它的安装包(桌面应用的安装包:默认 macOS 上是 dmg,Windows 上是 nsis,Linux 上是 deb)。
25
+
22
26
  Linux 安装包在容器(Docker)中测试:`npm run test:installer` 先在 Debian 和 Alpine 上制作安装包,再在 Debian、Fedora(systemd)和 Alpine(OpenRC)上安装、升级和卸载它们。macOS 安装包在 macOS 虚拟机中测试安装(`npm run test:installer:mac`,使用 tart),Windows 安装包则在 Windows 上以管理员身份测试安装(`LAKUTATA_WINDOWS_INSTALL=1 npm run test:installer:windows`)。
23
27
 
24
28
  ## 另见
package/dist/esm/index.js CHANGED
@@ -11,3 +11,6 @@ export { INSTALLER_HOOKS, NATIVE_HOOKS } from './lib/Scripts.js';
11
11
  export { detectLibc } from './lib/Platform.js';
12
12
  export { WINDOWS_TARGETS } from './lib/win/Windows.js';
13
13
  export { MAC_TARGETS } from './lib/mac/Mac.js';
14
+ export { downloadNwjs, nwjsPlatform, nwjsRelease, nwjsReleases } from './lib/nwjs/Runtime.js';
15
+ export { stageNwjsApp } from './lib/nwjs/Stage.js';
16
+ export { buildNwjsApp } from './lib/nwjs/Package.js';
@@ -1,5 +1,6 @@
1
- import { cp, rm, stat } from 'node:fs/promises';
1
+ import { rm, stat } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { copyDirectory } from './Directories.js';
3
4
  import { runCommand } from './Processes.js';
4
5
  /**
5
6
  * The tar.gz archive of the directory of the executable (its native addons and its resources next to it), named after
@@ -12,7 +13,7 @@ import { runCommand } from './Processes.js';
12
13
  export async function archiveDirectory(plan, arch, work, outdir) {
13
14
  const base = `${plan.name}-${plan.version}-${process.platform}-${arch}`;
14
15
  await rm(path.join(work, base), { recursive: true, force: true });
15
- await cp(plan.directory, path.join(work, base), { recursive: true, verbatimSymlinks: true });
16
+ await copyDirectory(plan.directory, path.join(work, base));
16
17
  const file = path.join(outdir, `${base}.tar.gz`);
17
18
  const archived = await runCommand('tar', ['-czf', file, base], work);
18
19
  if (archived.code !== 0)
@@ -25,6 +25,7 @@ const IMPORT_META_URL = '__lakutataImportMetaUrl';
25
25
  * arguments of the CommonJS modules: its file is the executable (as in the executables of Node.js), its require loads
26
26
  * the modules of node and the ones of a node_modules next to the executable (the ones left out of the bundle)
27
27
  */
28
+ //node-compat: process.getBuiltinModule (Bun only)
28
29
  const BUN_PRELUDE = 'var __filename=process.execPath,__dirname=process.getBuiltinModule(\'node:path\').dirname(__filename),require=typeof require===\'function\'?require:process.getBuiltinModule(\'node:module\').createRequire(__filename);';
29
30
  /**
30
31
  * The files of the resources of an application, declared by the "lakutata.resources" of its package.json: their paths in
@@ -98,7 +99,7 @@ export async function bundleProject(options) {
98
99
  throw new Error(`The entry ${path.relative(process.cwd(), entry) || entry} does not exist: build the project first`);
99
100
  const outfile = path.resolve(project, options.outfile ?? path.join('dist', `${name}.cjs`));
100
101
  const collector = new NativeCollector(project, options.runtime);
101
- const files = new FileCollector(project);
102
+ const files = new FileCollector(project, { packageOf: (file) => collector.addonPackageOf(file), nativeFilesOf: (root, directory) => collector.nativeFilesOf(root, directory) });
102
103
  //esbuild is loaded by the commands which need it
103
104
  const { build } = await import('esbuild');
104
105
  //The application, and each script of its workers (the bundler does not follow new Worker()): their native packages
@@ -111,7 +112,7 @@ export async function bundleProject(options) {
111
112
  metafile: true,
112
113
  bundle: true,
113
114
  platform: 'node',
114
- target: `node${process.versions.node.split('.')[0]}`,
115
+ target: `node${options.node?.replace(/^v/, '') ?? process.versions.node.split('.')[0]}`,
115
116
  format: 'cjs',
116
117
  minify: options.minify ?? true,
117
118
  //The names of the classes are the ones of the objects of the container and of the errors
@@ -147,9 +148,15 @@ export async function bundleProject(options) {
147
148
  const resourcesDirectory = path.resolve(path.dirname(outfile), options.directories?.resources ?? `${base}.resources`);
148
149
  //The scripts of the workers, until the ones they start are bundled too
149
150
  const scripts = new Map();
151
+ //The files of every bundle, the application's and its workers'
152
+ const inputs = new Set(application.inputs);
150
153
  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);
154
+ for (const [at, script] of pending) {
155
+ const bundled = await bundle(script, path.join(nativeDirectory, ...at.split('/')));
156
+ scripts.set(at, bundled.code);
157
+ for (const input of bundled.inputs)
158
+ inputs.add(input);
159
+ }
153
160
  }
154
161
  files.warnBundled(application.inputs);
155
162
  const embedded = options.native === 'embedded';
@@ -221,6 +228,6 @@ export async function bundleProject(options) {
221
228
  resourcesDirectory: resources.files.length && !embedded ? resourcesDirectory : undefined,
222
229
  workers: [...new Set([...scripts.keys()].map((at) => files.nameOf(files.scripts.get(at))))].sort(),
223
230
  files: [...files.roots].filter((root) => !files.scriptPaths().has(root)).map((root) => files.nameOf(root)).sort(),
224
- warnings: files.warnings
231
+ warnings: [...files.warnings, ...collector.unshippedAddons(inputs, files.files.values())]
225
232
  };
226
233
  }
@@ -0,0 +1,18 @@
1
+ import { cp, mkdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { runCommand } from './Processes.js';
4
+ /**
5
+ * Copy a directory into another one, its links kept as they are: cp -a on macOS and Linux (the copy of Node.js turns
6
+ * the relative links into absolute ones before Node.js 18.17 and 20.2: its option verbatimSymlinks), the copy of Node.js
7
+ * on Windows (its directories have no links)
8
+ * @param source
9
+ * @param target
10
+ */
11
+ export async function copyDirectory(source, target) {
12
+ if (process.platform === 'win32')
13
+ return await cp(source, target, { recursive: true });
14
+ await mkdir(target, { recursive: true });
15
+ const copied = await runCommand('cp', ['-a', `${source}/.`, target], path.dirname(target));
16
+ if (copied.code !== 0)
17
+ throw new Error(`${source} cannot be copied: ${copied.output.trim()}`);
18
+ }
@@ -76,14 +76,31 @@ async function writeIcon(work, icon) {
76
76
  return file;
77
77
  }
78
78
  /**
79
- * Embed a bundle in the Node.js binary running the tool (the single executable applications of Node.js)
79
+ * The node program embedded in an executable and its version: the one given, else the one running the tool. It must
80
+ * have the assets of the single executable applications (Node.js 20.12, 21.7 or later): node:sea and its getAsset.
81
+ * @param program
82
+ */
83
+ async function nodeOfExecutable(program) {
84
+ const node = program ? path.resolve(program) : process.execPath;
85
+ //node-compat: node:sea (the probe, run by the node program, loads it in a try)
86
+ const probe = await runCommand(node, ['-e', "let assets=false;try{assets=typeof require('node:sea').getAsset==='function'}catch{}process.stdout.write(process.version+' '+assets)"], process.cwd()).catch((error) => ({ code: 1, output: error.message }));
87
+ const [version, assets] = probe.output.trim().split(' ');
88
+ if (probe.code !== 0 || !/^v\d+\.\d+\.\d+$/.test(version))
89
+ throw new Error(`${node} cannot be run as the node program of the executable: ${probe.output.trim()}`);
90
+ if (assets !== 'true')
91
+ throw new Error(`The executables of Node.js need the assets of its single executable applications (Node.js 20.12, 21.7 or later): ${node} is Node.js ${version}. Give a newer node program (nodeProgram, --node-program). Or make the executable with Bun (--runtime bun)`);
92
+ return { path: node, version: version };
93
+ }
94
+ /**
95
+ * Embed a bundle in a Node.js binary (the single executable applications of Node.js)
80
96
  * @param bundle
81
97
  * @param work
82
98
  * @param file
83
99
  * @param warnings
100
+ * @param node the node program embedded
84
101
  * @param icon the icon of the application (ico, Windows)
85
102
  */
86
- async function embedInNode(bundle, work, file, warnings, icon) {
103
+ async function embedInNode(bundle, work, file, warnings, node, icon) {
87
104
  const assets = {};
88
105
  if (bundle.native) {
89
106
  //The files of the native packages, extracted by the executable at its first run
@@ -104,9 +121,9 @@ async function embedInNode(bundle, work, file, warnings, icon) {
104
121
  useSnapshot: false,
105
122
  assets: assets
106
123
  }));
107
- await must(process.execPath, ['--experimental-sea-config', 'sea-config.json'], work);
124
+ await must(node.path, ['--experimental-sea-config', 'sea-config.json'], work);
108
125
  const { inject } = await import('postject');
109
- await copyFile(process.execPath, file);
126
+ await copyFile(node.path, file);
110
127
  //The icon of the application replaces the one of node.exe, before the bundle is embedded
111
128
  if (icon)
112
129
  await setExecutableIcon(file, icon);
@@ -119,7 +136,7 @@ async function embedInNode(bundle, work, file, warnings, icon) {
119
136
  //The Authenticode signature of node.exe does not match the executable anymore
120
137
  if (process.platform === 'win32')
121
138
  warnings.push(`${path.basename(file)} has the signature of Node.js, which is not valid anymore: sign it (signtool) before distributing it`);
122
- return `Node.js ${process.version}`;
139
+ return `Node.js ${node.version}`;
123
140
  }
124
141
  /**
125
142
  * Compile a bundle into an executable of Bun, in bytecode: the files of the native tree are embedded beside it, the
@@ -189,6 +206,8 @@ export async function buildExecutable(options) {
189
206
  const outdir = path.resolve(project, options.outdir ?? 'dist');
190
207
  //Bun is found before the application is bundled
191
208
  const bun = options.runtime === 'bun' ? await findBun(project) : undefined;
209
+ //The node program embedded, checked before the application is bundled
210
+ const node = bun ? undefined : await nodeOfExecutable(options.nodeProgram);
192
211
  const work = await mkdtemp(path.join(os.tmpdir(), 'lakutata-executable-'));
193
212
  try {
194
213
  const directory = options.layout === 'directory';
@@ -200,6 +219,8 @@ export async function buildExecutable(options) {
200
219
  await rm(target, { recursive: true, force: true });
201
220
  await mkdir(target, { recursive: true });
202
221
  const bundle = await bundleProject({
222
+ //The syntax of the node embedded
223
+ ...node ? { node: node.version } : {},
203
224
  ...options,
204
225
  outfile: path.join(work, 'main.cjs'),
205
226
  native: directory ? 'directory' : 'embedded',
@@ -225,7 +246,7 @@ export async function buildExecutable(options) {
225
246
  //The icon of the application: the one of the executables of Windows (the ones of macOS and Linux have none)
226
247
  const application = applicationIcon(project, manifest);
227
248
  const icon = application && process.platform === 'win32' ? icoFile(application.image) : undefined;
228
- const runtime = bun ? await compileWithBun(bun, embedded, work, made, icon) : await embedInNode(embedded, work, made, warnings, icon);
249
+ const runtime = bun ? await compileWithBun(bun, embedded, work, made, icon) : await embedInNode(embedded, work, made, warnings, node, icon);
229
250
  await copyFile(made, file);
230
251
  await chmod(file, 0o755);
231
252
  return {
@@ -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);
@@ -3,6 +3,7 @@ import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { programName } from './Bundle.js';
5
5
  import { buildElectronInstaller } from './electron/Electron.js';
6
+ import { buildNwjsApp } from './nwjs/Package.js';
6
7
  import { buildExecutable } from './Executable.js';
7
8
  import { applicationIcon, icoFile } from './Icons.js';
8
9
  import { detectLibc } from './Platform.js';
@@ -51,10 +52,12 @@ export async function buildInstaller(options) {
51
52
  if (process.platform !== 'linux' && !mac && !windows)
52
53
  throw new Error(`The installers of ${process.platform} are not supported: the ones of Linux are made on Linux, the ones of macOS on macOS, the ones of Windows on Windows`);
53
54
  const libc = detectLibc();
55
+ //A desktop application (NW.js): its installers by default the ones of the desktop applications (as electron-builder)
56
+ const desktop = config.runtime === 'nwjs';
54
57
  const targets = options.targets?.length ? options.targets
55
- : mac ? config.mac?.targets ?? ['pkg', 'tar.gz']
56
- : windows ? config.win?.targets ?? ['nsis', 'msi', 'zip']
57
- : config.linux?.targets ?? (libc === 'glibc' ? ['deb', 'rpm', 'tar.gz'] : ['apk', 'tar.gz']);
58
+ : mac ? config.mac?.targets ?? (desktop ? ['dmg'] : ['pkg', 'tar.gz'])
59
+ : windows ? config.win?.targets ?? (desktop ? ['nsis'] : ['nsis', 'msi', 'zip'])
60
+ : config.linux?.targets ?? (libc === 'glibc' ? (desktop ? ['deb'] : ['deb', 'rpm', 'tar.gz']) : desktop ? ['tar.gz'] : ['apk', 'tar.gz']);
58
61
  for (const target of targets) {
59
62
  if (mac) {
60
63
  if (!MAC_TARGETS.includes(target))
@@ -91,6 +94,8 @@ export async function buildInstaller(options) {
91
94
  if (!/^[A-Za-z0-9][A-Za-z0-9.-]*$/.test(appId))
92
95
  throw new Error(`The identifier of the application "${appId}" must be letters, digits, "." and "-" (lakutata.installer.appId)`);
93
96
  const service = config.service === true ? {} : config.service || undefined;
97
+ if (desktop && service)
98
+ throw new Error('A desktop application (lakutata.installer.runtime nwjs) has no service (lakutata.installer.service)');
94
99
  if (service?.name !== undefined && !/^[A-Za-z0-9_.@-]+$/.test(service.name))
95
100
  throw new Error(`The name of the service "${service.name}" must be letters, digits, "_", ".", "@" and "-"`);
96
101
  //A user of the system (Linux, macOS), an account of Windows (DOMAIN\user, NT SERVICE\name, a managed account ending in $)
@@ -108,35 +113,42 @@ export async function buildInstaller(options) {
108
113
  icon = path.join(work, 'icon.ico');
109
114
  await writeFile(icon, icoFile(application.image));
110
115
  }
111
- const executable = await buildExecutable({
116
+ //The executable and what it loads; or the application of NW.js (its runtime, its executable named after it)
117
+ const nwjs = desktop ? await buildNwjsApp({ project: project, entry: options.entry, minify: options.minify, external: options.external, arch: options.arch, outdir: path.join(work, 'stage') }) : undefined;
118
+ const executable = nwjs ? undefined : await buildExecutable({
112
119
  project: project,
113
120
  entry: options.entry,
114
121
  minify: options.minify,
115
122
  external: options.external,
116
123
  runtime: config.runtime,
124
+ nodeProgram: options.nodeProgram,
117
125
  layout: 'directory',
118
126
  outdir: path.join(work, 'stage')
119
127
  });
120
- warnings.push(...executable.warnings);
128
+ warnings.push(...executable?.warnings ?? nwjs.stage.bundle.warnings);
129
+ //The directory installed: the one of the bundle of macOS (/Applications/<product>.app), the one of the runtime of NW.js
130
+ const installed = nwjs ? (mac ? path.dirname(nwjs.directory) : nwjs.directory) : executable.directory;
131
+ const launcher = nwjs && path.basename(mac ? nwjs.directory : nwjs.executable);
121
132
  const plan = {
122
133
  name: name,
123
134
  appId: appId,
124
135
  productName: config.productName ?? (typeof manifest.productName === 'string' ? manifest.productName : name),
125
136
  program: program,
126
137
  version: version,
127
- description: config.description ?? manifest.description ?? name,
138
+ //An empty description is none (the one of a project created without it): Windows Installer refuses it
139
+ description: config.description || manifest.description || name,
128
140
  maintainer: maintainer,
129
141
  homepage: config.homepage ?? (typeof manifest.homepage === 'string' ? manifest.homepage : undefined),
130
142
  license: config.license ?? (typeof manifest.license === 'string' ? manifest.license : undefined),
131
143
  vendor: config.vendor,
132
- addToPath: config.addToPath ?? true,
144
+ addToPath: config.addToPath ?? !desktop,
133
145
  depends: config.linux?.depends ?? [],
134
146
  recommends: config.linux?.recommends ?? [],
135
147
  section: config.linux?.section,
136
148
  priority: config.linux?.priority,
137
149
  service: service && {
138
150
  name: service.name ?? name,
139
- description: service.description ?? config.description ?? manifest.description ?? name,
151
+ description: service.description || config.description || manifest.description || name,
140
152
  args: service.args ?? [],
141
153
  env: { MODE: 'production', ...service.env },
142
154
  //The users of the services of macOS are hidden by their names (_www), the ones of Windows are virtual
@@ -144,17 +156,19 @@ export async function buildInstaller(options) {
144
156
  restart: service.restart ?? 'on-failure',
145
157
  autoStart: service.autoStart ?? true
146
158
  },
147
- directory: executable.directory,
148
- prefix: mac ? `/usr/local/lib/${name}` : windows ? `C:\\Program Files\\${name}` : `/opt/${name}`,
159
+ directory: installed,
160
+ prefix: mac ? (launcher ? `/Applications/${launcher}` : `/usr/local/lib/${name}`) : windows ? `C:\\Program Files\\${name}` : `/opt/${name}`,
149
161
  scripts: scripts,
150
- icon: icon
162
+ icon: icon,
163
+ arch: nwjs ? nwjs.runtime.platform.slice(nwjs.runtime.platform.indexOf('-') + 1) : process.arch,
164
+ ...launcher ? { desktop: { executable: launcher, icon: application?.file } } : {}
151
165
  };
152
166
  const outdir = path.resolve(project, options.outdir ?? path.join('dist', 'installer'));
153
167
  await mkdir(outdir, { recursive: true });
154
168
  const files = mac ? await buildMacPackages(plan, targets, path.join(work, 'mac'), outdir)
155
169
  : windows ? await buildWindowsPackages(plan, targets, path.join(work, 'win'), outdir)
156
170
  : await buildLinuxPackages(plan, targets, path.join(work, 'linux'), outdir);
157
- return { name: name, version: version, files: files, executable: executable, warnings: warnings };
171
+ return { name: name, version: version, files: files, ...executable ? { executable: executable } : {}, ...nwjs ? { nwjs: nwjs } : {}, warnings: warnings };
158
172
  }
159
173
  finally {
160
174
  await rm(work, { recursive: true, force: true });
@@ -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
@@ -30,8 +30,10 @@ export function lakutataNativeRequire(nodeRequire, config) {
30
30
  const fs = nodeRequire('node:fs');
31
31
  const os = nodeRequire('node:os');
32
32
  const { createRequire, isBuiltin } = nodeRequire('node:module');
33
+ //node-compat: node:sea (a type here; loaded below only where it exists)
33
34
  let sea;
34
35
  try {
36
+ //node-compat: node:sea
35
37
  sea = nodeRequire('node:sea');
36
38
  }
37
39
  catch {