vendeps 1.0.1 → 1.0.2

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.
Files changed (4) hide show
  1. package/README.md +79 -63
  2. package/index.js +60 -42
  3. package/package.json +50 -44
  4. package/parser.js +56 -38
package/README.md CHANGED
@@ -14,12 +14,17 @@ import confetti from './dependencies/canvas-confetti.js'
14
14
 
15
15
  Modern browsers support ES modules natively, but most npm packages still ship CommonJS or split their code across dozens of files. Using a CDN solves the format problem but introduces new ones:
16
16
 
17
- - **Security** — Every page load fetches code from a third-party server, opening the door to man-in-the-middle attacks or CDN compromises. Vendeps allows you to version-control your dependencies for audit.
18
- - **Reliability** — Your app's uptime becomes coupled to the CDN's uptime. Vendeps decouples your app's uptime from any CDN.
19
- - **Reproducibility** — CDN URLs can change, disappear, or serve different versions. Vendeps ensures that your app always uses the same version of a dependency.
20
- - **Predictability** — You know exactly what code your users are running. Vendeps ensures that the exact same dependency you used during development is used in production.
17
+ - **Security** — Every page load fetches code from a third-party server, opening the door to supply-chain, man-in-the-middle, and XSS attacks.
18
+ - **Reliability** — Your app's uptime becomes coupled to the CDN's uptime (and if you depend on multiple CDNs, [it's even worse](https://blog.alexewerlof.com/p/composite-slo)).
19
+ - **Reproducibility** — CDN URLs can change, disappear, or serve different versions.
20
+ - **Predictability** — You don't always know exactly what code your users are running.
21
21
 
22
- `vendeps` takes a different approach: convert each dependency into a single, self-contained `.js` file that you **check into your repository**. Your app ships everything it needs — no external requests at runtime, no surprises.
22
+ `vendeps` takes a different approach: convert each dependency into a single, self-contained `.js` file that you **check into your repository**. Your app ships everything it needs — no external requests at runtime to other servers, no surprises.
23
+
24
+ - **Security** — Vendeps allows you to version-control your dependencies for audit.
25
+ - **Reliability** — Vendeps decouples your app's uptime from external CDNs.
26
+ - **Reproducibility** — Vendeps ensures that your app always uses the same version of a dependency.
27
+ - **Predictability** — Vendeps ensures that the exact same dependency you used during development is used in production.
23
28
 
24
29
  ## Quick Start
25
30
 
@@ -31,18 +36,50 @@ npx vendeps
31
36
 
32
37
  That's it. A `dependencies/` folder appears with one `.js` file per dependency. Point your `<script type="module">` at them and go.
33
38
 
34
- ### Even better: use Import Maps
39
+ ### npm scripts
40
+
41
+ If you prefer to install it as a dev dependency:
42
+
43
+ ```bash
44
+ npm install --save-dev vendeps
45
+ ```
46
+
47
+ And then:
48
+
49
+ ```json
50
+ {
51
+ "scripts": {
52
+ "build": "vendeps"
53
+ }
54
+ }
55
+ ```
56
+
57
+ If you want it to run automatically after `npm install`, you can add a `postinstall` script:
58
+
59
+ ```json
60
+ {
61
+ "scripts": {
62
+ "postinstall": "vendeps"
63
+ }
64
+ }
65
+ ```
66
+
67
+ ⚠️ **Dependency drift** — The vendored bundles are snapshots of whatever versions are installed at build time. If you update a dependency version in `package.json` (or run `npm update`), remember to re-run `npx vendeps` (or trigger a fresh `npm ci`) so the bundles stay in sync. Checking in the `dependencies/` folder helps catch drift — any version bump will show up as a diff in your commit.
68
+
69
+ ℹ️ **Optional vendor check in** It is recommended to check in the `dependencies/` folder to version control but it's not a requirement for vendeps to work. All it does is to create bundles for your dependencies. So you may as well `.gitignore` the `dependencies/` folder and rely on `postinstall` to create the bundles after `npm install` or `npm ci`.
70
+
71
+ ### Import Maps
35
72
 
36
73
  Instead of rewriting your imports to point at `./dependencies/**/*.js`, you can use a [browser import map](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script/type/importmap) so your source code keeps using bare specifiers — exactly like Node.js:
37
74
 
38
75
  ```html
39
76
  <script type="importmap">
40
- {
41
- "imports": {
42
- "lit-html": "./dependencies/lit-html.js",
43
- "canvas-confetti": "./dependencies/canvas-confetti.js"
44
- }
45
- }
77
+ {
78
+ "imports": {
79
+ "lit-html": "./dependencies/lit-html.js",
80
+ "canvas-confetti": "./dependencies/canvas-confetti.js"
81
+ }
82
+ }
46
83
  </script>
47
84
  <script type="module" src="app.js"></script>
48
85
  ```
@@ -57,41 +94,18 @@ import confetti from 'canvas-confetti'
57
94
 
58
95
  The browser resolves the bare specifiers through the import map, so your code stays portable between Node.js and the browser with zero modifications.
59
96
 
60
- ## Installation
61
-
62
- If you prefer to install it as a dev dependency:
63
-
64
- ```bash
65
- npm install --save-dev vendeps
66
- ```
67
-
68
- ### Automate with `postinstall`
69
-
70
- To ensure the `dependencies/` folder is always up to date after `npm install` or `npm ci`, add a `postinstall` script to your `package.json`:
71
-
72
- ```json
73
- {
74
- "scripts": {
75
- "postinstall": "vendeps --minify"
76
- }
77
- }
78
- ```
79
-
80
- Now every `npm ci` on your CI server or a fresh clone will automatically populate the `dependencies/` folder with minified bundles — no extra step to remember.
81
-
82
- > ⚠️ **Dependency drift** — The vendored bundles are snapshots of whatever versions are installed at build time. If you update a dependency version in `package.json` (or run `npm update`), remember to re-run `npx vendeps` (or trigger a fresh `npm ci`) so the bundles stay in sync. Checking in the `dependencies/` folder helps catch drift — any version bump will show up as a diff in your commit.
83
-
84
97
  ## CLI Options
85
98
 
86
99
  ```
87
100
  npx vendeps [options]
88
101
  ```
89
102
 
90
- | Option | Description |
91
- |---|---|
92
- | `--minify` | Minify the output bundles. Also activates automatically when `NODE_ENV=production`. |
103
+ | Option | Description |
104
+ | ---------------- | -------------------------------------------------------------------------------------------------------- |
105
+ | `--minify` | Minify the output bundles. |
93
106
  | `--target <dir>` | Output to `<dir>/` instead of the default `dependencies/`. The directory is created if it doesn't exist. |
94
- | `--help` | Show usage information and exit. |
107
+ | `--src <file>` | Path to the `package.json` file (defaults to `./package.json`). |
108
+ | `--help` | Show usage information and exit. |
95
109
 
96
110
  ### Examples
97
111
 
@@ -99,7 +113,7 @@ npx vendeps [options]
99
113
  # Bundle everything into dependencies/
100
114
  npx vendeps
101
115
 
102
- # Minified production bundles
116
+ # Minified bundles
103
117
  npx vendeps --minify
104
118
 
105
119
  # Output to a custom folder
@@ -121,38 +135,40 @@ You can customize this behavior per-dependency via the `"vendeps"` key in your `
121
135
 
122
136
  ```json
123
137
  {
124
- "dependencies": {
125
- "lit-html": "^3.0.0",
126
- "chart.js": "^4.0.0",
127
- "some-internal-tool": "^1.0.0",
128
- "@huggingface/transformers": "^3.0.0"
129
- },
130
- "vendeps": {
131
- "some-internal-tool": null,
132
- "chart.js": {
133
- "export": "{ Chart }",
134
- "define": ["PRODUCTION"]
138
+ "dependencies": {
139
+ "lit-html": "^3.0.0",
140
+ "chart.js": "^4.0.0",
141
+ "onnxruntime-node": "1.24.2",
142
+ "@huggingface/transformers": "^3.0.0"
143
+ },
144
+ "vendeps": {
145
+ // Skip generating bundles for this node-only package in an isomorphic project
146
+ "onnxruntime-node": null,
147
+ // Only export Chart from chart.js and define PRODUCTION
148
+ "chart.js": {
149
+ "export": "{ Chart }",
150
+ "define": ["PRODUCTION"]
151
+ }
135
152
  }
136
- }
137
153
  }
138
154
  ```
139
155
 
140
156
  ### Config Options
141
157
 
142
- | Config value | Effect |
143
- |---|---|
144
- | *(not set)* | Default — `export * from '<packageName>'` |
145
- | `null` or `false` | **Skip** this dependency entirely. |
158
+ | Config value | Effect |
159
+ | ------------------- | ------------------------------------------------------- |
160
+ | _(not set)_ | Default — `export * from '<packageName>'` |
161
+ | `null` or `false` | **Skip** this dependency entirely. |
146
162
  | `"path/to/file.js"` | Use a custom import source instead of the package name. |
147
- | `{ ... }` | Full configuration object (see below). |
163
+ | `{ ... }` | Full configuration object (see below). |
148
164
 
149
165
  ### Full Config Object
150
166
 
151
- | Key | Type | Default | Description |
152
- |---|---|---|---|
153
- | `from` | `string` | package name | The import source path. |
154
- | `export` | `string` | `"*"` | The export style, e.g. `"*"`, `"{ Chart }"`, `"default"`. |
155
- | `define` | `object` or `string[]` | `{}` | [esbuild `define`](https://esbuild.github.io/api/#define) replacements. When an array of strings, each is defined as `"true"`. |
167
+ | Key | Type | Default | Description |
168
+ | -------- | ---------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------ |
169
+ | `from` | `string` | package name | The import source path. |
170
+ | `export` | `string` | `"*"` | The export style, e.g. `"*"`, `"{ Chart }"`, `"default"`. |
171
+ | `define` | `object` or `string[]` | `{}` | [esbuild `define`](https://esbuild.github.io/api/#define) replacements. When an array of strings, each is defined as `"true"`. |
156
172
 
157
173
  ### Scoped Packages
158
174
 
package/index.js CHANGED
@@ -1,64 +1,82 @@
1
1
  #!/usr/bin/env node
2
+
2
3
  import { mkdir, readFile } from 'node:fs/promises'
3
- import { join } from 'node:path'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { hideBin } from 'yargs/helpers'
6
+ import yargs from 'yargs'
4
7
  import * as esbuild from 'esbuild'
5
- import { toEsbuildOptionsArr } from './parser.js'
6
-
7
- const CONFIG_KEY = 'vendeps'
8
- const DEFAULT_TARGET_DIR = 'dependencies'
9
8
 
10
- function getTarget(argv) {
11
- const i = argv.indexOf('--target')
12
- return i !== -1 && argv[i + 1] ? argv[i + 1] : DEFAULT_TARGET_DIR
9
+ async function loadJson(filePath) {
10
+ try {
11
+ const content = await readFile(filePath, 'utf-8')
12
+ return JSON.parse(content)
13
+ } catch (err) {
14
+ throw new Error(`❌ Failed to load JSON from ${filePath}: ${err.message}`)
15
+ }
13
16
  }
14
17
 
15
- const targetDir = getTarget(process.argv)
18
+ import { fileURLToPath } from 'node:url'
19
+ import { packageJsonToEsbuildOptions } from './parser'
16
20
 
17
- if (process.argv.includes('--help')) {
18
- console.log([
19
- 'vendeps — Convert npm dependencies to ESM bundles ready for the browser environment.',
20
- '',
21
- 'Usage: npx vendeps [options]',
22
- '',
23
- 'Options:',
24
- ` --target <dir> Output directory (default: "${DEFAULT_TARGET_DIR}")`,
25
- ' --minify Minify the output bundles (default: false, also activates when NODE_ENV=production)',
26
- ' --help Show this help message',
27
- '',
28
- 'Reads "dependencies" from package.json and bundles each one into <target>/<name>.js.',
29
- 'Per-dependency config can be set via the "vendeps" key in package.json.',
30
- '',
31
- 'Scoped packages (e.g. @huggingface/transformers) are output as <target>/@scope/<name>.js.',
32
- ].join('\n'))
33
- process.exit(0)
34
- }
21
+ const vendepsPackageJson = await loadJson(resolve(dirname(fileURLToPath(import.meta.url)), 'package.json'))
35
22
 
36
- async function getDependenciesAndConfig(path) {
37
- const { dependencies, [CONFIG_KEY]: vendepsConfig } = JSON.parse(await readFile(path, 'utf-8'))
38
- return { dependencies, vendepsConfig }
39
- }
23
+ const CONFIG_KEY = vendepsPackageJson.name
24
+ const DEFAULT_TARGET_DIR = './dependencies'
25
+ const DEFAULT_SRC_FILE = './package.json'
40
26
 
41
- async function readConfig(resolveDir, minify) {
42
- const { dependencies, vendepsConfig } = await getDependenciesAndConfig(join(resolveDir, 'package.json'))
43
- return toEsbuildOptionsArr(Object.keys(dependencies), vendepsConfig, resolveDir, minify, targetDir).filter(Boolean)
44
- }
27
+ const argv = yargs(hideBin(process.argv))
28
+ .usage(`Usage: npx ${vendepsPackageJson.name} [options]`)
29
+ .option('src', {
30
+ alias: 's',
31
+ type: 'string',
32
+ describe: `Path to the package.json file where dependencies and the optional ${CONFIG_KEY} config are located`,
33
+ default: DEFAULT_SRC_FILE,
34
+ })
35
+ .option('target', {
36
+ alias: 't',
37
+ type: 'string',
38
+ describe: 'Output directory (it will be created if it does not exist)',
39
+ default: DEFAULT_TARGET_DIR,
40
+ })
41
+ .option('minify', {
42
+ type: 'boolean',
43
+ describe: 'Minify the output bundles',
44
+ default: false,
45
+ })
46
+ .help('help')
47
+ .alias('help', 'h')
48
+ .epilog(vendepsPackageJson.description).argv
49
+
50
+ const targetDir = resolve(process.cwd(), argv.target)
51
+ const srcFile = resolve(process.cwd(), argv.src)
52
+ const nodeModulesDir = dirname(srcFile)
45
53
 
46
54
  async function main() {
47
- const minify = process.argv.includes('--minify') || process.env.NODE_ENV === 'production'
55
+ // Only use --minify flag
56
+ const minify = argv.minify
48
57
 
49
- console.time('Read config')
50
- const optionsArr = await readConfig(process.cwd(), minify)
51
- console.timeEnd('Read config')
58
+ console.time(`Read and parse ${srcFile}`)
59
+ const optionsArr = await packageJsonToEsbuildOptions(
60
+ await loadJson(srcFile),
61
+ CONFIG_KEY,
62
+ nodeModulesDir,
63
+ targetDir,
64
+ minify,
65
+ )
66
+ console.timeEnd(`Read and parse ${srcFile}`)
52
67
  if (optionsArr.length === 0) {
53
68
  console.info('🫙 No dependencies to process. Exiting.')
54
- process.exit(0)
69
+ return
55
70
  }
56
71
 
57
- await mkdir(targetDir, { recursive: true });
72
+ await mkdir(targetDir, { recursive: true })
58
73
  console.info(`🏃 Updating ${targetDir} for ${optionsArr.length} dependency(ies). Minify: ${minify}`)
59
74
 
60
75
  console.time('Build')
61
- await Promise.all(optionsArr.map((options) => esbuild.build(options)))
76
+ // await Promise.all(optionsArr.map((options) => esbuild.build(options)))
77
+ for (const options of optionsArr) {
78
+ await esbuild.build(options)
79
+ }
62
80
  console.timeEnd('Build')
63
81
  console.log(`🎉 Updated ${targetDir} dir.`)
64
82
  }
package/package.json CHANGED
@@ -1,46 +1,52 @@
1
1
  {
2
- "name": "vendeps",
3
- "version": "1.0.1",
4
- "description": "Uses esbuild to convert npm packages to ESM bundles for the browser.",
5
- "main": "index.js",
6
- "bin": {
7
- "vendeps": "index.js"
8
- },
9
- "files": [
10
- "index.js",
11
- "parser.js",
12
- "README.md",
13
- "LICENSE"
14
- ],
15
- "type": "module",
16
- "scripts": {
17
- "test": "node --test"
18
- },
19
- "keywords": [
20
- "esm",
21
- "esbuild",
22
- "bundle",
23
- "browser",
24
- "dependencies",
25
- "modules",
26
- "import",
27
- "cdn-alternative"
28
- ],
29
- "author": "alexewerlof",
30
- "license": "MIT",
31
- "repository": {
32
- "type": "git",
33
- "url": "git+https://github.com/alexewerlof/vendeps.git"
34
- },
35
- "bugs": {
36
- "url": "https://github.com/alexewerlof/vendeps/issues"
37
- },
38
- "homepage": "https://github.com/alexewerlof/vendeps#readme",
39
- "engines": {
40
- "node": ">=24"
41
- },
42
- "dependencies": {
43
- "esbuild": "^0.27.3",
44
- "jty": "^4.0.0"
45
- }
2
+ "name": "vendeps",
3
+ "version": "1.0.2",
4
+ "description": "Uses esbuild to convert npm packages to ESM bundles for the browser.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "vendeps": "index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "parser.js",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "type": "module",
16
+ "scripts": {
17
+ "test": "node --test",
18
+ "fmt": "prettier --write .",
19
+ "preversion": "npm run fmt"
20
+ },
21
+ "keywords": [
22
+ "esm",
23
+ "esbuild",
24
+ "bundle",
25
+ "browser",
26
+ "dependencies",
27
+ "modules",
28
+ "import",
29
+ "cdn-alternative"
30
+ ],
31
+ "author": "alexewerlof",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/alexewerlof/vendeps.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/alexewerlof/vendeps/issues"
39
+ },
40
+ "homepage": "https://github.com/alexewerlof/vendeps#readme",
41
+ "engines": {
42
+ "node": ">=24"
43
+ },
44
+ "dependencies": {
45
+ "esbuild": "^0.27.3",
46
+ "jty": "^4.0.0",
47
+ "yargs": "^18.0.0"
48
+ },
49
+ "devDependencies": {
50
+ "prettier": "^3.8.1"
51
+ }
46
52
  }
package/parser.js CHANGED
@@ -1,6 +1,6 @@
1
- import { isArr, isBool, isObj, isStr } from "jty"
2
- import { join } from "node:path"
3
- const skipConfig = [null, false]
1
+ import { isArr, isBool, isObj, isStr } from 'jty'
2
+ import { join } from 'node:path'
3
+ const SKIP_CONFIG_VALUES = [null, false]
4
4
 
5
5
  function configObj(frm, exp = '*', def = {}) {
6
6
  if (!isStr(frm)) {
@@ -31,7 +31,9 @@ function configObj(frm, exp = '*', def = {}) {
31
31
  for (let i = 0; i < arr.length; i++) {
32
32
  const item = arr[i]
33
33
  if (!isStr(item)) {
34
- throw new TypeError(`When 'define' is an array, all items should be strings. Got ${item} (${typeof item}) at index ${i}.`)
34
+ throw new TypeError(
35
+ `When 'define' is an array, all items should be strings. Got ${item} (${typeof item}) at index ${i}.`,
36
+ )
35
37
  }
36
38
  def[item] = String(true)
37
39
  }
@@ -65,29 +67,28 @@ function normalizedConfig(name, config) {
65
67
  }
66
68
 
67
69
  function toEsbuildOptions(name, config, resolveDir, minify, outfile) {
68
- const { contents, define } = normalizedConfig(name, config)
69
- console.log(`${name}: ${contents}`)
70
- return {
71
- bundle: true,
72
- minify,
73
- format: 'esm',
74
- define,
75
- stdin: {
76
- contents,
77
- resolveDir,
78
- loader: 'js',
79
- },
80
- outfile,
70
+ try {
71
+ const { contents, define } = normalizedConfig(name, config)
72
+ console.log(`${name}: ${contents}`)
73
+ return {
74
+ bundle: true,
75
+ minify,
76
+ format: 'esm',
77
+ define,
78
+ stdin: {
79
+ contents,
80
+ resolveDir,
81
+ loader: 'js',
82
+ },
83
+ outfile,
84
+ }
85
+ } catch (err) {
86
+ err.message = `Error processing config for ${name}: ${err.message}`
87
+ throw err
81
88
  }
82
89
  }
83
90
 
84
- export const _test = {
85
- configObj,
86
- normalizedConfig,
87
- toEsbuildOptions,
88
- }
89
-
90
- export function toEsbuildOptionsArr(names, vendepsConfig = {}, resolveDir, minify, targetDir) {
91
+ function toEsbuildOptionsArr(names, vendepsConfig = {}, resolveDir, minify, targetDir) {
91
92
  if (!isArr(names)) {
92
93
  throw new TypeError(`Expected an array of names. Got ${names} (${typeof names}).`)
93
94
  }
@@ -101,17 +102,34 @@ export function toEsbuildOptionsArr(names, vendepsConfig = {}, resolveDir, minif
101
102
  throw new TypeError(`Expected 'targetDir' to be a string. Got ${targetDir} (${typeof targetDir}).`)
102
103
  }
103
104
 
104
- return names.map((name) => {
105
- const config = vendepsConfig?.[name];
106
- if (skipConfig.includes(config)) {
107
- console.warn(`Skipping ${name} as its config is ${config}.`)
108
- return null
109
- }
110
- try {
111
- return toEsbuildOptions(name, config, resolveDir, minify, join(targetDir, `${name}.js`))
112
- } catch (err) {
113
- err.message = `Processing config for ${name}: ${err.message}`
114
- throw err
115
- }
116
- })
117
- }
105
+ return names.map((name) =>
106
+ toEsbuildOptions(name, vendepsConfig?.[name], resolveDir, minify, join(targetDir, `${name}.js`)),
107
+ )
108
+ }
109
+
110
+ export async function packageJsonToEsbuildOptions(packageJson, configKey, resolveDir, targetDir, minify) {
111
+ if (!isObj(packageJson)) {
112
+ throw new TypeError(`Invalid package.json object`)
113
+ }
114
+ const { dependencies, [configKey]: vendepsConfig } = packageJson
115
+ if (!isObj(dependencies)) {
116
+ throw new TypeError(`No 'dependencies' object found`)
117
+ }
118
+
119
+ const dependencyNames = Object.keys(dependencies).filter(
120
+ (name) => vendepsConfig && !SKIP_CONFIG_VALUES.includes(vendepsConfig?.[name]),
121
+ )
122
+
123
+ if (dependencyNames.length === 0) {
124
+ throw new Error('No dependencies to process after filtering with config.')
125
+ }
126
+
127
+ return toEsbuildOptionsArr(dependencyNames, vendepsConfig, resolveDir, minify, targetDir).filter(Boolean)
128
+ }
129
+
130
+ export const _test = {
131
+ configObj,
132
+ normalizedConfig,
133
+ toEsbuildOptions,
134
+ toEsbuildOptionsArr,
135
+ }