iifeify 1.0.0 → 1.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/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  Turn an npm package into one classic browser script.
4
4
 
5
5
  ```bash
6
- npx iifeify vanilla-jsoneditor@3.13.0 --entry standalone.js --global-name VanillaJsonEditor
6
+ npx -y iifeify vanilla-jsoneditor@3.13.0 --entry standalone.js --global-name VanillaJsonEditor
7
7
  ```
8
8
 
9
9
  This writes `vanilla-jsoneditor.iife.js`. Loaded with a plain `<script>` tag, the file defines the global `VanillaJsonEditor`, which holds everything the package exports. The file starts with a license banner generated from the package's own metadata:
@@ -32,7 +32,7 @@ iifeify is for hosts that accept only classic scripts and cannot `import` anythi
32
32
  Run it directly with `npx`, no installation required:
33
33
 
34
34
  ```bash
35
- npx iifeify <package>
35
+ npx -y iifeify <package>
36
36
  ```
37
37
 
38
38
  Or install it globally:
@@ -62,7 +62,18 @@ npx iifeify <package>[@version] [options]
62
62
  | `-h`, `--help` | Show the help | |
63
63
  | `-v`, `--version` | Show the version | |
64
64
 
65
- The exit code is 0 on success, 1 if the package cannot be converted and 2 for a wrong command line.
65
+ While it runs, iifeify prints each step and how long it took on stderr, and the result on stdout:
66
+
67
+ ```text
68
+ Installing vanilla-jsoneditor@3.13.0 with npm... done in 3.4s
69
+ Bundling vanilla-jsoneditor 3.13.0 with esbuild... done in 555ms
70
+ Wrote vanilla-jsoneditor.iife.js (1.21 MB)
71
+ package vanilla-jsoneditor 3.13.0 - ISC
72
+ entry standalone.js
73
+ global VanillaJsonEditor
74
+ ```
75
+
76
+ A step that fails ends with `failed`, followed by the reason. The exit code is 0 on success, 1 if the package cannot be converted and 2 for a wrong command line.
66
77
 
67
78
  ### How it works
68
79
 
