node-link-local 1.0.0-alpha.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.
- package/README.md +88 -0
- package/bin/node-link-local.js +35 -0
- package/lib/detect-pm.js +9 -0
- package/lib/sync.js +229 -0
- package/package.json +36 -0
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# node-link-local
|
|
2
|
+
|
|
3
|
+
Sync a local npm package into another project via staged tarballs.
|
|
4
|
+
Works with **pnpm**, **yarn**, and **npm** in any combination (e.g. library uses pnpm, app uses yarn).
|
|
5
|
+
|
|
6
|
+
Replaces the need for `npm link` / `yarn link` with a workspace-safe flow: the library is packed into a tarball, staged in the app’s `.local-packages/`, and installed from that file. No symlinks, no cross-workspace protocol issues.
|
|
7
|
+
|
|
8
|
+
**Run commands from your app directory.** The app is always the current directory (cwd).
|
|
9
|
+
|
|
10
|
+
### Why node-link-local instead of yalc?
|
|
11
|
+
|
|
12
|
+
[Yalc](https://github.com/wclr/yalc) is a popular “local package” tool, but it relies on symlinks (with `yalc link`) or on copying into `.yalc/` and `file:` references. That can cause real pain when developing and running tests locally:
|
|
13
|
+
|
|
14
|
+
- **Symlinks and test runners**
|
|
15
|
+
When the consumer’s `node_modules` contains symlinks into your local package, tools like Jest can resolve the same module via two paths (symlink path vs. real path). That can lead to duplicate module instances, flaky tests, or the need for `--preserve-symlinks` and extra config. With node-link-local, the app gets a normal install from a tarball—no symlinks—so test runners see a single, consistent module tree.
|
|
16
|
+
|
|
17
|
+
- **File watchers and memory**
|
|
18
|
+
Dev servers and test runners often watch `node_modules`. If they follow symlinks, they can end up watching the entire linked package repo (including its own `node_modules` and build artifacts). That can mean thousands of extra files, high memory use, and slow or crashing processes. Node-link-local installs a plain directory from a tarball, so watchers don’t follow a link into another repo.
|
|
19
|
+
|
|
20
|
+
- **Bundlers and frameworks**
|
|
21
|
+
Webpack 5 and Next.js have known issues with symlinked packages (e.g. [Next.js #35110](https://github.com/vercel/next.js/issues/35110), [yalc #188](https://github.com/wclr/yalc/issues/188)): symlinked ESM packages can be bundled as internal instead of external, or resolution can break. Node-link-local avoids that by not using symlinks at all.
|
|
22
|
+
|
|
23
|
+
- **Nested dependencies**
|
|
24
|
+
Yalc does not resolve “dependencies of dependencies” when using `file:` refs; nested `.yalc` paths can break. Node-link-local uses a single packed tarball, so the app’s package manager does normal dependency resolution for the installed package.
|
|
25
|
+
|
|
26
|
+
Trade-off: with node-link-local you run `node-link-local add` again after changing the library (no live “push” like yalc). In return you get a normal install, no symlink-related test or tooling surprises, and behavior that matches a real publish.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g node-link-local
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Or run without installing:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx node-link-local add /path/to/lib
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Commands
|
|
41
|
+
|
|
42
|
+
| Command | Description |
|
|
43
|
+
|--------|-------------|
|
|
44
|
+
| `node-link-local add <path-to-lib>` | Add the local package at `<path-to-lib>` into the current directory (build, pack, install from staged tarball). |
|
|
45
|
+
| `node-link-local remove` | Remove all packages linked via node-link-local and delete `.local-packages/` if empty. |
|
|
46
|
+
| `node-link-local remove <name-or-path>` | Remove one package (by package name or path). If it was the last one, removes `.local-packages/`. |
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# From your app directory
|
|
52
|
+
node-link-local add ../packages/my-lib
|
|
53
|
+
|
|
54
|
+
# Remove all linked packages and clean bindings
|
|
55
|
+
node-link-local remove
|
|
56
|
+
|
|
57
|
+
# Remove one package by name
|
|
58
|
+
node-link-local remove my-lib
|
|
59
|
+
|
|
60
|
+
# Remove one package by path (same as used in add)
|
|
61
|
+
node-link-local remove ../packages/my-lib
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Paths are relative to the current directory. Both the library and app must contain a `package.json`. The app’s lockfile is used to detect pnpm/yarn/npm.
|
|
65
|
+
|
|
66
|
+
## How it works
|
|
67
|
+
|
|
68
|
+
1. **add <path-to-lib>**
|
|
69
|
+
Detects package manager in lib and app (cwd). If the lib has no `dist/`, runs `build`. Copies the lib into a temp dir, normalizes `package.json` (e.g. `workspace:*` → `*`, strips prepare/prepack/postpack), runs `pack`, copies the `.tgz` into the app’s `.local-packages/`, and installs from that tarball.
|
|
70
|
+
|
|
71
|
+
2. **remove** (no args)
|
|
72
|
+
Finds all dependencies in the app’s `package.json` that reference `file:.local-packages/`, uninstalls each via the app’s package manager, deletes the staged tarballs, and removes `.local-packages/` if empty.
|
|
73
|
+
|
|
74
|
+
3. **remove <name-or-path>**
|
|
75
|
+
Uninstalls that one package and deletes its tarball. If no `file:.local-packages/` dependencies remain, removes the `.local-packages/` directory.
|
|
76
|
+
|
|
77
|
+
## Requirements
|
|
78
|
+
|
|
79
|
+
- Node.js ≥ 18
|
|
80
|
+
- Library and app each have a `package.json`; library has a `name` (and ideally a build that produces `dist/` if you rely on it).
|
|
81
|
+
|
|
82
|
+
## Platform
|
|
83
|
+
|
|
84
|
+
Works on **Windows, macOS, and Linux**. Uses only Node built-ins (`path`, `fs`, `os`, `child_process`) and the system `PATH` to run pnpm/yarn/npm, so no shell-specific or platform-specific code.
|
|
85
|
+
|
|
86
|
+
## License
|
|
87
|
+
|
|
88
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/** node-link-local — sync a local package into current app via staged tarballs. */
|
|
3
|
+
import { add, remove } from '../lib/sync.js';
|
|
4
|
+
|
|
5
|
+
const [cmd, arg1] = process.argv.slice(2);
|
|
6
|
+
const usage = () => {
|
|
7
|
+
console.error('Usage: node-link-local add <path-to-lib>');
|
|
8
|
+
console.error(' node-link-local remove # remove all linked packages');
|
|
9
|
+
console.error(' node-link-local remove <name|path> # remove one package');
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
if (!cmd || (cmd !== 'add' && cmd !== 'remove')) {
|
|
13
|
+
console.error('❌ Command must be "add" or "remove".');
|
|
14
|
+
usage();
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (cmd === 'add') {
|
|
19
|
+
if (!arg1) {
|
|
20
|
+
console.error('❌ add requires <path-to-lib>.');
|
|
21
|
+
usage();
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
if (cmd === 'add') {
|
|
28
|
+
add({ libPath: arg1 });
|
|
29
|
+
} else {
|
|
30
|
+
remove({ packageNameOrPath: arg1 });
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('❌', err instanceof Error ? err.message : String(err));
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
package/lib/detect-pm.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Detect package manager from lockfile. Returns 'pnpm' | 'yarn' | 'npm'. */
|
|
5
|
+
export function detectPackageManager(dir) {
|
|
6
|
+
if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
7
|
+
if (fs.existsSync(path.join(dir, 'yarn.lock'))) return 'yarn';
|
|
8
|
+
return 'npm';
|
|
9
|
+
}
|
package/lib/sync.js
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local package sync via staged tarballs. Cross-platform: uses only Node path/fs/os
|
|
3
|
+
* and spawns package-manager binaries (pnpm/yarn/npm) via PATH.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { spawnSync } from 'child_process';
|
|
9
|
+
import { detectPackageManager } from './detect-pm.js';
|
|
10
|
+
|
|
11
|
+
const CACHE_DIR = '.local-packages';
|
|
12
|
+
|
|
13
|
+
function resolvePath(p) {
|
|
14
|
+
const resolved = path.resolve(p);
|
|
15
|
+
try {
|
|
16
|
+
return fs.realpathSync(resolved);
|
|
17
|
+
} catch {
|
|
18
|
+
return resolved;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readPackageInfo(dir) {
|
|
23
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
24
|
+
if (!fs.existsSync(pkgPath)) throw new Error(`No package.json in: ${dir}`);
|
|
25
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
26
|
+
return { name: pkg.name, version: pkg.version || '0.0.0' };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Run cmd with args in cwd. Uses shell so Windows finds pnpm.cmd / yarn.cmd / npm.cmd. */
|
|
30
|
+
function run(cwd, cmd, args, opts = {}) {
|
|
31
|
+
const env = { ...process.env, COREPACK_ENABLE_STRICT: '0' };
|
|
32
|
+
const result = spawnSync(cmd, args, {
|
|
33
|
+
cwd,
|
|
34
|
+
env,
|
|
35
|
+
stdio: opts.quiet ? 'pipe' : 'inherit',
|
|
36
|
+
shell: true,
|
|
37
|
+
windowsHide: true,
|
|
38
|
+
});
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
throw new Error(`Command failed: ${cmd} ${args.join(' ')} (exit ${result.status})`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Tarball basename for scoped packages: @scope/pkg -> scope-pkg-1.0.0.tgz */
|
|
45
|
+
function tarballBasename(pkgName) {
|
|
46
|
+
return pkgName.startsWith('@') ? pkgName.slice(1).replace('/', '-') : pkgName;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizePackageJson(dir) {
|
|
50
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
51
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
52
|
+
|
|
53
|
+
const fixWorkspace = (deps) => {
|
|
54
|
+
if (!deps) return;
|
|
55
|
+
for (const k of Object.keys(deps)) {
|
|
56
|
+
if (deps[k] === 'workspace:*') deps[k] = '*';
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
fixWorkspace(pkg.dependencies);
|
|
60
|
+
fixWorkspace(pkg.devDependencies);
|
|
61
|
+
fixWorkspace(pkg.peerDependencies);
|
|
62
|
+
|
|
63
|
+
pkg.scripts = pkg.scripts || {};
|
|
64
|
+
delete pkg.scripts.prepare;
|
|
65
|
+
delete pkg.scripts.prepack;
|
|
66
|
+
delete pkg.scripts.postpack;
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(pkg.files) && !pkg.files.includes('dist')) pkg.files.push('dist');
|
|
69
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function findLatestTarball(dir) {
|
|
73
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.tgz'));
|
|
74
|
+
if (files.length === 0) return null;
|
|
75
|
+
const withTime = files.map((f) => ({ name: f, mtime: fs.statSync(path.join(dir, f)).mtimeMs }));
|
|
76
|
+
withTime.sort((a, b) => b.mtime - a.mtime);
|
|
77
|
+
return withTime[0].name;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function deleteStagedTarballs(cacheDir, pkgName) {
|
|
81
|
+
if (!fs.existsSync(cacheDir)) return;
|
|
82
|
+
const prefix = tarballBasename(pkgName) + '-';
|
|
83
|
+
for (const f of fs.readdirSync(cacheDir)) {
|
|
84
|
+
if (f.startsWith(prefix) && f.endsWith('.tgz')) {
|
|
85
|
+
fs.unlinkSync(path.join(cacheDir, f));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Package names in app that are linked via file:.local-packages/ */
|
|
91
|
+
function getLocalPackageNames(appDir) {
|
|
92
|
+
const pkgPath = path.join(appDir, 'package.json');
|
|
93
|
+
if (!fs.existsSync(pkgPath)) return [];
|
|
94
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
|
+
const ref = 'file:.local-packages/';
|
|
96
|
+
const names = [];
|
|
97
|
+
for (const deps of [pkg.dependencies, pkg.devDependencies]) {
|
|
98
|
+
if (!deps) continue;
|
|
99
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
100
|
+
if (typeof value === 'string' && value.startsWith(ref)) names.push(name);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [...new Set(names)];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** True if arg looks like a path (not a bare package name). */
|
|
107
|
+
function isPathArg(arg) {
|
|
108
|
+
return arg.includes(path.sep) || arg.startsWith('.') || /^[A-Za-z]:/.test(arg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Resolve package name: if path, read from lib package.json; else return as name. */
|
|
112
|
+
function resolvePackageName(appDir, nameOrPath) {
|
|
113
|
+
if (!isPathArg(nameOrPath)) return nameOrPath;
|
|
114
|
+
const libDir = resolvePath(path.resolve(appDir, nameOrPath));
|
|
115
|
+
return readPackageInfo(libDir).name;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function removeOnePackage(appDir, pkgName, log) {
|
|
119
|
+
const pm = detectPackageManager(appDir);
|
|
120
|
+
log(`🧹 Removing ${pkgName}...`);
|
|
121
|
+
run(appDir, pm, pm === 'npm' ? ['uninstall', pkgName] : ['remove', pkgName]);
|
|
122
|
+
deleteStagedTarballs(path.join(appDir, CACHE_DIR), pkgName);
|
|
123
|
+
log(`✅ Removed ${pkgName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Remove .local-packages dir if empty (clean bindings). */
|
|
127
|
+
function cleanBindingsDir(appDir) {
|
|
128
|
+
const cacheDir = path.join(appDir, CACHE_DIR);
|
|
129
|
+
if (!fs.existsSync(cacheDir)) return;
|
|
130
|
+
const files = fs.readdirSync(cacheDir);
|
|
131
|
+
if (files.length === 0) {
|
|
132
|
+
fs.rmSync(cacheDir, { recursive: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function remove(opts, log = console.log) {
|
|
137
|
+
const appDir = resolvePath(opts.appPath || process.cwd());
|
|
138
|
+
if (!fs.existsSync(path.join(appDir, 'package.json'))) {
|
|
139
|
+
throw new Error(`No package.json in destination: ${appDir}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const nameOrPath = opts.packageNameOrPath;
|
|
143
|
+
|
|
144
|
+
if (nameOrPath === undefined || nameOrPath === '') {
|
|
145
|
+
const names = getLocalPackageNames(appDir);
|
|
146
|
+
if (names.length === 0) {
|
|
147
|
+
log('No node-link-local packages in this app.');
|
|
148
|
+
cleanBindingsDir(appDir);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
for (const pkgName of names) {
|
|
152
|
+
removeOnePackage(appDir, pkgName, log);
|
|
153
|
+
}
|
|
154
|
+
cleanBindingsDir(appDir);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const pkgName = resolvePackageName(appDir, nameOrPath);
|
|
159
|
+
removeOnePackage(appDir, pkgName, log);
|
|
160
|
+
if (getLocalPackageNames(appDir).length === 0) {
|
|
161
|
+
cleanBindingsDir(appDir);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function add(opts, log = console.log) {
|
|
166
|
+
const sourceDir = resolvePath(opts.libPath);
|
|
167
|
+
const destDir = resolvePath(opts.appPath || process.cwd());
|
|
168
|
+
if (!fs.existsSync(path.join(sourceDir, 'package.json'))) {
|
|
169
|
+
throw new Error(`No package.json in source: ${sourceDir}`);
|
|
170
|
+
}
|
|
171
|
+
if (!fs.existsSync(path.join(destDir, 'package.json'))) {
|
|
172
|
+
throw new Error(`No package.json in destination: ${destDir}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { name: pkgName, version } = readPackageInfo(sourceDir);
|
|
176
|
+
const srcPm = detectPackageManager(sourceDir);
|
|
177
|
+
const destPm = detectPackageManager(destDir);
|
|
178
|
+
log(`📦 ${pkgName}@${version} (source: ${srcPm}, app: ${destPm})`);
|
|
179
|
+
|
|
180
|
+
const distPath = path.join(sourceDir, 'dist');
|
|
181
|
+
if (!fs.existsSync(distPath)) {
|
|
182
|
+
log('🛠️ Building (no dist/)...');
|
|
183
|
+
run(sourceDir, srcPm, ['run', 'build']);
|
|
184
|
+
} else {
|
|
185
|
+
log('✨ dist/ present');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// mkdtempSync: use path.join so prefix is correct on Windows (path separators)
|
|
189
|
+
const tempPrefix = path.join(os.tmpdir(), 'node-link-local-');
|
|
190
|
+
const tempDir = fs.mkdtempSync(tempPrefix);
|
|
191
|
+
try {
|
|
192
|
+
log('📂 Snapshot + pack...');
|
|
193
|
+
fs.cpSync(sourceDir, tempDir, { recursive: true });
|
|
194
|
+
normalizePackageJson(tempDir);
|
|
195
|
+
run(tempDir, srcPm, ['pack'], { quiet: true });
|
|
196
|
+
|
|
197
|
+
const tarball = findLatestTarball(tempDir);
|
|
198
|
+
if (!tarball) throw new Error('Pack produced no tarball');
|
|
199
|
+
|
|
200
|
+
const cacheDir = path.join(destDir, CACHE_DIR);
|
|
201
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
202
|
+
fs.copyFileSync(path.join(tempDir, tarball), path.join(cacheDir, tarball));
|
|
203
|
+
|
|
204
|
+
const fileRef = `file:.local-packages/${tarball}`;
|
|
205
|
+
log(`🚀 Installing from staged tarball...`);
|
|
206
|
+
|
|
207
|
+
if (destPm === 'yarn') {
|
|
208
|
+
const pkgPath = path.join(destDir, 'package.json');
|
|
209
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
210
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
211
|
+
pkg.dependencies[pkgName] = fileRef;
|
|
212
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
|
|
213
|
+
run(destDir, 'yarn', ['install', '--mode=skip-build']);
|
|
214
|
+
} else if (destPm === 'pnpm') {
|
|
215
|
+
run(destDir, 'pnpm', [
|
|
216
|
+
'add',
|
|
217
|
+
`${pkgName}@${fileRef}`,
|
|
218
|
+
'--workspace=false',
|
|
219
|
+
'--no-link-workspace-packages',
|
|
220
|
+
'--resolve-workspace-protocol=false',
|
|
221
|
+
]);
|
|
222
|
+
} else {
|
|
223
|
+
run(destDir, 'npm', ['install', fileRef]);
|
|
224
|
+
}
|
|
225
|
+
log(`✅ Installed ${pkgName}`);
|
|
226
|
+
} finally {
|
|
227
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-link-local",
|
|
3
|
+
"version": "1.0.0-alpha.2",
|
|
4
|
+
"description": "Sync a local npm package into another project via staged tarballs (pnpm/yarn/npm agnostic). No symlinks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"node-link-local": "./bin/node-link-local.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib"
|
|
12
|
+
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/andrewmkhoury/node-link-local.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/andrewmkhoury/node-link-local#readme",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node bin/node-link-local.js",
|
|
20
|
+
"test": "node test/run.js"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"local",
|
|
24
|
+
"link",
|
|
25
|
+
"npm",
|
|
26
|
+
"pnpm",
|
|
27
|
+
"yarn",
|
|
28
|
+
"workspace",
|
|
29
|
+
"tarball",
|
|
30
|
+
"monorepo"
|
|
31
|
+
],
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|