quickbundle 2.6.0 → 2.8.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/README.md CHANGED
@@ -10,22 +10,27 @@
10
10
 
11
11
  Quickbundle allows you to bundle a library in a **quick**, **fast** and **easy** way:
12
12
 
13
- - Fast build and watch mode powered by Rollup[^1] and SWC[^2].
14
- - Zero configuration: define the build artifacts in your `package.json`, and you're all set!
15
- - Support of `cjs` & `esm` module formats output.
16
- - Support of several loaders including JavaScript, TypeScript, JSX, JSON, and Images.
17
- - TypeScript's declaration file (`.d.ts`) bundling.
18
- - Automatic dependency inclusion (`peerDependencies` and `dependencies` are not bundled in the final output, `devDependencies` are unless they're not imported).
13
+ - Zero configuration: define the build artifacts in your `package.json`, and you're all set!
14
+ - Fast build and watch mode powered by Rollup[^1] and SWC[^2].
15
+ - Compile and cross compile standalone executables for systems that do not have Node.js installed[^3].
16
+ - Support of `cjs` & `esm` module formats output.
17
+ - Support of several loaders including JavaScript, TypeScript, JSX, JSON, and Images.
18
+ - TypeScript's declaration file (`.d.ts`) bundling.
19
+ - Automatic dependency inclusion:
20
+ - For the build and watch mode, `peerDependencies` and `dependencies` are not bundled in the final output, `devDependencies` are unless they're not imported.
21
+ - For the compile mode, all dependencies are included to make the code standalone.
19
22
 
20
23
  [^1]: A [module bundler](https://rollupjs.org/) optimized for better tree-shaking processing and seamless interoperability of CommonJS and ESM formats with minimal code footprint.
21
24
 
22
25
  [^2]: A [TypeScript / JavaScript transpiler](https://swc.rs/) for quicker code processing including TypeScript transpilation, JavaScript transformation, and, minification.
23
26
 
27
+ [^3]: It relies on the [Node.js single executable applications feature](https://nodejs.org/api/single-executable-applications.html).
28
+
24
29
  <br>
25
30
 
26
31
  ## 🚀 Quick Start
27
32
 
28
- 1️⃣ Install by running:
33
+ ### 1️⃣ Install
29
34
 
30
35
  ```bash
31
36
  # Npm
@@ -36,9 +41,11 @@ pnpm add quickbundle
36
41
  yarn add quickbundle
37
42
  ```
38
43
 
39
- 2️⃣ Set up your package configuration (`package.json`):
44
+ ### 2️⃣ Update your package configuration (`package.json`)
45
+
46
+ #### For building libraries
40
47
 
41
- - When exporting exclusively ESM format:
48
+ - When exporting exclusively ESM format:
42
49
 
43
50
  ```jsonc
44
51
  {
@@ -53,8 +60,8 @@ yarn add quickbundle
53
60
  },
54
61
  "./otherModulePath": {
55
62
  // ...
56
- }
57
- }
63
+ },
64
+ },
58
65
  "scripts": {
59
66
  "build": "quickbundle build", // Production mode (optimizes bundle)
60
67
  "watch": "quickbundle watch", // Development mode (watches each file change)
@@ -63,7 +70,10 @@ yarn add quickbundle
63
70
  }
64
71
  ```
65
72
 
66
- - When exporting both CommonJS (CJS) and ECMAScript Modules (ESM) format (please be aware of [dual package hazard risk](https://nodejs.org/api/packages.html#dual-package-hazard)):
73
+ - When exporting both CommonJS (CJS) and ECMAScript Modules (ESM) format:
74
+
75
+ > [!warning]
76
+ > Please be aware of [dual-package-hazard-related risks](https://github.com/nodejs/package-examples?tab=readme-ov-file#dual-package-hazard) first.
67
77
 
68
78
  ```jsonc
69
79
  {
@@ -80,8 +90,8 @@ yarn add quickbundle
80
90
  },
81
91
  "./otherModulePath": {
82
92
  // ...
83
- }
84
- }
93
+ },
94
+ },
85
95
  "scripts": {
86
96
  "build": "quickbundle build", // Production mode (optimizes bundle)
87
97
  "watch": "quickbundle watch", // Development mode (watches each file change)
@@ -90,7 +100,30 @@ yarn add quickbundle
90
100
  }
91
101
  ```
92
102
 
93
- 3️⃣ Try it by running:
103
+ #### For compiling executables
104
+
105
+ > [!warning]
106
+ > The `compile` command relies on the [Node.js SEA (Single Executable Applications feature)](https://nodejs.org/api/single-executable-applications.html). This feature comes with some limitations which Quickbundle attempts to address:
107
+ >
108
+ > - The source code must not rely on [external dependencies](https://github.com/nodejs/single-executable/discussions/70) 🛑. To partially address this, Quickbundle bundles all dependencies (whatever their types, as long as they're used) and inline dynamic imports by default ✅.
109
+ > - The bundled code must use the CommonJS module system 🛑. To address this, Quickbundle always outputs CJS modules in compile mode ✅.
110
+
111
+ ```jsonc
112
+ {
113
+ "name": "lib", // Package name
114
+ "source": "./src/index.ts", // Source code entry point. Make sure that it starts with `#!/usr/bin/env node` pragme to make the binary portable for consumers who would like to use it by installing the package instead of using the generated standalone executable.
115
+ "bin": {
116
+ "your-binary-name": "./dist/index.cjs", // Binary information to get the executable name from the key and, from the value, the bundled file to generate from the source code and inject into the executable. The generated executable will be located in the same folder as the bundled file and, by default, dependending on the current operating system running the `compile` command, the executable will be named either `your-binary-name.exe` on Windows or `your-binary-name` on Linux and macOS. For cross-compilation output, check the `Patterns` section.
117
+ },
118
+ // "bin": "./dist/index.cjs", // Or, if the binary name follows the package name, you can define a string-based `bin` value.
119
+ "scripts": {
120
+ "build": "quickbundle compile",
121
+ },
122
+ // ...
123
+ }
124
+ ```
125
+
126
+ ### 3️⃣ Try it
94
127
 
95
128
  ```bash
96
129
  # Npm
@@ -109,14 +142,14 @@ yarn build
109
142
 
110
143
  By default, Quickbundle does the following built-in optimizations during the bundling process:
111
144
 
112
- - Include, in the build output, only the code that is effectively imported and used in the source code. Setting the `sideEffects` package.json field to `false` marks the package as a side-effect-free one and helps Quickbundle to safely prune unused exports.
113
- - [Identify and annotate](https://rollupjs.org/configuration-options/#treeshake-annotations) side-effect-free code (functions, ...) to enable a fine-grained dead-code elimination process later consumer side. For example, if a consumer uses only one library API, build output annotations added by Quickbundle allow the consumer's bundler remove all other unused APIs.
145
+ - Include, in the build output, only the code that is effectively imported and used in the source code. Setting the `sideEffects` package.json field to `false` marks the package as a side-effect-free one and helps Quickbundle to safely prune unused exports.
146
+ - [Identify and annotate](https://rollupjs.org/configuration-options/#treeshake-annotations) side-effect-free code (functions, ...) to enable a fine-grained dead-code elimination process later consumer side. For example, if a consumer uses only one library API, build output annotations added by Quickbundle allow the consumer's bundler remove all other unused APIs.
114
147
 
115
148
  However, Quickbundle doesn't minify the build output. Indeed, in general, **if the build targets a library (the most Quickbundle use case)**, minification is not necessary since enabling it can introduce some challenges:
116
149
 
117
- - Reduce the build output discoverability inside the `node_modules` folder (minified code is an obfuscated code that can be hard to read for code audit/debugging purposes).
118
- - Generate suboptimal source maps for the bundled library, as the consumer bundler will generate source maps based on the already minified library build (transformed code, mangled variable names, etc.).
119
- - Risk of side effects with double optimizations (producer side and then consumer side).
150
+ - Reduce the build output discoverability inside the `node_modules` folder (minified code is an obfuscated code that can be hard to read for code audit/debugging purposes).
151
+ - Generate suboptimal source maps for the bundled library, as the consumer bundler will generate source maps based on the already minified library build (transformed code, mangled variable names, etc.).
152
+ - Risk of side effects with double optimizations (producer side and then consumer side).
120
153
 
121
154
  Popular open source libraries ([Vue](https://unpkg.com/browse/vue@3.4.24/dist/), [SolidJS](https://unpkg.com/browse/solid-js@1.8.17/dist/), [Material UI](https://unpkg.com/browse/@material-ui/core@4.12.4/), ...) do not provide minified builds as the build optimization is sensitive to the consumer context (e.g. environment targets (browser support), ...) and needs to be fully owned upstream (i.e. consumer/application-side).
122
155
 
@@ -138,11 +171,29 @@ quickbundle watch --source-maps
138
171
 
139
172
  Enabling source map generation is needed only if a build is [obfuscated (minified)](#optimize-the-build-output) for debugging-easing purposes. It generally pairs with the [`minification` flag](#optimize-the-build-output).
140
173
 
174
+ ### Cross compilation to other platforms
175
+
176
+ By default, the `compile` command embeds the runtime at the origin of its execution which means it generates executables compatible only with machines running the same operating system, processor architecture, and Node.js version.
177
+
178
+ However, Quickbundle provides the ability to target a different operating system, processor architecture, or Node.js version (also known as cross-compilation):
179
+
180
+ ```bash
181
+ quickbundle compile --target node-v23.1.0-darwin-arm64 # Embeds Node v23 runtime built for macOS ARM64 target
182
+ quickbundle compile --target node-v23.1.0-darwin-x64 # Embeds Node v23 runtime built for macOS X64 target
183
+ quickbundle compile --target node-v23.1.0-linux-arm64 # Embeds Node v23 runtime built for Linux ARM64 target
184
+ quickbundle compile --target node-v23.1.0-linux-x64 # Embeds Node v23 runtime built for Linux X64 target
185
+ quickbundle compile --target node-v23.1.0-win-arm64 # Embeds Node v23 runtime built for Windows ARM64 target
186
+ quickbundle compile --target node-v23.1.0-win-x64 # Embeds Node v23 runtime built for Windows X64 target
187
+ ```
188
+
189
+ > [!note]
190
+ > The target input must follow the `node-vx.y.z-(darwin|linux|win)-(arm64|x64|x86)` format (for an exhaustive view, check available filenames in [https://nodejs.org/download/release/vx.y.z](https://nodejs.org/download/release/latest/)).
191
+
141
192
  <br>
142
193
 
143
194
  ## 🤩 Used by
144
195
 
145
- - [@adbayb/stack](https://github.com/adbayb/stack) My opinionated toolbox for JavaScript/TypeScript projects.
196
+ - [@adbayb/stack](https://github.com/adbayb/stack) My opinionated toolbox for JavaScript/TypeScript projects.
146
197
 
147
198
  <br>
148
199
 
@@ -154,11 +205,13 @@ We're open to new contributions, you can find more details [here](./CONTRIBUTING
154
205
 
155
206
  ## 💙 Acknowledgements
156
207
 
157
- - The backend is powered by [Rollup](https://github.com/rollup/rollup) and its plugin ecosystem (including [SWC](https://github.com/swc-project/swc)) to make blazing-fast builds. A special shoutout to all contributors involved.
158
- - The zero-configuration approach was inspired by [microbundle](https://github.com/developit/microbundle). A special shoutout to its author [Jason Miller](https://github.com/developit) and [all contributors](https://github.com/developit/microbundle/graphs/contributors).
208
+ - The backend is powered by [Rollup](https://github.com/rollup/rollup) and its plugin ecosystem (including [SWC](https://github.com/swc-project/swc)) to make blazing-fast builds. A special shoutout to all contributors involved.
209
+ - The zero-configuration approach was inspired by [microbundle](https://github.com/developit/microbundle). A special shoutout to its author [Jason Miller](https://github.com/developit) and [all contributors](https://github.com/developit/microbundle/graphs/contributors).
159
210
 
160
211
  <br>
161
212
 
162
213
  ## 📖 License
163
214
 
164
215
  [MIT](./LICENSE "License MIT").
216
+
217
+ <br>
package/bin/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { createRequire } from "node:module";
4
3
  import { join } from "node:path";
4
+ import { createRequire } from "node:module";
5
5
 
6
6
  const pkg = createRequire(import.meta.url)("../package.json");
7
7
 
package/dist/index.mjs CHANGED
@@ -1,17 +1,105 @@
1
- #!/usr/bin/env node
2
1
  import { helpers, termost } from 'termost';
3
- import { gzipSize } from 'gzip-size';
4
- import { rollup, watch as watch$1 } from 'rollup';
5
- import commonjs from '@rollup/plugin-commonjs';
6
- import json from '@rollup/plugin-json';
7
- import { nodeResolve } from '@rollup/plugin-node-resolve';
8
- import url from '@rollup/plugin-url';
2
+ import { finished } from 'node:stream/promises';
3
+ import { Readable } from 'node:stream';
4
+ import process from 'node:process';
5
+ import { resolve, dirname, join, basename } from 'node:path';
6
+ import { copyFile as copyFile$1, rm, writeFile as writeFile$1, rename, mkdir, readFile as readFile$1 } from 'node:fs/promises';
7
+ import { createWriteStream } from 'node:fs';
8
+ import decompress from 'decompress';
9
+ import { watch as watch$1, rollup } from 'rollup';
9
10
  import { createRequire } from 'node:module';
10
- import { join } from 'node:path';
11
- import dts from 'rollup-plugin-dts';
12
- import externals from 'rollup-plugin-node-externals';
13
11
  import { swc } from 'rollup-plugin-swc3';
14
- import { readFile as readFile$1 } from 'node:fs/promises';
12
+ import externals from 'rollup-plugin-node-externals';
13
+ import dts from 'rollup-plugin-dts';
14
+ import url from '@rollup/plugin-url';
15
+ import { nodeResolve } from '@rollup/plugin-node-resolve';
16
+ import json from '@rollup/plugin-json';
17
+ import commonjs from '@rollup/plugin-commonjs';
18
+ import os from 'node:os';
19
+ import { gzipSize } from 'gzip-size';
20
+
21
+ var name = "quickbundle";
22
+ var version = "2.8.0";
23
+
24
+ /**
25
+ * Resolve a relative path from the Quickbundle node modules directory.
26
+ * @param paths - Relative paths.
27
+ * @returns The resolved absolute path.
28
+ * @example
29
+ * resolveFromInternalDirectory("dist", "node");
30
+ */ const resolveFromInternalDirectory = (...paths)=>{
31
+ return resolve(import.meta.dirname, "../", ...paths);
32
+ };
33
+ /**
34
+ * Resolve a relative path from the current working project directory.
35
+ * @param paths - Relative paths.
36
+ * @returns The resolved absolute path.
37
+ * @example
38
+ * resolveFromExternalDirectory("package.json");
39
+ */ const resolveFromExternalDirectory = (...paths)=>{
40
+ return resolve(process.cwd(), ...paths);
41
+ };
42
+ const createRegExpMatcher = (regex)=>{
43
+ return (value)=>{
44
+ return regex.exec(value)?.groups;
45
+ };
46
+ };
47
+ const createDirectory = async (path)=>{
48
+ await mkdir(path, {
49
+ recursive: true
50
+ });
51
+ };
52
+ const copyFile = async (fromPath, toPath)=>{
53
+ await createDirectory(dirname(toPath));
54
+ await copyFile$1(fromPath, toPath);
55
+ };
56
+ const removePath = async (path)=>{
57
+ await rm(path, {
58
+ force: true,
59
+ recursive: true
60
+ });
61
+ };
62
+ const readFile = async (filePath)=>{
63
+ return readFile$1(filePath);
64
+ };
65
+ const writeFile = async (filePath, content)=>{
66
+ await createDirectory(dirname(filePath));
67
+ await writeFile$1(filePath, content, "utf8");
68
+ };
69
+ const download = async (url, filePath)=>{
70
+ await createDirectory(dirname(filePath));
71
+ const { body, ok, status, statusText } = await fetch(url);
72
+ if (!ok) {
73
+ throw new Error(`An error ocurred while downloading \`${url}\`. Received \`${status}\` status code with the following message \`${statusText}\`.`);
74
+ }
75
+ if (!body) {
76
+ throw new Error(`Empty body received while downloading \`${url}\`.`);
77
+ }
78
+ await finished(Readable.fromWeb(body).pipe(createWriteStream(filePath)));
79
+ };
80
+ const unzip = async (input, output)=>{
81
+ const { targetedArchivePath } = input;
82
+ const { directoryPath } = output;
83
+ await decompress(input.path, directoryPath, {
84
+ filter (file) {
85
+ return file.path === targetedArchivePath;
86
+ }
87
+ });
88
+ await rename(join(directoryPath, targetedArchivePath), join(directoryPath, output.filename));
89
+ };
90
+ const createCommand = (program, input)=>{
91
+ return program.command(input).option({
92
+ key: "minification",
93
+ name: "minification",
94
+ description: "Enable minification",
95
+ defaultValue: false
96
+ }).option({
97
+ key: "sourceMaps",
98
+ name: "source-maps",
99
+ description: "Enable source maps generation",
100
+ defaultValue: false
101
+ });
102
+ };
15
103
 
16
104
  const onLog = (_, log)=>{
17
105
  if (log.message.includes("Generated an empty chunk")) return;
@@ -20,68 +108,109 @@ const isRecord = (value)=>{
20
108
  return typeof value === "object" && value !== null && !Array.isArray(value);
21
109
  };
22
110
 
23
- const build = async (configurations)=>{
24
- process.env.NODE_ENV ??= "production";
25
- const output = [];
26
- for (const config of configurations){
27
- const initialTime = Date.now();
28
- const bundle = await rollup({
29
- ...config,
111
+ const watch = (input)=>{
112
+ process.env.NODE_ENV ??= "development";
113
+ const watcher = watch$1(input.data.map((configItem)=>({
114
+ ...configItem,
30
115
  onLog
31
- });
32
- if (config.output) {
33
- const outputEntries = Array.isArray(config.output) ? config.output : [
34
- config.output
35
- ];
36
- const promises = [];
37
- for (const outputEntry of outputEntries){
38
- const outputFilename = outputEntry.file ?? outputEntry.dir;
39
- if (!outputFilename) {
40
- throw new Error("Misconfigured file entry point. Make sure to define an `import`, `require`, or `default` field.");
116
+ })));
117
+ let startDuration;
118
+ console.clear();
119
+ watcher.on("event", async (event)=>{
120
+ switch(event.code){
121
+ case "START":
122
+ {
123
+ startDuration = Date.now();
124
+ clearLog(`Build in progress...`, {
125
+ type: "information"
126
+ });
127
+ return;
128
+ }
129
+ case "BUNDLE_END":
130
+ await event.result.close();
131
+ break;
132
+ case "END":
133
+ {
134
+ const duration = Date.now() - startDuration;
135
+ clearLog(`Build done in ${duration}ms (at ${new Date().toLocaleTimeString()})`, {
136
+ type: "success"
137
+ });
138
+ return;
139
+ }
140
+ case "ERROR":
141
+ {
142
+ const { error } = event;
143
+ clearLog(error.message, {
144
+ type: "error"
145
+ });
146
+ console.error("\n", error);
147
+ return;
41
148
  }
42
- promises.push(new Promise((resolve, reject)=>{
43
- bundle.write(outputEntry).then(()=>{
44
- resolve({
45
- elapsedTime: Date.now() - initialTime,
46
- filename: outputFilename
47
- });
48
- }).catch(reject);
49
- }));
50
- }
51
- output.push(...await Promise.all(promises));
52
149
  }
53
- }
54
- return output;
150
+ });
151
+ };
152
+ const clearLog = (...input)=>{
153
+ console.clear();
154
+ helpers.message(...input);
55
155
  };
56
-
57
- const CWD = process.cwd();
58
156
 
59
157
  const require = createRequire(import.meta.url);
60
- const PKG = require(join(CWD, "./package.json"));
61
- const createConfigurations = (options = {
158
+ const PKG = require(resolveFromExternalDirectory("package.json"));
159
+ const createConfiguration = (options = {
62
160
  minification: false,
63
- sourceMaps: false
161
+ sourceMaps: false,
162
+ standalone: false
64
163
  })=>{
65
- return getBuildableExports().flatMap((buildableExport)=>{
66
- return [
67
- buildableExport.source && createMainConfig({
68
- ...buildableExport,
69
- source: buildableExport.source
70
- }, options),
71
- buildableExport.source && buildableExport.types && createTypesConfig({
72
- source: buildableExport.source,
73
- types: buildableExport.types
74
- })
75
- ].filter(Boolean);
76
- });
164
+ const buildableExports = getBuildableExports(options);
165
+ return {
166
+ data: buildableExports.flatMap((buildableExport)=>{
167
+ return [
168
+ buildableExport.source && createMainConfig({
169
+ ...buildableExport,
170
+ source: buildableExport.source
171
+ }, options),
172
+ buildableExport.source && buildableExport.types && createTypesConfig({
173
+ source: buildableExport.source,
174
+ types: buildableExport.types
175
+ }, options)
176
+ ].filter(Boolean);
177
+ }),
178
+ metadata: buildableExports
179
+ };
77
180
  };
78
- const getBuildableExports = ()=>{
181
+ // eslint-disable-next-line sonarjs/cyclomatic-complexity
182
+ const getBuildableExports = ({ standalone })=>{
183
+ if (standalone) {
184
+ /**
185
+ * Entry-point resolution invariants for standalone target (mostly binaries).
186
+ */ if (!PKG.source || !PKG.bin || !PKG.name) {
187
+ throw new Error("Invalid package entry points contract. Standalone compilation is enabled but required fields are missing. Make sure to set `name`, `source`, and `bin` fields.");
188
+ }
189
+ const bin = PKG.bin;
190
+ const name = PKG.name;
191
+ const source = PKG.source;
192
+ if (isRecord(bin)) {
193
+ return Object.entries(bin).map((data)=>({
194
+ bin: data[0],
195
+ require: data[1],
196
+ source
197
+ }));
198
+ }
199
+ return [
200
+ {
201
+ // For scoped packages and if the `bin` is defined with a string value, the [scope name is discarded](the scope name is discarded when creating a binary) when creating a binary.
202
+ bin: name.replace(/^(@.*?\/)/, ""),
203
+ require: bin,
204
+ source
205
+ }
206
+ ];
207
+ }
79
208
  /**
80
- * Entry-point resolution:
209
+ * Entry-point resolution invariants for non-standalone target (mostly libraries):
81
210
  * Following the [package entry-point specification](https://nodejs.org/api/packages.html#package-entry-points),
82
211
  * whenever an export object is defined, it take precedence over other classical entry-point fields
83
212
  * (such as main, module, and types defined at the root package.json level).
84
- */ if (PKG.main ?? PKG.module ?? PKG.types ?? !PKG.exports) {
213
+ */ if (PKG.main || PKG.module || PKG.types || !PKG.exports) {
85
214
  throw new Error("Invalid package entry points contract. Use the recommended [`exports` field](https://nodejs.org/api/packages.html#package-entry-points) instead and, for TypeScript-based projects, update the `tsconfig.json` file to resolve it properly (`moduleResolution` must be set to `Bundler` (or `NodeNext`)).");
86
215
  }
87
216
  const buildableExportFields = [
@@ -130,15 +259,15 @@ const getBuildableExports = ()=>{
130
259
  }
131
260
  return output;
132
261
  };
133
- const getPlugins = (...customPlugins)=>{
262
+ const getPlugins = (customPlugins, options)=>{
134
263
  return [
135
- externals({
264
+ !options.standalone && externals({
136
265
  builtins: true,
137
266
  deps: true,
138
267
  /**
139
- * As they're not installed consumer side, `devDependencies` are declared as internal dependencies (via the `false` value)
140
- * and bundled into the dist if and only if imported and not listed as `peerDependencies` (otherwise, they're considered external).
141
- */ devDeps: false,
268
+ * As they're not installed consumer side, `devDependencies` are declared as internal dependencies (via the `false` value)
269
+ * and bundled into the dist if and only if imported and not listed as `peerDependencies` (otherwise, they're considered external).
270
+ */ devDeps: false,
142
271
  optDeps: true,
143
272
  peerDeps: true
144
273
  }),
@@ -146,7 +275,7 @@ const getPlugins = (...customPlugins)=>{
146
275
  url(),
147
276
  json(),
148
277
  ...customPlugins
149
- ];
278
+ ].filter(Boolean);
150
279
  };
151
280
  const createMainConfig = (entryPoints, options)=>{
152
281
  const { minification, sourceMaps } = options;
@@ -158,6 +287,7 @@ const createMainConfig = (entryPoints, options)=>{
158
287
  entryPoints.require && {
159
288
  file: entryPoints.require,
160
289
  format: "cjs",
290
+ inlineDynamicImports: Boolean(options.standalone),
161
291
  sourcemap: sourceMaps
162
292
  },
163
293
  esmInput && {
@@ -169,13 +299,16 @@ const createMainConfig = (entryPoints, options)=>{
169
299
  return {
170
300
  input: entryPoints.source,
171
301
  output,
172
- plugins: getPlugins(nodeResolve(), swc({
173
- minify: minification,
174
- sourceMaps
175
- }))
302
+ plugins: getPlugins([
303
+ nodeResolve(),
304
+ swc({
305
+ minify: minification,
306
+ sourceMaps
307
+ })
308
+ ], options)
176
309
  };
177
310
  };
178
- const createTypesConfig = (entryPoints)=>{
311
+ const createTypesConfig = (entryPoints, options)=>{
179
312
  return {
180
313
  input: entryPoints.source,
181
314
  output: [
@@ -183,52 +316,216 @@ const createTypesConfig = (entryPoints)=>{
183
316
  file: entryPoints.types
184
317
  }
185
318
  ],
186
- plugins: getPlugins(nodeResolve({
187
- /**
188
- * The `exports` conditional fields definition order is important in the `package.json file`.
189
- * To be resolved first, `types` field must always come first in the package.json exports definition.
190
- * @see https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#package-json-exports-imports-and-self-referencing.
191
- */ exportConditions: [
192
- "types"
193
- ]
194
- }), dts({
195
- compilerOptions: {
196
- incremental: false
197
- },
198
- respectExternal: true
199
- }))
319
+ plugins: getPlugins([
320
+ nodeResolve({
321
+ /**
322
+ * The `exports` conditional fields definition order is important in the `package.json file`.
323
+ * To be resolved first, `types` field must always come first in the package.json exports definition.
324
+ * @see https://devblogs.microsoft.com/typescript/announcing-typescript-4-7/#package-json-exports-imports-and-self-referencing.
325
+ */ exportConditions: [
326
+ "types"
327
+ ]
328
+ }),
329
+ dts({
330
+ compilerOptions: {
331
+ incremental: false
332
+ },
333
+ respectExternal: true
334
+ })
335
+ ], options)
200
336
  };
201
337
  };
202
- // eslint-disable-next-line import/no-default-export
203
- createConfigurations();
204
338
 
205
- const readFile = readFile$1;
339
+ const createWatchCommand = (program)=>{
340
+ return createCommand(program, {
341
+ name: "watch",
342
+ description: "Watch and rebuild on any code change (development mode)"
343
+ }).task({
344
+ handler (context) {
345
+ watch(createConfiguration({
346
+ minification: context.minification,
347
+ sourceMaps: context.sourceMaps,
348
+ standalone: false
349
+ }));
350
+ }
351
+ });
352
+ };
206
353
 
207
- const createCommand = (program, input)=>{
208
- return program.command(input).option({
209
- key: "minification",
210
- name: "minification",
211
- description: "Enable minification",
212
- defaultValue: false
354
+ const build = async (input)=>{
355
+ process.env.NODE_ENV ??= "production";
356
+ const { data: configurations } = input;
357
+ const output = [];
358
+ for (const config of configurations){
359
+ const initialTime = Date.now();
360
+ const bundle = await rollup({
361
+ ...config,
362
+ onLog
363
+ });
364
+ if (config.output) {
365
+ const outputEntries = Array.isArray(config.output) ? config.output : [
366
+ config.output
367
+ ];
368
+ const promises = [];
369
+ for (const outputEntry of outputEntries){
370
+ const outputFilePath = outputEntry.file ?? outputEntry.dir;
371
+ if (!outputFilePath) {
372
+ throw new Error("Misconfigured file entry point. Make sure to define an `import`, `require`, or `default` field.");
373
+ }
374
+ promises.push(new Promise((resolve, reject)=>{
375
+ bundle.write(outputEntry).then(()=>{
376
+ resolve({
377
+ elapsedTime: Date.now() - initialTime,
378
+ filePath: outputFilePath
379
+ });
380
+ }).catch((reason)=>{
381
+ reject(reason);
382
+ });
383
+ }));
384
+ }
385
+ output.push(...await Promise.all(promises));
386
+ }
387
+ }
388
+ return output;
389
+ };
390
+
391
+ const TEMPORARY_PATH = resolveFromInternalDirectory("dist", "tmp");
392
+ const TEMPORARY_DOWNLOAD_PATH = join(TEMPORARY_PATH, "zip");
393
+ const TEMPORARY_RUNTIME_PATH = join(TEMPORARY_PATH, "runtime");
394
+ const createCompileCommand = (program)=>{
395
+ return program.command({
396
+ name: "compile",
397
+ description: "Compiles the source code into a self-contained executable"
213
398
  }).option({
214
- key: "sourceMaps",
215
- name: "source-maps",
216
- description: "Enable source maps generation",
217
- defaultValue: false
399
+ key: "targetInput",
400
+ name: {
401
+ long: "target",
402
+ short: "t"
403
+ },
404
+ description: "Set a different cross-compilation target",
405
+ defaultValue: "local"
406
+ }).task({
407
+ key: "config",
408
+ label: "Create configuration",
409
+ handler () {
410
+ return createConfiguration({
411
+ minification: true,
412
+ sourceMaps: false,
413
+ standalone: true
414
+ });
415
+ }
416
+ }).task({
417
+ key: "osType",
418
+ label ({ targetInput }) {
419
+ return `Get \`${targetInput}\` runtime`;
420
+ },
421
+ async handler ({ targetInput }) {
422
+ if (targetInput === "local") {
423
+ await copyFile(process.execPath, TEMPORARY_RUNTIME_PATH);
424
+ return getOsType(os.type());
425
+ }
426
+ const matchedRuntimeParts = matchRuntimeParts(targetInput);
427
+ if (!matchedRuntimeParts) {
428
+ throw new Error("Invalid `runtime` flag input. The accepted targets are the one listed in https://nodejs.org/download/release/ with the following format `node-vx.y.z-(darwin|linux|win)-(arm64|x64|x86)`.");
429
+ }
430
+ const osType = getOsType(matchedRuntimeParts.os);
431
+ const extension = osType === "windows" ? "zip" : "tar.gz";
432
+ await download(`https://nodejs.org/download/release/${matchedRuntimeParts.version}/${targetInput}.${extension}`, TEMPORARY_DOWNLOAD_PATH);
433
+ await unzip({
434
+ path: TEMPORARY_DOWNLOAD_PATH,
435
+ targetedArchivePath: osType === "windows" ? join(targetInput, "node.exe") : join(targetInput, "bin", "node")
436
+ }, {
437
+ directoryPath: dirname(TEMPORARY_RUNTIME_PATH),
438
+ filename: basename(TEMPORARY_RUNTIME_PATH)
439
+ });
440
+ return osType;
441
+ }
442
+ }).task({
443
+ label: "Build",
444
+ async handler ({ config }) {
445
+ await build(config);
446
+ }
447
+ }).task({
448
+ label ({ config }) {
449
+ const binaries = config.metadata.map(({ bin })=>{
450
+ if (!bin) return undefined;
451
+ return `\`${bin}\``;
452
+ }).filter(Boolean).join(", ");
453
+ return `Compile ${binaries}`;
454
+ },
455
+ async handler ({ config, osType }) {
456
+ await Promise.all(config.metadata.map(async ({ bin, require })=>{
457
+ if (!require || !bin) return;
458
+ return compile({
459
+ bin,
460
+ input: require,
461
+ osType
462
+ });
463
+ }));
464
+ }
218
465
  });
219
466
  };
467
+ const getOsType = (input)=>{
468
+ switch(input){
469
+ case "Windows_NT":
470
+ case "win":
471
+ return "windows";
472
+ case "Darwin":
473
+ case "darwin":
474
+ return "macos";
475
+ case "Linux":
476
+ case "linux":
477
+ return "linux";
478
+ default:
479
+ throw new Error(`Unsupported operating system \`${input}\``);
480
+ }
481
+ };
482
+ const matchRuntimeParts = createRegExpMatcher(/^node-(?<version>v\d+\.\d+\.\d+)-(?<os>darwin|linux|win)-(?<architecture>arm64|x64|x86)$/);
483
+ const compile = async ({ bin, input, osType })=>{
484
+ const inputFileName = basename(input);
485
+ const inputDirectory = dirname(input);
486
+ const resolveFromInputDirectory = (...paths)=>{
487
+ return resolve(inputDirectory, ...paths);
488
+ };
489
+ const blobFileName = resolveFromInputDirectory(`${inputFileName}.blob`);
490
+ const executableFileName = resolveFromInputDirectory(`${bin}${osType === "windows" ? ".exe" : ""}`);
491
+ const seaConfigFileName = resolveFromInputDirectory(`${inputFileName}.sea-config.json`);
492
+ await writeFile(seaConfigFileName, JSON.stringify({
493
+ disableExperimentalSEAWarning: true,
494
+ main: input,
495
+ output: blobFileName,
496
+ useCodeCache: false,
497
+ useSnapshot: false
498
+ }));
499
+ await Promise.all([
500
+ helpers.exec(`node --experimental-sea-config ${seaConfigFileName}`),
501
+ copyFile(TEMPORARY_RUNTIME_PATH, executableFileName)
502
+ ]);
503
+ if (osType === "macos") {
504
+ await helpers.exec(`codesign --remove-signature ${executableFileName}`);
505
+ }
506
+ await helpers.exec(`npx postject ${executableFileName} NODE_SEA_BLOB ${blobFileName} --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2 ${osType === "macos" ? "--macho-segment-name NODE_SEA" : ""}`);
507
+ if (osType === "macos") {
508
+ await helpers.exec(`codesign --sign - ${executableFileName}`);
509
+ }
510
+ await Promise.all([
511
+ blobFileName,
512
+ seaConfigFileName,
513
+ TEMPORARY_PATH
514
+ ].map(async (path)=>removePath(path)));
515
+ };
220
516
 
221
517
  const createBuildCommand = (program)=>{
222
- createCommand(program, {
518
+ return createCommand(program, {
223
519
  name: "build",
224
520
  description: "Build the source code (production mode)"
225
521
  }).task({
226
522
  key: "buildOutput",
227
523
  label: "Bundle assets 📦",
228
524
  async handler (context) {
229
- return build(createConfigurations({
525
+ return build(createConfiguration({
230
526
  minification: context.minification,
231
- sourceMaps: context.sourceMaps
527
+ sourceMaps: context.sourceMaps,
528
+ standalone: false
232
529
  }));
233
530
  }
234
531
  }).task({
@@ -249,7 +546,7 @@ const createBuildCommand = (program)=>{
249
546
  ].map((message, index)=>{
250
547
  return index === 0 ? message : ` ${message}`;
251
548
  }).join("\n"), {
252
- label: `${item.filename} (took ${item.elapsedTime}ms)`,
549
+ label: `${item.filePath} (took ${item.elapsedTime}ms)`,
253
550
  type: "information"
254
551
  });
255
552
  });
@@ -261,7 +558,7 @@ const createBuildCommand = (program)=>{
261
558
  };
262
559
  const computeBundleSize = async (buildOutput)=>{
263
560
  const computeFileSize = async (buildItemOutput)=>{
264
- const content = await readFile(buildItemOutput.filename);
561
+ const content = await readFile(buildItemOutput.filePath);
265
562
  const gzSize = await gzipSize(content);
266
563
  return {
267
564
  ...buildItemOutput,
@@ -276,70 +573,14 @@ const formatSize = (bytes)=>{
276
573
  return kiloBytes < 1 ? `${bytes} B` : `${kiloBytes.toFixed(2)} kB`;
277
574
  };
278
575
 
279
- const watch = (configurations)=>{
280
- process.env.NODE_ENV ??= "development";
281
- const watcher = watch$1(configurations.map((config)=>({
282
- ...config,
283
- onLog
284
- })));
285
- let startDuration;
286
- console.clear();
287
- watcher.on("event", async (event)=>{
288
- switch(event.code){
289
- case "START":
290
- {
291
- startDuration = Date.now();
292
- clearLog(`Build in progress...`, {
293
- type: "information"
294
- });
295
- return;
296
- }
297
- case "BUNDLE_END":
298
- await event.result.close();
299
- break;
300
- case "END":
301
- {
302
- const duration = Date.now() - startDuration;
303
- clearLog(`Build done in ${duration}ms (at ${new Date().toLocaleTimeString()})`, {
304
- type: "success"
305
- });
306
- return;
307
- }
308
- case "ERROR":
309
- {
310
- const { error } = event;
311
- clearLog(`${String(error)}`, {
312
- type: "error"
313
- });
314
- console.error("\n", error);
315
- return;
316
- }
317
- }
318
- });
319
- };
320
- const clearLog = (...input)=>{
321
- console.clear();
322
- helpers.message(...input);
323
- };
324
-
325
- const createWatchCommand = (program)=>{
326
- createCommand(program, {
327
- name: "watch",
328
- description: "Watch and rebuild on any code change (development mode)"
329
- }).task({
330
- handler (context) {
331
- watch(createConfigurations({
332
- minification: context.minification,
333
- sourceMaps: context.sourceMaps
334
- }));
335
- }
336
- });
337
- };
338
-
339
576
  const createProgram = (...commandBuilders)=>{
340
- const program = termost("The zero-configuration transpiler and bundler for the web");
577
+ const program = termost({
578
+ name,
579
+ description: "The zero-configuration transpiler and bundler for the web",
580
+ version
581
+ });
341
582
  for (const commandBuilder of commandBuilders){
342
583
  commandBuilder(program);
343
584
  }
344
585
  };
345
- createProgram(createBuildCommand, createWatchCommand);
586
+ createProgram(createBuildCommand, createWatchCommand, createCompileCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quickbundle",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "description": "The zero-configuration transpiler and bundler for the web",
5
5
  "author": {
6
6
  "name": "Ayoub Adib",
@@ -40,7 +40,7 @@
40
40
  }
41
41
  },
42
42
  "peerDependencies": {
43
- "typescript": ">=4.7.0"
43
+ "typescript": "^4.7.0 || ^5.0.0"
44
44
  },
45
45
  "peerDependenciesMeta": {
46
46
  "typescript": {
@@ -52,19 +52,21 @@
52
52
  "@rollup/plugin-json": "^6.1.0",
53
53
  "@rollup/plugin-node-resolve": "^15.3.0",
54
54
  "@rollup/plugin-url": "^8.0.2",
55
- "@swc/core": "^1.7.42",
55
+ "@swc/core": "^1.9.1",
56
+ "decompress": "^4.2.1",
56
57
  "gzip-size": "^7.0.0",
57
- "rollup": "^4.24.3",
58
+ "rollup": "^4.24.4",
58
59
  "rollup-plugin-dts": "^6.1.1",
59
60
  "rollup-plugin-node-externals": "^7.1.3",
60
61
  "rollup-plugin-swc3": "^0.12.1",
61
- "termost": "^0.18.0"
62
+ "termost": "^1.2.0"
62
63
  },
63
64
  "devDependencies": {
64
- "@types/node": "22.8.5"
65
+ "@types/decompress": "4.2.7",
66
+ "@types/node": "22.9.0"
65
67
  },
66
68
  "scripts": {
67
- "build": "rollup --config ./src/bundler/config.ts --configPlugin rollup-plugin-swc3",
68
- "watch": "rollup --watch --config ./src/bundler/config.ts --configPlugin rollup-plugin-swc3"
69
+ "build": "rollup --config ./bundler.config.ts --configPlugin rollup-plugin-swc3",
70
+ "watch": "rollup --watch --config ./bundler.config.ts --configPlugin rollup-plugin-swc3"
69
71
  }
70
72
  }