package/bin/cli.js CHANGED
@@ -24,7 +24,7 @@ cli.command('<package>', 'Turn an npm package into one classic browser script th
24
24
  .example(' $ iifeify ms')
25
25
  .example(' $ iifeify vanilla-jsoneditor@3.13.0 --entry standalone.js --global-name VanillaJsonEditor')
26
26
  .action(async (spec, options) => {
27
- report(await convert({
27
+ const result = await convert({
28
28
  spec: String(spec),
29
29
  entry: text(options.entry),
30
30
  globalName: text(options.globalName),
@@ -32,11 +32,43 @@ cli.command('<package>', 'Turn an npm package into one classic browser script th
32
32
  minify: options.minify,
33
33
  target: text(options.target)?.split(','),
34
34
  notices: options.notices,
35
- }));
35
+ onStep: step => startStep(step.name === 'install'
36
+ ? `Installing ${step.spec} with npm`
37
+ : `Bundling ${step.label} with esbuild`),
38
+ });
39
+ endStep('done');
40
+ report(result);
36
41
  });
37
42
  cli.help();
38
43
  cli.version(version);
39
44
 
45
+ // While a conversion runs, each step gets a line on stderr, like the warnings: what is being done,
46
+ // then how long it took, or that it failed. stdout gets the result only.
47
+ /** @type {number | undefined} */
48
+ let stepStartedAt;
49
+
50
+ /** @param {string} description */
51
+ function startStep(description) {
52
+ endStep('done');
53
+ process.stderr.write(`${description}... `);
54
+ stepStartedAt = performance.now();
55
+ }
56
+
57
+ /** @param {'done' | 'failed'} outcome */
58
+ function endStep(outcome) {
59
+ if (stepStartedAt === undefined) {
60
+ return;
61
+ }
62
+ const ms = performance.now() - stepStartedAt;
63
+ stepStartedAt = undefined;
64
+ if (outcome === 'failed') {
65
+ process.stderr.write('failed\n\n'); // the error follows after a blank line
66
+ return;
67
+ }
68
+ const took = ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
69
+ process.stderr.write(`done in ${took}\n`);
70
+ }
71
+
40
72
  /** @param {Awaited<ReturnType<typeof convert>>} result */
41
73
  function report({ files, bytes, packages, entry, globalName, warnings, hint }) {
42
74
  const [root, ...dependencies] = packages;
@@ -79,6 +111,7 @@ try {
79
111
  cli.parse(process.argv, { run: false });
80
112
  await cli.runMatchedCommand();
81
113
  } catch (error) {
114
+ endStep('failed');
82
115
  // cac does not export the class of the errors it throws for a wrong command line.
83
116
  if (error instanceof UsageError || error.name === 'CACError') {
84
117
  console.error(`${error.message}\nRun "iifeify --help" for the options.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iifeify",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Turn any browser-compatible npm package into a single classic script (an IIFE that defines one global), with a generated license banner.",
5
5
  "license": "MIT",
6
6
  "author": "Florian Noever",
package/src/convert.js CHANGED
@@ -7,6 +7,12 @@ import { bundle, checkOptions } from './bundle.js';
7
7
  import { installPackage, parseSpec } from './install.js';
8
8
  import { bundledPackages } from './packages.js';
9
9
 
10
+ /**
11
+ * @typedef {{ name: 'install', spec: string } | { name: 'bundle', label: string }} Step
12
+ * a step of a conversion: npm installs the package as the user named it, or esbuild bundles
13
+ * the installed package, labelled with its name and version such as "ms 2.1.3"
14
+ */
15
+
10
16
  /**
11
17
  * Installs an npm package into a temporary folder and writes it as one classic browser script.
12
18
  *
@@ -18,10 +24,11 @@ import { bundledPackages } from './packages.js';
18
24
  * @param {boolean} [options.minify]
19
25
  * @param {string[]} [options.target] esbuild targets such as ["es2020"]
20
26
  * @param {boolean} [options.notices] also write the full license texts next to the script
27
+ * @param {(step: Step) => void} [options.onStep] called as each step starts, to report progress
21
28
  * @returns {Promise<{ files: { js: string, css?: string, notices?: string }, bytes: number,
22
29
  * packages: object[], entry: string, globalName: string, warnings: string[], hint?: string }>}
23
30
  */
24
- export async function convert({ spec, entry, globalName: requestedGlobalName, outfile, minify = true, target, notices = false }) {
31
+ export async function convert({ spec, entry, globalName: requestedGlobalName, outfile, minify = true, target, notices = false, onStep }) {
25
32
  const wanted = parseSpec(spec);
26
33
  await checkOptions({ globalName: requestedGlobalName, target });
27
34
 
@@ -29,10 +36,12 @@ export async function convert({ spec, entry, globalName: requestedGlobalName, ou
29
36
  // reports files by their real paths.
30
37
  const workDir = realpathSync.native(mkdtempSync(join(tmpdir(), 'iifeify-')));
31
38
  try {
32
- const { dir, manifest } = installPackage(wanted, workDir);
39
+ onStep?.({ name: 'install', spec });
40
+ const { dir, manifest } = await installPackage(wanted, workDir);
33
41
  const globalName = requestedGlobalName ?? defaultGlobalName(manifest.name);
34
42
 
35
43
  const label = `${manifest.name} ${manifest.version}`;
44
+ onStep?.({ name: 'bundle', label });
36
45
  const built = await bundle({ workDir, name: wanted.name, dir, label, entry, globalName, minify, target });
37
46
  const packages = bundledPackages(built.inputs, dir);
38
47
  const banner = renderBanner(packages);
package/src/install.js CHANGED
@@ -1,9 +1,13 @@
1
- import { execSync } from 'node:child_process';
1
+ import { exec } from 'node:child_process';
2
2
  import { readFileSync, realpathSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
+ import { promisify } from 'node:util';
4
5
 
5
6
  import { ConversionError, UsageError } from './errors.js';
6
7
 
8
+ // Asynchronous, so that the caller can report progress while npm works.
9
+ const run = promisify(exec);
10
+
7
11
  // npm's rule for package names, including old names with capital letters.
8
12
  const PACKAGE_NAME = /^(@[a-z0-9~-][a-z0-9._~-]*\/)?[a-z0-9~-][a-z0-9._~-]*$/i;
9
13
 
@@ -29,9 +33,9 @@ export function parseSpec(spec) {
29
33
  /**
30
34
  * Installs the package and its dependencies into workDir/node_modules.
31
35
  *
32
- * @returns {{ dir: string, manifest: object }} the package's folder and its package.json
36
+ * @returns {Promise<{ dir: string, manifest: object }>} the package's folder and its package.json
33
37
  */
34
- export function installPackage({ name, range }, workDir) {
38
+ export async function installPackage({ name, range }, workDir) {
35
39
  // A package.json of our own makes workDir the project root. Without one, npm installs into the
36
40
  // nearest parent folder that has a package.json, and the temp folder lies inside the user
37
41
  // profile. It also keeps the range off the command line: on Windows npm starts through cmd.exe,
@@ -39,7 +43,7 @@ export function installPackage({ name, range }, workDir) {
39
43
  writeFileSync(join(workDir, 'package.json'), JSON.stringify({ private: true, dependencies: { [name]: range } }));
40
44
 
41
45
  try {
42
- execSync(INSTALL, { cwd: workDir, stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8', windowsHide: true });
46
+ await run(INSTALL, { cwd: workDir, encoding: 'utf8', windowsHide: true, maxBuffer: 64 * 1024 * 1024 });
43
47
  } catch (error) {
44
48
  const output = String(error.stderr ?? '').trim() || error.message;
45
49
  throw new ConversionError(`Could not install ${name}@${range}.\n\n${output}`);