@nwutils/builder 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021-2024 NW.js Utilities
4
+ Copyright (c) 2014-2021 Steffen Müller
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
7
+ this software and associated documentation files (the "Software"), to deal in
8
+ the Software without restriction, including without limitation the rights to
9
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10
+ the Software, and to permit persons to whom the Software is furnished to do so,
11
+ subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # @nwutils/builder
2
+
3
+ Build NW.js applications for Linux, MacOS and Windows: assembles the app source and a cached NW.js runtime into a distributable package, and applies platform-specific metadata (Linux `.desktop` entry, Windows executable resources, MacOS `Info.plist` and helper app bundles).
4
+
5
+ Extracted from [nw-builder](../nw-builder)'s own build implementation so it can be developed, tested and versioned independently, the same way `@nwutils/getter` and `@nwutils/runner` already are. `nw-builder` depends on this package for its `build` mode; most users should keep using `nw-builder` (or `@nwutils/cli`) directly rather than this package on its own.
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@nwutils/builder",
3
+ "version": "0.1.0",
4
+ "description": "Build NW.js applications for Linux, MacOS and Windows.",
5
+ "keywords": [
6
+ "NW.js",
7
+ "node-webkit",
8
+ "Desktop",
9
+ "Application"
10
+ ],
11
+ "author": {
12
+ "name": "Ayushman Chhabra",
13
+ "url": "https://ayushmanchhabra.com"
14
+ },
15
+ "maintainers": [
16
+ {
17
+ "name": "Ayushman Chhabra",
18
+ "url": "https://ayushmanchhabra.com"
19
+ }
20
+ ],
21
+ "contributors": [
22
+ {
23
+ "name": "@nwutils/builder Contributors",
24
+ "url": "https://github.com/nwutils/nw-builder/graphs/contributors"
25
+ }
26
+ ],
27
+ "license": "MIT",
28
+ "main": "./src/main.js",
29
+ "type": "module",
30
+ "files": [
31
+ "LICENSE",
32
+ "src"
33
+ ],
34
+ "homepage": "https://github.com/nwutils/nw-builder",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/nwutils/nw-builder.git"
38
+ },
39
+ "scripts": {
40
+ "lint": "eslint . ../../tests/specs/builder",
41
+ "lint:fix": "eslint . ../../tests/specs/builder --fix",
42
+ "test": "node --test \"../../tests/specs/builder/*.test.js\""
43
+ },
44
+ "dependencies": {
45
+ "archiver": "^8.0.0",
46
+ "glob": "^13.0.6",
47
+ "pe-library": "^2.0.1",
48
+ "plist": "^5.0.0",
49
+ "resedit": "^3.1.0",
50
+ "semver": "^7.8.5",
51
+ "tar": "^7.5.22"
52
+ },
53
+ "devDependencies": {
54
+ "base-volta-off-of-nwjs": "^1.0.5",
55
+ "nw": "^0.115.0",
56
+ "selenium-webdriver": "^4.47.0"
57
+ }
58
+ }
package/src/main.js ADDED
@@ -0,0 +1,389 @@
1
+ import child_process from "node:child_process";
2
+ import console from "node:console";
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import process from "node:process";
6
+
7
+ import { ZipArchive } from "archiver";
8
+ import * as resedit from "resedit";
9
+ // pe-library is a direct dependency of resedit
10
+ import * as peLibrary from "pe-library";
11
+ import * as tar from "tar";
12
+
13
+ import util from "./util.js";
14
+ import setOsxConfig from "./osx.js";
15
+
16
+ /**
17
+ * References:
18
+ * https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html
19
+ * @typedef {object} LinuxRc Linux configuration options
20
+ * @property {string} name Name of the application
21
+ * @property {string} genericName Generic name of the application
22
+ * @property {boolean} noDisplay If true the application is not displayed
23
+ * @property {string} comment Tooltip for the entry, for example "View sites on the Internet".
24
+ * @property {string} icon Icon to display in file manager, menus, etc.
25
+ * @property {boolean} hidden TBD
26
+ * @property {string[]} onlyShowIn A list of strings identifying the desktop environments that should (/not) display a given desktop entry
27
+ * @property {string[]} notShowIn A list of strings identifying the desktop environments that should (/not) display a given desktop entry
28
+ * @property {boolean} dBusActivatable A boolean value specifying if D-Bus activation is supported for this application
29
+ * @property {string} tryExec Path to an executable file on disk used to determine if the program is actually installed
30
+ * @property {string} exec Program to execute, possibly with arguments.
31
+ * @property {string} path If entry is of type Application, the working directory to run the program in.
32
+ * @property {boolean} terminal Whether the program runs in a terminal window.
33
+ * @property {string[]} actions Identifiers for application actions.
34
+ * @property {string[]} mimeType The MIME type(s) supported by this application.
35
+ * @property {string[]} categories Categories in which the entry should be shown in a menu
36
+ * @property {string[]} implements A list of interfaces that this application implements.
37
+ * @property {string[]} keywords A list of strings which may be used in addition to other metadata to describe this entry.
38
+ * @property {boolean} startupNotify If true, it is KNOWN that the application will send a "remove" message when started with the DESKTOP_STARTUP_ID environment variable set. If false, it is KNOWN that the application does not work with startup notification at all.
39
+ * @property {string} startupWMClass If specified, it is known that the application will map at least one window with the given string as its WM class or WM name hin
40
+ * @property {boolean} prefersNonDefaultGPU If true, the application prefers to be run on a more powerful discrete GPU if available.
41
+ * @property {string} singleMainWindow If true, the application has a single main window, and does not support having an additional one opened.
42
+ */
43
+
44
+ /**
45
+ * References:
46
+ * https://developer.apple.com/documentation/bundleresources/information_property_list
47
+ * @typedef {object} OsxRc OSX resource configuration options
48
+ * @property {string} name The name of the application
49
+ * @property {string} icon The path to the icon file. It should be a .icns file.
50
+ * @property {string} LSApplicationCategoryType The category that best describes your app for the App Store.
51
+ * @property {string} CFBundleIdentifier A unique identifier for a bundle usually in reverse DNS format.
52
+ * @property {string} CFBundleName A user-visible short name for the bundle.
53
+ * @property {string} CFBundleDisplayName The user-visible name for the bundle.
54
+ * @property {string} CFBundleSpokenName A replacement for the app name in text-to-speech operations.
55
+ * @property {string} CFBundleVersion The version of the build that identifies an iteration of the bundle.
56
+ * @property {string} CFBundleShortVersionString The release or version number of the bundle.
57
+ * @property {string} NSHumanReadableCopyright A human-readable copyright notice for the bundle.
58
+ * @property {string} NSLocalNetworkUsageDescription A human-readable description of why the application needs access to the local network.
59
+ */
60
+
61
+ /**
62
+ * References:
63
+ * https://learn.microsoft.com/en-us/windows/win32/msi/version
64
+ * https://learn.microsoft.com/en-gb/windows/win32/sbscs/application-manifests
65
+ * https://learn.microsoft.com/en-us/visualstudio/deployment/trustinfo-element-clickonce-application?view=vs-2022#requestedexecutionlevel
66
+ * https://learn.microsoft.com/en-gb/windows/win32/menurc/versioninfo-resource
67
+ * @typedef {object} WinRc Windows configuration options. More info
68
+ * @property {string} name The name of the application
69
+ * @property {string} version The version of the application
70
+ * @property {string} comments Additional information that should be displayed for diagnostic purposes.
71
+ * @property {string} company Company that produced the file—for example, Microsoft Corporation or Standard Microsystems Corporation, Inc. This string is required.
72
+ * @property {string} fileDescription File description to be presented to users. This string may be displayed in a list box when the user is choosing files to install. For example, Keyboard Driver for AT-Style Keyboards. This string is required.
73
+ * @property {string} fileVersion Version number of the file. For example, 3.10 or 5.00.RC2. This string is required.
74
+ * @property {string} icon The path to the icon file. It should be a .ico file.
75
+ * @property {string} internalName Internal name of the file, if one exists—for example, a module name if the file is a dynamic-link library. If the file has no internal name, this string should be the original filename, without extension. This string is required.
76
+ * @property {string} legalCopyright Copyright notices that apply to the file. This should include the full text of all notices, legal symbols, copyright dates, and so on. This string is optional.
77
+ * @property {string} legalTrademark Trademarks and registered trademarks that apply to the file. This should include the full text of all notices, legal symbols, trademark numbers, and so on. This string is optional.
78
+ * @property {string} originalFilename Original name of the file, not including a path. This information enables an application to determine whether a file has been renamed by a user. The format of the name depends on the file system for which the file was created. This string is required.
79
+ * @property {string} privateBuild Information about a private version of the file—for example, Built by TESTER1 on \\TESTBED. This string should be present only if VS_FF_PRIVATEBUILD is specified in the fileflags parameter of the root block.
80
+ * @property {string} productName Name of the product with which the file is distributed. This string is required.
81
+ * @property {string} productVersion Version of the product with which the file is distributed—for example, 3.10 or 5.00.RC2. This string is required.
82
+ * @property {string} specialBuild Text that specifies how this version of the file differs from the standard version—for example, Private build for TESTER1 solving mouse problems on M250 and M250E computers. This string should be present only if VS_FF_SPECIALBUILD is specified in the fileflags parameter of the root block.
83
+ * @property {string} languageCode Language of the file, defined by Microsoft, see: https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/6c085406-a698-4e12-9d4d-c3b0ee3dbc4a
84
+ */
85
+
86
+ /**
87
+ * @typedef {object} BuildOptions
88
+ * @property {string | "latest" | "stable" | "lts"} [version = "latest"] Runtime version
89
+ * @property {"normal" | "sdk"} [flavor = "normal"] Build flavor
90
+ * @property {"linux" | "osx" | "win"} [platform] Target platform
91
+ * @property {"ia32" | "x64" | "arm64"} [arch] Target arch
92
+ * @property {string} [manifestUrl = "https://nwjs.io/versions.json"] Versions manifest URI, https or file path
93
+ * @property {string} [srcDir = "./src"] Source directory
94
+ * @property {string} [cacheDir = "./cache"] Cache directory
95
+ * @property {string} [outDir = "./out"] Out directory
96
+ * @property {LinuxRc | WinRc | OsxRc} [app] Platform specific rc
97
+ * @property {boolean} [glob = true] File globbing
98
+ * @property {boolean | string | object} [managedManifest = false] Manage manifest
99
+ * @property {false | "zip" | "tar" | "tgz"} [zip = false] Compress built artifacts
100
+ * @property {object} [releaseInfo = {}] Version specific release metadata.
101
+ */
102
+
103
+ /**
104
+ * Build NW.js application.
105
+ * @async
106
+ * @function
107
+ * @param {BuildOptions} options - Build options
108
+ * @returns {Promise<void>}
109
+ */
110
+ async function build({
111
+ version = "latest",
112
+ flavor = "normal",
113
+ platform = util.PLATFORM_KV[process.platform],
114
+ arch = util.ARCH_KV[process.arch],
115
+ srcDir = "./src",
116
+ cacheDir = "./cache",
117
+ outDir = "./out",
118
+ app,
119
+ glob = true,
120
+ managedManifest = false,
121
+ zip = false,
122
+ releaseInfo = {},
123
+ }) {
124
+ const nwDir = path.resolve(
125
+ cacheDir,
126
+ `nwjs${flavor === "sdk" ? "-sdk" : ""}-v${version}-${platform}-${arch}`,
127
+ );
128
+
129
+ await fs.promises.rm(outDir, { force: true, recursive: true });
130
+ await fs.promises.cp(nwDir, outDir, {
131
+ recursive: true,
132
+ verbatimSymlinks: true,
133
+ });
134
+
135
+ const files = await util.globFiles({ srcDir, glob });
136
+
137
+ const nwProjectDir = path.resolve(
138
+ outDir,
139
+ platform !== "osx" ? "package.nw" : "nwjs.app/Contents/Resources/app.nw",
140
+ );
141
+
142
+ await fs.promises.mkdir(nwProjectDir, { recursive: true });
143
+
144
+ if (glob) {
145
+ for (let file of files) {
146
+ const stats = await fs.promises.stat(file);
147
+ if (stats.isDirectory()) {
148
+ continue;
149
+ }
150
+ await fs.promises.cp(file, path.resolve(nwProjectDir, file), {
151
+ recursive: true,
152
+ force: true,
153
+ });
154
+ }
155
+ } else {
156
+ await fs.promises.cp(files, path.resolve(nwProjectDir), {
157
+ recursive: true,
158
+ verbatimSymlinks: true,
159
+ });
160
+ }
161
+
162
+ const builtManifest = JSON.parse(
163
+ await fs.promises.readFile(
164
+ path.resolve(nwProjectDir, "package.json"),
165
+ "utf8",
166
+ ),
167
+ );
168
+
169
+ /* Set `product_string` in manifest for MacOS. This is used in renaming the Helper apps. */
170
+ if (platform === "osx") {
171
+ builtManifest.product_string = app.name;
172
+ await fs.promises.writeFile(
173
+ path.resolve(nwProjectDir, "package.json"),
174
+ JSON.stringify(builtManifest, null, 2),
175
+ );
176
+ }
177
+
178
+ if (
179
+ managedManifest === true ||
180
+ typeof managedManifest === "object" ||
181
+ typeof managedManifest === "string"
182
+ ) {
183
+ await manageManifest({
184
+ nwPkg: builtManifest,
185
+ managedManifest,
186
+ outDir,
187
+ platform,
188
+ });
189
+ }
190
+
191
+ if (platform === "linux") {
192
+ await setLinuxConfig({ app, outDir });
193
+ } else if (platform === "win") {
194
+ await setWinConfig({ app, outDir });
195
+ } else if (platform === "osx") {
196
+ await setOsxConfig({ version, app, outDir, releaseInfo });
197
+ }
198
+
199
+ if (zip !== false) {
200
+ await compress({ zip, outDir });
201
+ }
202
+ }
203
+
204
+ const manageManifest = async ({ nwPkg, managedManifest, outDir, platform }) => {
205
+ let manifest = undefined;
206
+
207
+ if (managedManifest === true) {
208
+ manifest = nwPkg;
209
+ }
210
+
211
+ if (typeof managedManifest === "object") {
212
+ manifest = managedManifest;
213
+ }
214
+
215
+ if (typeof managedManifest === "string") {
216
+ manifest = JSON.parse(await fs.promises.readFile(managedManifest));
217
+ }
218
+
219
+ if (manifest.devDependencies) {
220
+ manifest.devDependencies = undefined;
221
+ }
222
+ manifest.packageManager = manifest.packageManager ?? "npm@*";
223
+
224
+ await fs.promises.writeFile(
225
+ path.resolve(
226
+ outDir,
227
+ platform !== "osx" ? "package.nw" : "nwjs.app/Contents/Resources/app.nw",
228
+ "package.json",
229
+ ),
230
+ JSON.stringify(manifest, null, 2),
231
+ "utf8",
232
+ );
233
+
234
+ const cwd = path.resolve(
235
+ outDir,
236
+ platform !== "osx" ? "package.nw" : "nwjs.app/Contents/Resources/app.nw",
237
+ );
238
+
239
+ if (manifest.packageManager.startsWith("npm")) {
240
+ child_process.execSync("npm install", { cwd });
241
+ } else if (manifest.packageManager.startsWith("yarn")) {
242
+ child_process.execSync("yarn install", { cwd });
243
+ } else if (manifest.packageManager.startsWith("pnpm")) {
244
+ child_process.execSync("pnpm install", { cwd });
245
+ }
246
+ };
247
+
248
+ const setLinuxConfig = async ({ app, outDir }) => {
249
+ if (process.platform === "win32") {
250
+ console.warn(
251
+ "Linux apps built on Windows platform do not preserve all file permissions. See #716",
252
+ );
253
+ }
254
+ let desktopEntryFile = {
255
+ Type: "Application",
256
+ Version: "1.5",
257
+ Name: app.name,
258
+ GenericName: app.genericName,
259
+ NoDisplay: app.noDisplay,
260
+ Comment: app.comment,
261
+ Icon: app.icon ? path.resolve(outDir, "package.nw", app.icon) : "",
262
+ Hidden: app.hidden,
263
+ OnlyShowIn: app.onlyShowIn,
264
+ NotShowIn: app.notShowIn,
265
+ DBusActivatable: app.dBusActivatable,
266
+ TryExec: app.tryExec,
267
+ Exec: app.exec,
268
+ Path: app.path,
269
+ Terminal: app.terminal,
270
+ Actions: app.actions,
271
+ MimeType: app.mimeType,
272
+ Categories: app.categories,
273
+ Implements: app.implements,
274
+ Keywords: app.keywords,
275
+ StartupNotify: app.startupNotify,
276
+ StartupWMClass: app.startupWMClass,
277
+ PrefersNonDefaultGPU: app.prefersNonDefaultGPU,
278
+ SingleMainWindow: app.singleMainWindow,
279
+ };
280
+
281
+ await fs.promises.rename(`${outDir}/nw`, `${outDir}/${app.name}`);
282
+
283
+ let fileContent = "[Desktop Entry]\n";
284
+ Object.keys(desktopEntryFile).forEach((key) => {
285
+ if (desktopEntryFile[key] !== undefined) {
286
+ fileContent += `${key}=${desktopEntryFile[key]}\n`;
287
+ }
288
+ });
289
+ let filePath = `${outDir}/${app.name}.desktop`;
290
+ await fs.promises.writeFile(filePath, fileContent);
291
+ };
292
+
293
+ const setWinConfig = async ({ app, outDir }) => {
294
+ let versionString = {
295
+ Comments: app.comments,
296
+ CompanyName: app.company,
297
+ FileDescription: app.fileDescription,
298
+ FileVersion: app.fileVersion,
299
+ InternalName: app.internalName,
300
+ LegalCopyright: app.legalCopyright,
301
+ LegalTrademarks: app.legalTrademark,
302
+ OriginalFilename: app.originalFilename,
303
+ PrivateBuild: app.privateBuild,
304
+ ProductName: app.productName,
305
+ ProductVersion: app.productVersion,
306
+ SpecialBuild: app.specialBuild,
307
+ };
308
+
309
+ Object.keys(versionString).forEach((option) => {
310
+ if (versionString[option] === undefined) {
311
+ delete versionString[option];
312
+ }
313
+ });
314
+
315
+ const outDirAppExe = path.resolve(outDir, `${app.name}.exe`);
316
+ await fs.promises.rename(path.resolve(outDir, "nw.exe"), outDirAppExe);
317
+ const exe = peLibrary.NtExecutable.from(
318
+ await fs.promises.readFile(outDirAppExe),
319
+ );
320
+ const res = peLibrary.NtExecutableResource.from(exe);
321
+ // English (United States)
322
+ const EN_US = 1033;
323
+ if (app.icon) {
324
+ const iconBuffer = await fs.promises.readFile(path.resolve(app.icon));
325
+ const iconFile = resedit.Data.IconFile.from(iconBuffer);
326
+ const iconGroupIDs = resedit.Resource.IconGroupEntry.fromEntries(
327
+ res.entries,
328
+ ).map((entry) => entry.id);
329
+ resedit.Resource.IconGroupEntry.replaceIconsForResource(
330
+ res.entries,
331
+ /* Should be `IDR_MAINFRAME` */
332
+ iconGroupIDs[0],
333
+ EN_US,
334
+ iconFile.icons.map((i) => i.data),
335
+ );
336
+ }
337
+ const [vi] = resedit.Resource.VersionInfo.fromEntries(res.entries);
338
+ if (app.languageCode !== EN_US) {
339
+ res.removeResourceEntry(16, 1, EN_US);
340
+ vi.removeAllStringValues({
341
+ lang: EN_US,
342
+ codepage: 1200,
343
+ });
344
+ vi.lang = app.languageCode;
345
+ }
346
+ vi.setFileVersion(app.fileVersion, app.languageCode);
347
+ vi.setProductVersion(app.productVersion, app.languageCode);
348
+ vi.setStringValues(
349
+ {
350
+ lang: app.languageCode,
351
+ codepage: 1200,
352
+ },
353
+ versionString,
354
+ );
355
+ vi.outputToResourceEntries(res.entries);
356
+ res.outputResource(exe);
357
+ const outBuffer = Buffer.from(exe.generate());
358
+ await fs.promises.writeFile(outDirAppExe, outBuffer);
359
+ };
360
+
361
+ const compress = async ({ zip, outDir }) => {
362
+ if (zip === true || zip === "zip") {
363
+ const archive = new ZipArchive();
364
+ const writeStream = fs.createWriteStream(`${outDir}.zip`);
365
+ archive.pipe(writeStream);
366
+ archive.directory(outDir, false);
367
+ await archive.finalize();
368
+ } else if (zip === "tar") {
369
+ await tar.create(
370
+ {
371
+ gzip: false,
372
+ file: `${outDir}.tar`,
373
+ },
374
+ [outDir],
375
+ );
376
+ } else if (zip === "tgz") {
377
+ await tar.create(
378
+ {
379
+ gzip: true,
380
+ file: `${outDir}.tgz`,
381
+ },
382
+ [outDir],
383
+ );
384
+ }
385
+
386
+ await fs.promises.rm(outDir, { recursive: true, force: true });
387
+ };
388
+
389
+ export default build;
package/src/osx.js ADDED
@@ -0,0 +1,228 @@
1
+ import console from "node:console";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import process from "node:process";
5
+
6
+ import { build, parse } from "plist";
7
+ import semver from "semver";
8
+
9
+ /**
10
+ * Function to update Helper App Plist Files
11
+ * @param {string} plistPath - Path to Helper App Plist File
12
+ * @param {string} helperName - Helper App Name
13
+ * @param {string} helperId - Helper App ID
14
+ * @param {string} appCFBundleIdentifier - options.app.CFBundleIdentifier
15
+ * @param {string} LSFileQuarantineEnabled - options.app.LSFileQuarantineEnabled
16
+ */
17
+ async function updateHelperPlist(
18
+ plistPath,
19
+ helperName,
20
+ helperId,
21
+ appCFBundleIdentifier,
22
+ LSFileQuarantineEnabled,
23
+ ) {
24
+ const plistFullPath = path.resolve(plistPath, "Contents/Info.plist");
25
+ const plistJson = parse(await fs.promises.readFile(plistFullPath, "utf-8"));
26
+ plistJson.CFBundleDisplayName = helperName;
27
+ plistJson.CFBundleName = helperName;
28
+ plistJson.CFBundleExecutable = helperName;
29
+ plistJson.CFBundleIdentifier = `${appCFBundleIdentifier}.${helperId}`;
30
+ plistJson.LSFileQuarantineEnabled = LSFileQuarantineEnabled;
31
+ await fs.promises.writeFile(plistFullPath, build(plistJson));
32
+ }
33
+
34
+ /**
35
+ *
36
+ * @param {object} options - Options.
37
+ * @param {string} options.version - NW.js version.
38
+ * @param {object} options.app - Application configuration.
39
+ * @param {string} options.outDir - Output directory.
40
+ * @param {string} options.releaseInfo - Release information.
41
+ * @returns {Promise<void>} - Promise.
42
+ */
43
+ export default async function setOsxConfig({
44
+ version,
45
+ app,
46
+ outDir,
47
+ releaseInfo,
48
+ }) {
49
+ if (process.platform === "win32") {
50
+ console.warn(
51
+ "MacOS apps built on Windows platform do not preserve all file permissions. See https://github.com/nwutils/nw-builder/issues/716",
52
+ );
53
+ }
54
+
55
+ try {
56
+ /**
57
+ * @type {string | null}
58
+ */
59
+ const chromiumVersion = releaseInfo?.components?.chromium;
60
+ if (!chromiumVersion) {
61
+ throw new Error("Chromium version is missing.");
62
+ }
63
+ /**
64
+ * Path to MacOS application.
65
+ * @type {string}
66
+ */
67
+ const nwjsApp = path.resolve(outDir, "nwjs.app");
68
+
69
+ /**
70
+ * Path to renamed MacOS application.
71
+ * @type {string}
72
+ */
73
+ const outApp = path.resolve(outDir, `${app.name}.app`);
74
+
75
+ /* Rename `nwjs.app` to `${app.name}.app` */
76
+ await fs.promises.rename(nwjsApp, outApp);
77
+
78
+ /* Rename `Contents/MacOS/nwjs` to `Contents/MacOS/${app.name}` */
79
+ await fs.promises.rename(
80
+ path.resolve(outApp, "Contents", "MacOS", "nwjs"),
81
+ path.resolve(outApp, "Contents", "MacOS", app.name),
82
+ );
83
+
84
+ /* Rename all Helper apps */
85
+ const helperBaseDir = path.resolve(
86
+ outApp,
87
+ "Contents/Frameworks/nwjs Framework.framework/Versions",
88
+ /* TODO: Version mismatch tracked in https://github.com/nwjs/nw.js/issues/8371 */
89
+ "Current",
90
+ "Helpers",
91
+ );
92
+
93
+ const helperApps = [
94
+ { name: "nwjs Helper (Alerts).app", id: "helper.alert" },
95
+ { name: "nwjs Helper (GPU).app", id: "helper.gpu" },
96
+ { name: "nwjs Helper (Renderer).app", id: "helper.renderer" },
97
+ { name: "nwjs Helper.app", id: "helper" },
98
+ ];
99
+
100
+ /* MacOS Plugin Helper is removed in NW.js v0.111.0 (Chromium M148) */
101
+ if (semver.lt(version, "0.111.0")) {
102
+ helperApps.push({
103
+ name: "nwjs Helper (Plugin).app",
104
+ id: "helper.plugin",
105
+ });
106
+ }
107
+
108
+ for (const helperApp of helperApps) {
109
+ const newHelperAppName = helperApp.name.replace(/^nwjs/, app.name);
110
+ const oldPath = path.resolve(helperBaseDir, helperApp.name);
111
+ const newPath = path.resolve(helperBaseDir, newHelperAppName);
112
+
113
+ // Rename Helper base directory
114
+ await fs.promises.rename(oldPath, newPath);
115
+
116
+ // Rename Helper sub-directory
117
+ const helperBaseName = helperApp.name.replace(/.app$/, "");
118
+ const subPathBase = path.resolve(newPath, "Contents/MacOS/");
119
+ const oldSubPath = path.resolve(subPathBase, helperBaseName);
120
+ const newSubPath = path.resolve(
121
+ subPathBase,
122
+ helperBaseName.replace(/^nwjs/, app.name),
123
+ );
124
+ await fs.promises.rename(oldSubPath, newSubPath);
125
+
126
+ // Update Helper Plist file
127
+ await updateHelperPlist(
128
+ newPath,
129
+ newHelperAppName.replace(/.app$/, ""),
130
+ helperApp.id,
131
+ app.CFBundleIdentifier,
132
+ app.LSFileQuarantineEnabled,
133
+ );
134
+ }
135
+
136
+ /* Replace default icon with user defined icon if specified. */
137
+ if (app.icon !== undefined) {
138
+ await fs.promises.copyFile(
139
+ path.resolve(app.icon),
140
+ path.resolve(outApp, "Contents", "Resources", "app.icns"),
141
+ );
142
+ }
143
+
144
+ /**
145
+ * Path to `nwjs.app/Contents/Info.plist`
146
+ * @type {string}
147
+ */
148
+ const contentsInfoPlistPath = path.resolve(
149
+ outApp,
150
+ "Contents",
151
+ "Info.plist",
152
+ );
153
+
154
+ /**
155
+ * Path to `nwjs.app/Contents/Resources/en.lproj/InfoPlist.settings`
156
+ * @type {string}
157
+ */
158
+ const contentsResourcesEnLprojInfoPlistStringsPath = path.resolve(
159
+ outApp,
160
+ "Contents",
161
+ "Resources",
162
+ "en.lproj",
163
+ "InfoPlist.strings",
164
+ );
165
+
166
+ /**
167
+ * JSON from `nwjs.app/Contents/Info.plist`
168
+ * @type {object}
169
+ */
170
+ const contentsInfoPlistJson = parse(
171
+ await fs.promises.readFile(contentsInfoPlistPath, "utf-8"),
172
+ );
173
+
174
+ /* Update Plist with user defined values. */
175
+ contentsInfoPlistJson.LSApplicationCategoryType =
176
+ app.LSApplicationCategoryType;
177
+ contentsInfoPlistJson.CFBundleIdentifier = app.CFBundleIdentifier;
178
+ contentsInfoPlistJson.CFBundleName = app.CFBundleName;
179
+ contentsInfoPlistJson.CFBundleDisplayName = app.CFBundleDisplayName;
180
+ contentsInfoPlistJson.CFBundleSpokenName = app.CFBundleSpokenName;
181
+ contentsInfoPlistJson.CFBundleVersion = app.CFBundleVersion;
182
+ contentsInfoPlistJson.CFBundleShortVersionString =
183
+ app.CFBundleShortVersionString;
184
+ contentsInfoPlistJson.CFBundleExecutable = app.name;
185
+ contentsInfoPlistJson.NSLocalNetworkUsageDescription =
186
+ app.NSLocalNetworkUsageDescription;
187
+ /* See https://github.com/nwjs/nw.js/issues/7646 */
188
+ contentsInfoPlistJson.LSFileQuarantineEnabled = app.LSFileQuarantineEnabled;
189
+
190
+ /* Remove properties that were not updated by the user. */
191
+ Object.keys(contentsInfoPlistJson).forEach((option) => {
192
+ if (contentsInfoPlistJson[option] === undefined) {
193
+ delete contentsInfoPlistJson[option];
194
+ }
195
+ });
196
+
197
+ /**
198
+ * Data from `nwjs.app/Contents/Resources/en.lproj/InfoPlist.settings`
199
+ * @type {string[]}
200
+ */
201
+ const contentsResourcesEnLprojInfoPlistStringsArray = (
202
+ await fs.promises.readFile(
203
+ contentsResourcesEnLprojInfoPlistStringsPath,
204
+ "utf-8",
205
+ )
206
+ ).split("\n");
207
+ contentsResourcesEnLprojInfoPlistStringsArray.forEach((line, idx, arr) => {
208
+ if (line.includes("NSHumanReadableCopyright")) {
209
+ arr[idx] =
210
+ `NSHumanReadableCopyright = "${app.NSHumanReadableCopyright}";`;
211
+ }
212
+ });
213
+
214
+ /* Write the updated values to their config files. */
215
+ await fs.promises.writeFile(
216
+ contentsInfoPlistPath,
217
+ build(contentsInfoPlistJson),
218
+ );
219
+ await fs.promises.writeFile(
220
+ contentsResourcesEnLprojInfoPlistStringsPath,
221
+ contentsResourcesEnLprojInfoPlistStringsArray
222
+ .toString()
223
+ .replace(/,/g, "\n"),
224
+ );
225
+ } catch (error) {
226
+ console.error(error);
227
+ }
228
+ }
package/src/util.js ADDED
@@ -0,0 +1,51 @@
1
+ import * as GlobModule from "glob";
2
+
3
+ const PLATFORM_KV = {
4
+ darwin: "osx",
5
+ linux: "linux",
6
+ win32: "win",
7
+ };
8
+
9
+ const ARCH_KV = {
10
+ x64: "x64",
11
+ ia32: "ia32",
12
+ arm64: "arm64",
13
+ };
14
+
15
+ /**
16
+ * Glob files.
17
+ * @async
18
+ * @function
19
+ * @param {object} options - glob file options
20
+ * @param {string | string[]} options.srcDir - app src dir
21
+ * @param {boolean} options.glob - glob flag
22
+ * @returns {Promise<string[]>} - Returns array of file paths
23
+ */
24
+ async function globFiles({ srcDir, glob }) {
25
+ let files;
26
+ let patterns;
27
+ if (glob) {
28
+ files = [];
29
+ if (Array.isArray(srcDir)) {
30
+ patterns = srcDir;
31
+ } else {
32
+ patterns = srcDir.split(" ");
33
+ }
34
+
35
+ const include = patterns.filter((p) => !p.startsWith("!"));
36
+ const ignore = patterns
37
+ .filter((p) => p.startsWith("!"))
38
+ .map((p) => p.substring(1));
39
+ let filePath = await GlobModule.glob(include, { ignore });
40
+ files.push(...filePath);
41
+ } else {
42
+ files = srcDir;
43
+ }
44
+ return files;
45
+ }
46
+
47
+ export default {
48
+ PLATFORM_KV,
49
+ ARCH_KV,
50
+ globFiles,
51
+ };