@nwutils/builder 0.1.4 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nwutils/builder",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Build NW.js applications for Linux, MacOS and Windows.",
5
5
  "keywords": [
6
6
  "NW.js",
@@ -46,13 +46,13 @@
46
46
  "test": "node --test \"../../tests/specs/builder/*.test.js\""
47
47
  },
48
48
  "dependencies": {
49
- "archiver": "^8.0.0",
50
49
  "glob": "^14.0.3",
51
50
  "pe-library": "^2.0.1",
52
51
  "plist": "^5.0.0",
53
52
  "resedit": "^3.1.0",
54
53
  "semver": "^7.8.5",
55
- "tar": "^7.5.22"
54
+ "tar": "^7.5.22",
55
+ "zip-stream": "^7.0.5"
56
56
  },
57
57
  "devEngines": {
58
58
  "runtime": {
package/src/main.js CHANGED
@@ -4,7 +4,6 @@ import fs from "node:fs";
4
4
  import path from "node:path";
5
5
  import process from "node:process";
6
6
 
7
- import { ZipArchive } from "archiver";
8
7
  import * as resedit from "resedit";
9
8
  // pe-library is a direct dependency of resedit
10
9
  import * as peLibrary from "pe-library";
@@ -149,16 +148,7 @@ async function build({
149
148
  await fs.promises.mkdir(nwProjectDir, { recursive: true });
150
149
 
151
150
  if (glob) {
152
- for (let file of /** @type {string[]} */ (files)) {
153
- const stats = await fs.promises.stat(file);
154
- if (stats.isDirectory()) {
155
- continue;
156
- }
157
- await fs.promises.cp(file, path.resolve(nwProjectDir, file), {
158
- recursive: true,
159
- force: true,
160
- });
161
- }
151
+ await util.copyGlobFiles(/** @type {string[]} */ (files), nwProjectDir);
162
152
  } else {
163
153
  await fs.promises.cp(
164
154
  /** @type {string} */ (files),
@@ -404,11 +394,7 @@ const setWinConfig = async ({ app, outDir }) => {
404
394
  */
405
395
  const compress = async ({ zip, outDir }) => {
406
396
  if (zip === true || zip === "zip") {
407
- const archive = new ZipArchive();
408
- const writeStream = fs.createWriteStream(`${outDir}.zip`);
409
- archive.pipe(writeStream);
410
- archive.directory(outDir, false);
411
- await archive.finalize();
397
+ await util.zipDirectory(outDir, `${outDir}.zip`);
412
398
  } else if (zip === "tar") {
413
399
  await tar.create(
414
400
  {
package/src/util.js CHANGED
@@ -1,4 +1,10 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { pipeline } from "node:stream/promises";
5
+
1
6
  import * as GlobModule from "glob";
7
+ import ZipStream from "zip-stream";
2
8
 
3
9
  const PLATFORM_KV = {
4
10
  darwin: "osx",
@@ -44,8 +50,166 @@ async function globFiles({ srcDir, glob }) {
44
50
  return files;
45
51
  }
46
52
 
53
+ /**
54
+ * Recursively list a directory without following symlinks. A directory is
55
+ * yielded before its contents, and entries are sorted so output is stable.
56
+ * @param {string} absDir - directory to walk
57
+ * @param {string} relDir - path of `absDir` relative to the walk root, using `/`
58
+ * @returns {AsyncGenerator<{ absolute: string, relative: string, stats: fs.Stats }>}
59
+ */
60
+ async function* walkDirectory(absDir, relDir) {
61
+ const names = (await fs.promises.readdir(absDir)).sort();
62
+ for (const name of names) {
63
+ const absolute = path.join(absDir, name);
64
+ const relative = relDir === "" ? name : `${relDir}/${name}`;
65
+ const stats = await fs.promises.lstat(absolute);
66
+ yield { absolute, relative, stats };
67
+ if (stats.isDirectory()) {
68
+ yield* walkDirectory(absolute, relative);
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Zip the contents of a directory, so its children sit at the archive root.
75
+ *
76
+ * Matches what `archiver`'s `directory(srcDir, false)` produced: directories
77
+ * get their own entries, symlinks are stored as symlinks (relative to the
78
+ * link's directory) instead of being followed, and each entry keeps its
79
+ * permission bits and mtime. Other file types (sockets, FIFOs) are skipped.
80
+ * @async
81
+ * @function
82
+ * @param {string} srcDir - directory to compress
83
+ * @param {string} destFile - path of the zip file to write
84
+ * @returns {Promise<void>} - resolves once the zip file is fully written
85
+ */
86
+ async function zipDirectory(srcDir, destFile) {
87
+ const isWin = process.platform === "win32";
88
+ const archive = new ZipStream();
89
+ const written = pipeline(archive, fs.createWriteStream(destFile));
90
+
91
+ /**
92
+ * @param {Buffer | fs.ReadStream} source
93
+ * @param {import("zip-stream").FileDataInput} data
94
+ * @returns {Promise<void>}
95
+ */
96
+ const addEntry = (source, data) =>
97
+ new Promise((resolve, reject) => {
98
+ archive.entry(source, data, (error) =>
99
+ error ? reject(error) : resolve(),
100
+ );
101
+ });
102
+
103
+ try {
104
+ for await (const { absolute, relative, stats } of walkDirectory(
105
+ srcDir,
106
+ "",
107
+ )) {
108
+ const date = stats.mtime;
109
+ let mode = stats.mode & (isWin ? 0o777 : 0o7777);
110
+ if (stats.isFile()) {
111
+ await addEntry(fs.createReadStream(absolute), {
112
+ name: relative,
113
+ type: "file",
114
+ date,
115
+ mode,
116
+ });
117
+ } else if (stats.isDirectory()) {
118
+ // stat is unreliable for directories on Windows
119
+ if (isWin) mode = 0o755;
120
+ await addEntry(Buffer.alloc(0), {
121
+ name: `${relative}/`,
122
+ type: "directory",
123
+ date,
124
+ mode,
125
+ });
126
+ } else if (stats.isSymbolicLink()) {
127
+ const linkDir = path.dirname(absolute);
128
+ const target = await fs.promises.readlink(absolute);
129
+ const linkname = path
130
+ .relative(linkDir, path.resolve(linkDir, target))
131
+ .replace(/\\/g, "/");
132
+ await addEntry(Buffer.alloc(0), {
133
+ name: relative,
134
+ type: "symlink",
135
+ linkname,
136
+ date,
137
+ mode,
138
+ });
139
+ }
140
+ }
141
+ archive.finalize();
142
+ } catch (error) {
143
+ archive.destroy(/** @type {Error} */ (error));
144
+ }
145
+ await written;
146
+ }
147
+
148
+ /**
149
+ * Copy globbed files into the NW.js project directory.
150
+ *
151
+ * In glob mode the current working directory is the app root: each file keeps
152
+ * its path relative to `cwd`, so `package.json src/**` produces
153
+ * `package.json` and `src/...` at the top of `destDir`. Absolute matches
154
+ * inside `cwd` are placed the same way.
155
+ *
156
+ * A match outside `cwd` (for example from `../app/**`) has no place in that
157
+ * layout, so this rejects before copying anything instead of writing files
158
+ * next to, or outside, `destDir`.
159
+ * @async
160
+ * @function
161
+ * @param {string[]} files - paths returned by `globFiles`
162
+ * @param {string} destDir - NW.js project directory (`package.nw` / `app.nw`)
163
+ * @param {string} [cwd] - app root the files are relative to
164
+ * @returns {Promise<void>}
165
+ */
166
+ async function copyGlobFiles(files, destDir, cwd = process.cwd()) {
167
+ /** @type {{ src: string, dest: string }[]} */
168
+ const copies = [];
169
+ /** @type {string[]} */
170
+ const outside = [];
171
+ for (const file of files) {
172
+ const src = path.resolve(cwd, file);
173
+ const relative = path.relative(cwd, src);
174
+ // ".." or "../x" escapes cwd; an absolute result means another drive on Windows.
175
+ if (
176
+ relative === ".." ||
177
+ relative.startsWith(`..${path.sep}`) ||
178
+ path.isAbsolute(relative)
179
+ ) {
180
+ outside.push(file);
181
+ continue;
182
+ }
183
+ copies.push({ src, dest: path.join(destDir, relative) });
184
+ }
185
+
186
+ if (outside.length > 0) {
187
+ const shown = outside.slice(0, 5).join(", ");
188
+ const more = outside.length > 5 ? ` and ${outside.length - 5} more` : "";
189
+ throw new Error(
190
+ `srcDir glob matched files outside the current working directory (${cwd}): ${shown}${more}. ` +
191
+ "With glob enabled, paths are kept relative to the current working directory, which becomes the app root, " +
192
+ "so these files have no place in the build. Run nwbuild from the app directory, " +
193
+ 'or set glob to false and srcDir to the app directory (for example srcDir: "../app").',
194
+ );
195
+ }
196
+
197
+ for (const { src, dest } of copies) {
198
+ const stats = await fs.promises.stat(src);
199
+ if (stats.isDirectory()) {
200
+ continue;
201
+ }
202
+ await fs.promises.cp(src, dest, {
203
+ recursive: true,
204
+ force: true,
205
+ });
206
+ }
207
+ }
208
+
47
209
  export default {
48
210
  PLATFORM_KV,
49
211
  ARCH_KV,
50
212
  globFiles,
213
+ zipDirectory,
214
+ copyGlobFiles,
51
215
  };
package/types/main.d.ts CHANGED
@@ -230,7 +230,7 @@ export type BuildOptions = {
230
230
  /**
231
231
  * Target arch
232
232
  */
233
- arch?: "x64" | "ia32" | "arm64" | undefined;
233
+ arch?: "arm64" | "ia32" | "x64" | undefined;
234
234
  /**
235
235
  * Versions manifest URI, https or file path
236
236
  */
package/types/util.d.ts CHANGED
@@ -2,6 +2,8 @@ declare namespace _default {
2
2
  export { PLATFORM_KV };
3
3
  export { ARCH_KV };
4
4
  export { globFiles };
5
+ export { zipDirectory };
6
+ export { copyGlobFiles };
5
7
  }
6
8
  export default _default;
7
9
  declare namespace PLATFORM_KV {
@@ -27,3 +29,36 @@ declare function globFiles({ srcDir, glob }: {
27
29
  srcDir: string | string[];
28
30
  glob: boolean;
29
31
  }): Promise<string[] | string>;
32
+ /**
33
+ * Zip the contents of a directory, so its children sit at the archive root.
34
+ *
35
+ * Matches what `archiver`'s `directory(srcDir, false)` produced: directories
36
+ * get their own entries, symlinks are stored as symlinks (relative to the
37
+ * link's directory) instead of being followed, and each entry keeps its
38
+ * permission bits and mtime. Other file types (sockets, FIFOs) are skipped.
39
+ * @async
40
+ * @function
41
+ * @param {string} srcDir - directory to compress
42
+ * @param {string} destFile - path of the zip file to write
43
+ * @returns {Promise<void>} - resolves once the zip file is fully written
44
+ */
45
+ declare function zipDirectory(srcDir: string, destFile: string): Promise<void>;
46
+ /**
47
+ * Copy globbed files into the NW.js project directory.
48
+ *
49
+ * In glob mode the current working directory is the app root: each file keeps
50
+ * its path relative to `cwd`, so `package.json src/**` produces
51
+ * `package.json` and `src/...` at the top of `destDir`. Absolute matches
52
+ * inside `cwd` are placed the same way.
53
+ *
54
+ * A match outside `cwd` (for example from `../app/**`) has no place in that
55
+ * layout, so this rejects before copying anything instead of writing files
56
+ * next to, or outside, `destDir`.
57
+ * @async
58
+ * @function
59
+ * @param {string[]} files - paths returned by `globFiles`
60
+ * @param {string} destDir - NW.js project directory (`package.nw` / `app.nw`)
61
+ * @param {string} [cwd] - app root the files are relative to
62
+ * @returns {Promise<void>}
63
+ */
64
+ declare function copyGlobFiles(files: string[], destDir: string, cwd?: string): Promise<void>;