vendeps 1.0.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.
Files changed (5) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +171 -0
  3. package/index.js +69 -0
  4. package/package.json +46 -0
  5. package/parser.js +117 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 alexewerlof
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # vendeps
2
+
3
+ **One ESM file per dependency, ready for the browser.**
4
+ Vendor your dependencies for added reliability, security and simplicity.
5
+
6
+ `vendeps` reads the `dependencies` in your `package.json`, uses [esbuild](https://esbuild.github.io/) to bundle each one into a single ESM file, and drops the results into a `dependencies/` folder. Your browser code can then import them with plain `import` statements — no bundler, no build step, no CDN required.
7
+
8
+ ```js
9
+ import { html, render } from './dependencies/lit-html.js'
10
+ import confetti from './dependencies/canvas-confetti.js'
11
+ ```
12
+
13
+ ## Why?
14
+
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
+
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.
19
+ - **Reproducibility** — CDN URLs can change, disappear, or serve different versions.
20
+ - **Predictability** — You know exactly what code your users are running.
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.
23
+
24
+ ## Quick Start
25
+
26
+ No installation required. In any project that has a `package.json` with `dependencies`:
27
+
28
+ ```bash
29
+ npx vendeps
30
+ ```
31
+
32
+ That's it. A `dependencies/` folder appears with one `.js` file per dependency. Point your `<script type="module">` at them and go.
33
+
34
+ ## Installation
35
+
36
+ If you prefer to install it as a dev dependency:
37
+
38
+ ```bash
39
+ npm install --save-dev vendeps
40
+ ```
41
+
42
+ ### Automate with `postinstall`
43
+
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`:
45
+
46
+ ```json
47
+ {
48
+ "scripts": {
49
+ "postinstall": "vendeps --minify"
50
+ }
51
+ }
52
+ ```
53
+
54
+ 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.
55
+
56
+ ## CLI Options
57
+
58
+ ```
59
+ npx vendeps [options]
60
+ ```
61
+
62
+ | Option | Description |
63
+ |---|---|
64
+ | `--minify` | Minify the output bundles. Also activates automatically when `NODE_ENV=production`. |
65
+ | `--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. |
67
+
68
+ ### Examples
69
+
70
+ ```bash
71
+ # Bundle everything into dependencies/
72
+ npx vendeps
73
+
74
+ # Minified production bundles
75
+ npx vendeps --minify
76
+
77
+ # Output to a custom folder
78
+ npx vendeps --target vendor
79
+
80
+ # Combine options
81
+ npx vendeps --minify --target lib/vendor
82
+ ```
83
+
84
+ ## Per-Dependency Configuration
85
+
86
+ By default, `vendeps` re-exports everything from each dependency:
87
+
88
+ ```js
89
+ // generated: export * from 'some-package'
90
+ ```
91
+
92
+ You can customize this behavior per-dependency via the `"vendeps"` key in your `package.json`:
93
+
94
+ ```json
95
+ {
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"]
107
+ }
108
+ }
109
+ }
110
+ ```
111
+
112
+ ### Config Options
113
+
114
+ | Config value | Effect |
115
+ |---|---|
116
+ | *(not set)* | Default — `export * from '<packageName>'` |
117
+ | `null` or `false` | **Skip** this dependency entirely. |
118
+ | `"path/to/file.js"` | Use a custom import source instead of the package name. |
119
+ | `{ ... }` | Full configuration object (see below). |
120
+
121
+ ### Full Config Object
122
+
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"`. |
128
+
129
+ ### Scoped Packages
130
+
131
+ Scoped packages like `@huggingface/transformers` are output with their scope directory preserved:
132
+
133
+ ```
134
+ dependencies/@huggingface/transformers.js
135
+ ```
136
+
137
+ Import them the same way:
138
+
139
+ ```js
140
+ import { pipeline } from './dependencies/@huggingface/transformers.js'
141
+ ```
142
+
143
+ ## Recommended Workflow
144
+
145
+ 1. **Add your npm dependencies** as usual with `npm install`.
146
+ 2. **Run `npx vendeps`** (or let `postinstall` handle it).
147
+ 3. **Check in the `dependencies/` folder** to version control.
148
+ 4. **Import from `dependencies/`** in your browser JS files.
149
+
150
+ ```
151
+ my-project/
152
+ ├── package.json
153
+ ├── dependencies/ ← checked into git
154
+ │ ├── lit-html.js
155
+ │ ├── canvas-confetti.js
156
+ │ └── @huggingface/
157
+ │ └── transformers.js
158
+ ├── index.html
159
+ └── app.js ← import from ./dependencies/
160
+ ```
161
+
162
+ By committing the `dependencies/` folder, you get:
163
+
164
+ - **Self-contained deployments** — no install step needed on the server, no external fetches at runtime.
165
+ - **No CDN dependency** — your app works even if every CDN on the internet goes down.
166
+ - **Auditability** — the exact code your users receive is visible in your repo.
167
+ - **Reproducibility** — every clone, every checkout, every deploy uses the exact same dependency code.
168
+
169
+ ## License
170
+
171
+ [MIT](LICENSE)
package/index.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readFile } from 'node:fs/promises'
3
+ import { join } from 'node:path'
4
+ import * as esbuild from 'esbuild'
5
+ import { toEsbuildOptionsArr } from './parser.js'
6
+
7
+ const CONFIG_KEY = 'vendeps'
8
+ const DEFAULT_TARGET_DIR = 'dependencies'
9
+
10
+ function getTarget(argv) {
11
+ const i = argv.indexOf('--target')
12
+ return i !== -1 && argv[i + 1] ? argv[i + 1] : DEFAULT_TARGET_DIR
13
+ }
14
+
15
+ const targetDir = getTarget(process.argv)
16
+
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
+ }
35
+
36
+ async function getDependenciesAndConfig(path) {
37
+ const { dependencies, [CONFIG_KEY]: vendepsConfig } = JSON.parse(await readFile(path, 'utf-8'))
38
+ return { dependencies, vendepsConfig }
39
+ }
40
+
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
+ }
45
+
46
+ async function main() {
47
+ const minify = process.argv.includes('--minify') || process.env.NODE_ENV === 'production'
48
+
49
+ console.time('Read config')
50
+ const optionsArr = await readConfig(process.cwd(), minify)
51
+ console.timeEnd('Read config')
52
+ if (optionsArr.length === 0) {
53
+ console.info('🫙 No dependencies to process. Exiting.')
54
+ process.exit(0)
55
+ }
56
+
57
+ await mkdir(targetDir, { recursive: true });
58
+ console.info(`🏃 Updating ${targetDir} for ${optionsArr.length} dependency(ies). Minify: ${minify}`)
59
+
60
+ console.time('Build')
61
+ await Promise.all(optionsArr.map((options) => esbuild.build(options)))
62
+ console.timeEnd('Build')
63
+ console.log(`🎉 Updated ${targetDir} dir.`)
64
+ }
65
+
66
+ main().catch((err) => {
67
+ console.error(`❌ ${err}`)
68
+ process.exitCode = 1
69
+ })
package/package.json ADDED
@@ -0,0 +1,46 @@
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
+ }
package/parser.js ADDED
@@ -0,0 +1,117 @@
1
+ import { isArr, isBool, isObj, isStr } from "jty"
2
+ import { join } from "node:path"
3
+ const skipConfig = [null, false]
4
+
5
+ function configObj(frm, exp = '*', def = {}) {
6
+ if (!isStr(frm)) {
7
+ throw new TypeError(`'from' must be a string. Got ${frm} (${typeof frm})`)
8
+ }
9
+ if (frm.length === 0) {
10
+ throw new RangeError(`'from' cannot be an empty string.`)
11
+ }
12
+ // Name cannot contain quotation marks ' or ", as they would break the generated code.
13
+ if (frm.includes('"') || frm.includes("'")) {
14
+ throw new SyntaxError(`'from' cannot contain quotation marks. Got ${frm}`)
15
+ }
16
+
17
+ if (!isStr(exp)) {
18
+ throw new TypeError(`'export' should be a string. Got ${exp} (${typeof exp}).`)
19
+ }
20
+ if (exp.length === 0) {
21
+ throw new RangeError(`'export' cannot be an empty string.`)
22
+ }
23
+
24
+ if (!isObj(def)) {
25
+ throw new TypeError(`'define' should be an object. Got ${def} (${typeof def}).`)
26
+ }
27
+ if (Array.isArray(def)) {
28
+ const arr = def
29
+ def = {}
30
+
31
+ for (let i = 0; i < arr.length; i++) {
32
+ const item = arr[i]
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}.`)
35
+ }
36
+ def[item] = String(true)
37
+ }
38
+ }
39
+
40
+ const ret = {
41
+ contents: `export ${exp} from '${frm}'`,
42
+ define: def || {},
43
+ }
44
+ return ret
45
+ }
46
+
47
+ function normalizedConfig(name, config) {
48
+ switch (typeof config) {
49
+ case 'undefined':
50
+ return configObj(name)
51
+ case 'string':
52
+ return configObj(config)
53
+ case 'object':
54
+ if (config === null) {
55
+ throw new TypeError(`When specified, config should be a string or object, got null.`)
56
+ }
57
+ if (Array.isArray(config)) {
58
+ throw new TypeError(`When specified, config should be a string or object, got array.`)
59
+ }
60
+ const { export: exp, from: frm = name, define: def } = config
61
+ return configObj(frm, exp, def)
62
+ default:
63
+ throw new TypeError(`When specified, config should be a string or object, got ${typeof config}.`)
64
+ }
65
+ }
66
+
67
+ 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,
81
+ }
82
+ }
83
+
84
+ export const _test = {
85
+ configObj,
86
+ normalizedConfig,
87
+ toEsbuildOptions,
88
+ }
89
+
90
+ export function toEsbuildOptionsArr(names, vendepsConfig = {}, resolveDir, minify, targetDir) {
91
+ if (!isArr(names)) {
92
+ throw new TypeError(`Expected an array of names. Got ${names} (${typeof names}).`)
93
+ }
94
+ if (!isStr(resolveDir)) {
95
+ throw new TypeError(`Expected 'resolveDir' to be a string. Got ${resolveDir} (${typeof resolveDir}).`)
96
+ }
97
+ if (!isBool(minify)) {
98
+ throw new TypeError(`Expected 'minify' to be a boolean. Got ${minify} (${typeof minify}).`)
99
+ }
100
+ if (!isStr(targetDir)) {
101
+ throw new TypeError(`Expected 'targetDir' to be a string. Got ${targetDir} (${typeof targetDir}).`)
102
+ }
103
+
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
+ }