vendeps 1.0.0 → 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 +86 -38
  2. package/index.js +60 -42
  3. package/package.json +51 -45
  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.
18
- - **Reliability** — Your app's uptime becomes coupled to the CDN's uptime.
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
19
  - **Reproducibility** — CDN URLs can change, disappear, or serve different versions.
20
- - **Predictability** — You know exactly what code your users are running.
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,7 +36,7 @@ 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
- ## Installation
39
+ ### npm scripts
35
40
 
36
41
  If you prefer to install it as a dev dependency:
37
42
 
@@ -39,19 +44,55 @@ If you prefer to install it as a dev dependency:
39
44
  npm install --save-dev vendeps
40
45
  ```
41
46
 
42
- ### Automate with `postinstall`
47
+ And then:
43
48
 
44
- To ensure the `dependencies/` folder is always up to date after `npm install` or `npm ci`, add a `postinstall` script to your `package.json`:
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:
45
58
 
46
59
  ```json
47
60
  {
48
- "scripts": {
49
- "postinstall": "vendeps --minify"
50
- }
61
+ "scripts": {
62
+ "postinstall": "vendeps"
63
+ }
51
64
  }
52
65
  ```
53
66
 
54
- Now every `npm ci` on your CI server or a fresh clone will automatically populate the `dependencies/` folder with minified bundlesno extra step to remember.
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 driftany 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
72
+
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:
74
+
75
+ ```html
76
+ <script type="importmap">
77
+ {
78
+ "imports": {
79
+ "lit-html": "./dependencies/lit-html.js",
80
+ "canvas-confetti": "./dependencies/canvas-confetti.js"
81
+ }
82
+ }
83
+ </script>
84
+ <script type="module" src="app.js"></script>
85
+ ```
86
+
87
+ Now your application code works without any path changes:
88
+
89
+ ```js
90
+ // app.js — same imports you'd write in Node
91
+ import { html, render } from 'lit-html'
92
+ import confetti from 'canvas-confetti'
93
+ ```
94
+
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.
55
96
 
56
97
  ## CLI Options
57
98
 
@@ -59,11 +100,12 @@ Now every `npm ci` on your CI server or a fresh clone will automatically populat
59
100
  npx vendeps [options]
60
101
  ```
61
102
 
62
- | Option | Description |
63
- |---|---|
64
- | `--minify` | Minify the output bundles. Also activates automatically when `NODE_ENV=production`. |
103
+ | Option | Description |
104
+ | ---------------- | -------------------------------------------------------------------------------------------------------- |
105
+ | `--minify` | Minify the output bundles. |
65
106
  | `--target <dir>` | Output to `<dir>/` instead of the default `dependencies/`. The directory is created if it doesn't exist. |
66
- | `--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. |
67
109
 
68
110
  ### Examples
69
111
 
@@ -71,7 +113,7 @@ npx vendeps [options]
71
113
  # Bundle everything into dependencies/
72
114
  npx vendeps
73
115
 
74
- # Minified production bundles
116
+ # Minified bundles
75
117
  npx vendeps --minify
76
118
 
77
119
  # Output to a custom folder
@@ -93,38 +135,40 @@ You can customize this behavior per-dependency via the `"vendeps"` key in your `
93
135
 
94
136
  ```json
95
137
  {
96
- "dependencies": {
97
- "lit-html": "^3.0.0",
98
- "chart.js": "^4.0.0",
99
- "some-internal-tool": "^1.0.0",
100
- "@huggingface/transformers": "^3.0.0"
101
- },
102
- "vendeps": {
103
- "some-internal-tool": null,
104
- "chart.js": {
105
- "export": "{ Chart }",
106
- "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
+ }
107
152
  }
108
- }
109
153
  }
110
154
  ```
111
155
 
112
156
  ### Config Options
113
157
 
114
- | Config value | Effect |
115
- |---|---|
116
- | *(not set)* | Default — `export * from '<packageName>'` |
117
- | `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. |
118
162
  | `"path/to/file.js"` | Use a custom import source instead of the package name. |
119
- | `{ ... }` | Full configuration object (see below). |
163
+ | `{ ... }` | Full configuration object (see below). |
120
164
 
121
165
  ### Full Config Object
122
166
 
123
- | Key | Type | Default | Description |
124
- |---|---|---|---|
125
- | `from` | `string` | package name | The import source path. |
126
- | `export` | `string` | `"*"` | The export style, e.g. `"*"`, `"{ Chart }"`, `"default"`. |
127
- | `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"`. |
128
172
 
129
173
  ### Scoped Packages
130
174
 
@@ -166,6 +210,10 @@ By committing the `dependencies/` folder, you get:
166
210
  - **Auditability** — the exact code your users receive is visible in your repo.
167
211
  - **Reproducibility** — every clone, every checkout, every deploy uses the exact same dependency code.
168
212
 
213
+ ## Limitations
214
+
215
+ > ⚠️ **CSS-only packages** — `vendeps` bundles JavaScript only. If a dependency ships exclusively CSS (e.g. `normalize.css`), it will not be handled. You will need to copy those assets manually or use a separate tool.
216
+
169
217
  ## License
170
218
 
171
219
  [MIT](LICENSE)
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.0",
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
- }
46
- }
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
+ }
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
+ }