@lenne.tech/cli 1.45.0 → 1.46.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/build/commands/dev/up.js +3 -0
- package/build/lib/dev-env-bridge.js +14 -3
- package/build/lib/dev-env.js +117 -3
- package/build/lib/dev-patches.js +1 -1
- package/build/lib/dev-test-session.js +3 -0
- package/build/lib/ensure-root-dockerignore.js +7 -0
- package/docs/commands.md +21 -0
- package/package.json +7 -1
package/build/commands/dev/up.js
CHANGED
|
@@ -354,6 +354,9 @@ const UpCommand = {
|
|
|
354
354
|
// Build env per process.
|
|
355
355
|
const devEnv = (0, dev_env_1.buildDevEnv)({
|
|
356
356
|
apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
|
|
357
|
+
// Lets `buildDevEnv` consult the app's own `.env` for keys a project may have set
|
|
358
|
+
// itself, so `lt dev` never replaces one of its values with a derived fallback.
|
|
359
|
+
appDir: layout.appDir,
|
|
357
360
|
appInternalPort: appPort !== null && appPort !== void 0 ? appPort : 0,
|
|
358
361
|
baseEnv: process.env,
|
|
359
362
|
dbName,
|
|
@@ -16,8 +16,11 @@ exports.writeEnvBridge = writeEnvBridge;
|
|
|
16
16
|
* Shell B does not inherit shell A's exports. Reading a file solves
|
|
17
17
|
* this without polluting global state.
|
|
18
18
|
*
|
|
19
|
-
* The file is gitignored via `.lt-dev
|
|
20
|
-
*
|
|
19
|
+
* The file is gitignored via `.lt-dev/` and written 0600, because it is no longer
|
|
20
|
+
* URLs only: `NUXT_SESSION_PASSWORD` is a session-sealing key. Usually that is the
|
|
21
|
+
* value `lt dev` derived itself, but when the project exported its own it is THAT
|
|
22
|
+
* one being copied here — so treat the file as credential-bearing, and think twice
|
|
23
|
+
* before adding a key to it. Format: standard dotenv KEY=VALUE.
|
|
21
24
|
*/
|
|
22
25
|
const fs_1 = require("fs");
|
|
23
26
|
const os_1 = require("os");
|
|
@@ -84,6 +87,11 @@ function writeEnvBridge(projectRoot, devEnv, dbName, fileName = '.env') {
|
|
|
84
87
|
'NUXT_PUBLIC_SITE_URL',
|
|
85
88
|
'NUXT_PUBLIC_STORAGE_PREFIX',
|
|
86
89
|
'NUXT_PUBLIC_API_PROXY',
|
|
90
|
+
// Not needed by the runner itself, but external suites check it to tell a stack that
|
|
91
|
+
// can log in from one that will 500 on every login, and a suite that seals its own
|
|
92
|
+
// session cookie to skip the login form needs the value (see dev-env.ts). This is the
|
|
93
|
+
// one credential-shaped key in the bridge — it is why the file is written 0600.
|
|
94
|
+
'NUXT_SESSION_PASSWORD',
|
|
87
95
|
'NSC__MONGOOSE__URI',
|
|
88
96
|
'DATABASE_URL',
|
|
89
97
|
// Legacy aliases — see dev-env.ts for the rationale.
|
|
@@ -107,6 +115,9 @@ function writeEnvBridge(projectRoot, devEnv, dbName, fileName = '.env') {
|
|
|
107
115
|
if ((0, fs_1.existsSync)(file) && (0, fs_1.readFileSync)(file, 'utf8') === content)
|
|
108
116
|
return file;
|
|
109
117
|
(0, fs_1.mkdirSync)((0, path_1.dirname)(file), { recursive: true });
|
|
110
|
-
(0, fs_1.writeFileSync)(file, content, 'utf8');
|
|
118
|
+
(0, fs_1.writeFileSync)(file, content, { encoding: 'utf8', mode: 0o600 });
|
|
119
|
+
// `mode` only applies when the file is CREATED, so an existing bridge from an older
|
|
120
|
+
// lt version would keep its 0644. chmod unconditionally to heal those.
|
|
121
|
+
(0, fs_1.chmodSync)(file, 0o600);
|
|
111
122
|
return file;
|
|
112
123
|
}
|
package/build/lib/dev-env.js
CHANGED
|
@@ -14,6 +14,8 @@ exports.buildDevEnv = buildDevEnv;
|
|
|
14
14
|
* - `NUXT_PUBLIC_*` lock the App to its own API
|
|
15
15
|
* - `NUXT_PUBLIC_STORAGE_PREFIX` namespaces localStorage/sessionStorage
|
|
16
16
|
* - `NSC__MONGOOSE__URI` / `DATABASE_URL` namespace the database per project
|
|
17
|
+
* - `NUXT_SESSION_PASSWORD` is keyed per slug, so a session cookie sealed by one
|
|
18
|
+
* stack cannot be unsealed by another (dev vs. `-test` vs. `-test-N` shard)
|
|
17
19
|
*
|
|
18
20
|
* CA trust for SSR fetches:
|
|
19
21
|
* - Both API and App receive `NODE_EXTRA_CA_CERTS` pointing at the
|
|
@@ -21,6 +23,10 @@ exports.buildDevEnv = buildDevEnv;
|
|
|
21
23
|
* subdomains succeed. Without this Nuxt SSR fails with "unable to
|
|
22
24
|
* get local issuer certificate" when the app calls its own API.
|
|
23
25
|
*/
|
|
26
|
+
const node_crypto_1 = require("node:crypto");
|
|
27
|
+
const node_fs_1 = require("node:fs");
|
|
28
|
+
const node_os_1 = require("node:os");
|
|
29
|
+
const node_path_1 = require("node:path");
|
|
24
30
|
const dev_env_bridge_1 = require("./dev-env-bridge");
|
|
25
31
|
/**
|
|
26
32
|
* Build the environment maps for both API and App processes.
|
|
@@ -29,7 +35,8 @@ const dev_env_bridge_1 = require("./dev-env-bridge");
|
|
|
29
35
|
* vars survive. `lt dev`-managed keys win on top.
|
|
30
36
|
*/
|
|
31
37
|
function buildDevEnv(input) {
|
|
32
|
-
|
|
38
|
+
var _a;
|
|
39
|
+
const { apiInternalPort, appDir, appInternalPort, baseEnv = {}, dbName, identity } = input;
|
|
33
40
|
const apiSub = identity.subdomains.api;
|
|
34
41
|
const appSub = identity.subdomains.app;
|
|
35
42
|
const apiUrl = apiSub ? `https://${apiSub.hostname}` : '';
|
|
@@ -52,14 +59,18 @@ function buildDevEnv(input) {
|
|
|
52
59
|
internalPort: apiInternalPort,
|
|
53
60
|
},
|
|
54
61
|
app: {
|
|
55
|
-
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
62
|
+
env: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, baseEnv), sharedKeys), {
|
|
56
63
|
// See API note above: pin the dev server to IPv4 so Caddy's
|
|
57
64
|
// `127.0.0.1` upstream is unambiguous.
|
|
58
65
|
HOST: '127.0.0.1', NITRO_HOST: '127.0.0.1' }), (apiUrl ? { API_URL: apiUrl, NUXT_API_URL: apiUrl, NUXT_PUBLIC_API_URL: apiUrl } : {})), (appUrl ? { NUXT_PUBLIC_SITE_URL: appUrl, SITE_URL: appUrl } : {})), {
|
|
59
66
|
// Vite-API-Proxy is OFF by default in lt dev mode — Caddy serves
|
|
60
67
|
// both subdomains under HTTPS with shared cookie domain, so
|
|
61
68
|
// same-origin trickery is no longer required.
|
|
62
|
-
NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug
|
|
69
|
+
NUXT_PUBLIC_API_PROXY: 'false', NUXT_PUBLIC_STORAGE_PREFIX: identity.slug }), (baseEnv.NUXT_SESSION_PASSWORD
|
|
70
|
+
? {}
|
|
71
|
+
: {
|
|
72
|
+
NUXT_SESSION_PASSWORD: (_a = readEnvFileValue(appDir, 'NUXT_SESSION_PASSWORD')) !== null && _a !== void 0 ? _a : deriveSessionPassword(identity.slug),
|
|
73
|
+
})), { PORT: String(appInternalPort) }), (process.platform === 'darwin' ? { TMPDIR: '/tmp' } : {})),
|
|
63
74
|
internalPort: appInternalPort,
|
|
64
75
|
},
|
|
65
76
|
};
|
|
@@ -68,3 +79,106 @@ function buildDevEnv(input) {
|
|
|
68
79
|
function buildPostgresUrl(dbName) {
|
|
69
80
|
return `postgresql://${dbName}:${dbName}@localhost:5432/${dbName}`;
|
|
70
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* Stable local session password for a project's app process.
|
|
84
|
+
*
|
|
85
|
+
* 32 hex chars — h3's floor. Deterministic per slug AND per machine: the same stack on the
|
|
86
|
+
* same machine always gets the same value, so a restart does not invalidate open sessions,
|
|
87
|
+
* while a different slug (or a different developer's machine) gets a different one.
|
|
88
|
+
*
|
|
89
|
+
* The machine salt is what makes it unguessable. Without it the only input is the slug, which
|
|
90
|
+
* the app publishes itself via `NUXT_PUBLIC_STORAGE_PREFIX`, so anyone who loaded a page —
|
|
91
|
+
* including any visitor of a `lt dev tunnel` URL — could recompute the key and forge a sealed
|
|
92
|
+
* session cookie. With it, an attacker would have to read the developer's home directory.
|
|
93
|
+
*/
|
|
94
|
+
function deriveSessionPassword(slug) {
|
|
95
|
+
var _a;
|
|
96
|
+
// No salt means the home directory is unwritable. Fall back to a constant key rather than a
|
|
97
|
+
// random one: determinism is the property every caller depends on, and a fresh value per run
|
|
98
|
+
// would log everybody out on every restart. The fallback is guessable — hence last resort.
|
|
99
|
+
return (0, node_crypto_1.createHmac)('sha256', (_a = machineSessionSalt()) !== null && _a !== void 0 ? _a : 'lt-dev')
|
|
100
|
+
.update(`lt-dev:session:${slug}`)
|
|
101
|
+
.digest('hex')
|
|
102
|
+
.slice(0, 32);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Read (or create) the machine-local salt at `~/.lenneTech/dev-session-salt`, mode 0600.
|
|
106
|
+
*
|
|
107
|
+
* Returns `null` when it can be neither read nor created — see {@link deriveSessionPassword}
|
|
108
|
+
* for what happens then.
|
|
109
|
+
*/
|
|
110
|
+
function machineSessionSalt() {
|
|
111
|
+
const file = sessionSaltPath();
|
|
112
|
+
try {
|
|
113
|
+
const existing = (0, node_fs_1.readFileSync)(file, 'utf8').trim();
|
|
114
|
+
if (existing) {
|
|
115
|
+
return existing;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (_a) {
|
|
119
|
+
// Not created yet — fall through and create it.
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(file), { recursive: true });
|
|
123
|
+
// `wx` fails when the file already exists, so two shards racing on a cold machine can
|
|
124
|
+
// never end up with two different salts: the loser falls into the catch and re-reads.
|
|
125
|
+
const salt = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
126
|
+
(0, node_fs_1.writeFileSync)(file, `${salt}\n`, { flag: 'wx', mode: 0o600 });
|
|
127
|
+
return salt;
|
|
128
|
+
}
|
|
129
|
+
catch (_b) {
|
|
130
|
+
try {
|
|
131
|
+
return (0, node_fs_1.readFileSync)(file, 'utf8').trim() || null;
|
|
132
|
+
}
|
|
133
|
+
catch (_c) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Read one key out of `<dir>/.env` without pulling in a dotenv dependency.
|
|
140
|
+
*
|
|
141
|
+
* Deliberately minimal: no interpolation, no multi-line values, no `.env.local` cascade. It
|
|
142
|
+
* exists to answer one question — did the project set this key itself? — for a value the built
|
|
143
|
+
* Nitro server would otherwise never see.
|
|
144
|
+
*/
|
|
145
|
+
function readEnvFileValue(dir, key) {
|
|
146
|
+
if (!dir) {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
let content;
|
|
150
|
+
try {
|
|
151
|
+
content = (0, node_fs_1.readFileSync)((0, node_path_1.join)(dir, '.env'), 'utf8');
|
|
152
|
+
}
|
|
153
|
+
catch (_a) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
for (const line of content.split(/\r?\n/)) {
|
|
157
|
+
const trimmed = line.trim();
|
|
158
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const eq = trimmed.indexOf('=');
|
|
162
|
+
if (eq === -1) {
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (trimmed
|
|
166
|
+
.slice(0, eq)
|
|
167
|
+
.replace(/^export\s+/, '')
|
|
168
|
+
.trim() !== key) {
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const raw = trimmed.slice(eq + 1).trim();
|
|
172
|
+
const quoted = raw.length >= 2 && ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'")));
|
|
173
|
+
return (quoted ? raw.slice(1, -1) : raw) || undefined;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Path of the machine-local salt. `LT_DEV_SESSION_SALT_PATH` overrides it, and `HOME` is read
|
|
179
|
+
* before `os.homedir()` — both so tests can redirect the write to a tmpdir, the same reason
|
|
180
|
+
* `dev-service.ts#userHome` does it.
|
|
181
|
+
*/
|
|
182
|
+
function sessionSaltPath() {
|
|
183
|
+
return process.env.LT_DEV_SESSION_SALT_PATH || (0, node_path_1.join)(process.env.HOME || (0, node_os_1.homedir)(), '.lenneTech', 'dev-session-salt');
|
|
184
|
+
}
|
package/build/lib/dev-patches.js
CHANGED
|
@@ -157,7 +157,7 @@ function patchClaudeMd(file, options) {
|
|
|
157
157
|
if (dbName)
|
|
158
158
|
lines.push(`- DB: \`mongodb://127.0.0.1/${dbName}\``);
|
|
159
159
|
lines.push('');
|
|
160
|
-
lines.push('Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.');
|
|
160
|
+
lines.push('Env vars set automatically by `lt dev up`: `BASE_URL`, `APP_URL`, `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL`, `NUXT_PUBLIC_STORAGE_PREFIX`, `NUXT_SESSION_PASSWORD`, `NSC__MONGOOSE__URI`, `DATABASE_URL`. **Never assume `localhost:3000` / `localhost:3001` for this project** — those are the framework defaults, not the active URLs.');
|
|
161
161
|
lines.push('');
|
|
162
162
|
lines.push(endMarker);
|
|
163
163
|
const block = lines.join('\n');
|
|
@@ -274,6 +274,9 @@ function bringUpTestSession(layout_1, baseIdentity_1, log_1) {
|
|
|
274
274
|
log.info('');
|
|
275
275
|
const devEnv = (0, dev_env_1.buildDevEnv)({
|
|
276
276
|
apiInternalPort: apiPort !== null && apiPort !== void 0 ? apiPort : 0,
|
|
277
|
+
// The built server this stack runs reads no `.env` at all, so a key the project set
|
|
278
|
+
// there has to be forwarded explicitly — that is the whole reason this argument exists.
|
|
279
|
+
appDir: layout.appDir,
|
|
277
280
|
appInternalPort: appPort !== null && appPort !== void 0 ? appPort : 0,
|
|
278
281
|
baseEnv: process.env,
|
|
279
282
|
dbName,
|
|
@@ -16,8 +16,15 @@ exports.ensureRootDockerignore = ensureRootDockerignore;
|
|
|
16
16
|
// bundle itself changes.
|
|
17
17
|
const REQUIRED_PATTERNS = [
|
|
18
18
|
'**/node_modules',
|
|
19
|
+
// `**/.output` matches a path component EXACTLY, so the isolated build dirs of
|
|
20
|
+
// `lt dev test` (`.output-test`, `.nuxt-test`, `.nuxt-test-2`, …) need their own
|
|
21
|
+
// globs. They are built with the app's full dev env, so Nitro freezes values like
|
|
22
|
+
// `NUXT_SESSION_PASSWORD` into them as runtime-config defaults — and a `--keep`
|
|
23
|
+
// run or a crashed teardown leaves the tree on disk for the next `docker build`.
|
|
19
24
|
'**/.output',
|
|
25
|
+
'**/.output-*',
|
|
20
26
|
'**/.nuxt',
|
|
27
|
+
'**/.nuxt-*',
|
|
21
28
|
'**/dist',
|
|
22
29
|
// `**/.env` matches a path component EXACTLY, so it does NOT cover
|
|
23
30
|
// `.env.production` / `.env.staging` / `.env.test` — all of which routinely
|
package/docs/commands.md
CHANGED
|
@@ -451,9 +451,24 @@ lt dev up
|
|
|
451
451
|
| `NUXT_PUBLIC_SITE_URL` | Nuxt `useRuntimeConfig().public.siteUrl` + Playwright | `https://crm.localhost` |
|
|
452
452
|
| `NUXT_PUBLIC_STORAGE_PREFIX` | namespaces sessionStorage/localStorage | `crm` |
|
|
453
453
|
| `NUXT_PUBLIC_API_PROXY` | always `false` — Caddy + cookie-domain make it obsolete | `false` |
|
|
454
|
+
| `NUXT_SESSION_PASSWORD` | Nuxt/h3 `useSession` — only projects that added h3 sessions or `nuxt-auth-utils` read it; the Better-Auth default stack ignores it | `a1b2…` (32 hex chars) |
|
|
454
455
|
| `NSC__MONGOOSE__URI` | nest-server Mongoose URI | `mongodb://127.0.0.1/crm-local` |
|
|
455
456
|
| `DATABASE_URL` | Postgres convenience URL (for nest-base-style projects) | `postgresql://crm-local:crm-local@localhost:5432/crm-local` |
|
|
456
457
|
|
|
458
|
+
`NUXT_SESSION_PASSWORD` exists because `lt dev test` serves the **built** Nitro server, which
|
|
459
|
+
reads no `.env` — so a project that keeps its password there watched every login answer 500
|
|
460
|
+
(`H3Error: Empty password`). Resolution order: a value exported in the shell, then the app's own
|
|
461
|
+
`.env`, then a fallback derived as `HMAC-SHA256(machine salt, 'lt-dev:session:<slug>')` truncated
|
|
462
|
+
to 32 hex chars. `lt dev` only fills the gap — it never replaces a value the project chose.
|
|
463
|
+
|
|
464
|
+
The salt lives at `~/.lenneTech/dev-session-salt` (created once, mode 0600; override the path with
|
|
465
|
+
`LT_DEV_SESSION_SALT_PATH`). It is what makes the value unguessable: the slug itself is public —
|
|
466
|
+
the app ships it to every browser as `NUXT_PUBLIC_STORAGE_PREFIX`, and `lt dev tunnel` can put
|
|
467
|
+
that app on a public URL — so an unsalted derivation would hand any visitor the key that seals
|
|
468
|
+
the stack's session cookies. Keying on the slug also means the dev stack, the `lt dev test` stack
|
|
469
|
+
and each `--shard` stack get **different** passwords, so one stack's cookies never validate
|
|
470
|
+
against another's.
|
|
471
|
+
|
|
457
472
|
**Override the binary** for both spawns via `LT_PNPM_BIN` (e.g. `LT_PNPM_BIN=/usr/local/bin/pnpm lt dev up`).
|
|
458
473
|
|
|
459
474
|
**Pre-flight guards (exit code 1 each):**
|
|
@@ -685,10 +700,16 @@ lt dev test -- --ui spec.ts # everything after `--` is forwarded to playwri
|
|
|
685
700
|
| `NUXT_API_URL`, `NUXT_PUBLIC_API_URL`, `NUXT_PUBLIC_SITE_URL` | Same URLs for Nuxt |
|
|
686
701
|
| `NUXT_PUBLIC_STORAGE_PREFIX` | Project slug |
|
|
687
702
|
| `NUXT_PUBLIC_API_PROXY` | Always `false` under `lt dev` |
|
|
703
|
+
| `NUXT_SESSION_PASSWORD` | The app process's h3 session password — lets a suite tell a stack that can log in from one that 500s on every login, and seal its own cookie to skip the login form |
|
|
688
704
|
| `NSC__MONGOOSE__URI`, `DATABASE_URL` | Project-namespaced DB URI (when `dbName` known) |
|
|
689
705
|
| `LT_DEV_ACTIVE`, `LT_DEV_DB_NAME` | Marker keys for consumers |
|
|
690
706
|
| `NODE_EXTRA_CA_CERTS` | Path to Caddy's root CA cert (auto-detected) |
|
|
691
707
|
|
|
708
|
+
Because `NUXT_SESSION_PASSWORD` is a session-sealing key, the bridge file is written with mode
|
|
709
|
+
`0600` (and chmod'ed on every rewrite, since a file created by an older `lt` would otherwise keep
|
|
710
|
+
its `0644`). It is gitignored via `.lt-dev/`. Treat it as credential-bearing when deciding what
|
|
711
|
+
else may be written there.
|
|
712
|
+
|
|
692
713
|
Additionally, `lt dev test` exports two build-directory keys into the app process it
|
|
693
714
|
spawns. They are **not** written to the bridge file — they scope one run, not the
|
|
694
715
|
project:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.46.0",
|
|
4
4
|
"description": "lenne.Tech CLI: lt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"lenne.Tech",
|
|
@@ -77,7 +77,13 @@
|
|
|
77
77
|
"turndown-plugin-gfm": "1.0.2",
|
|
78
78
|
"typescript": "6.0.3"
|
|
79
79
|
},
|
|
80
|
+
"//devDependencies": {
|
|
81
|
+
"@emnapi/core": "Not imported by this CLI. @napi-rs/wasm-runtime 1.x moved @emnapi/core and @emnapi/runtime from dependencies to peerDependencies (napi-rs#3174), and the only path to them here is dev-only AND optional: eslint-plugin-import-x / jest-resolve > unrs-resolver > @unrs/resolver-binding-wasm32-wasi (cpu: wasm32) > @napi-rs/wasm-runtime. npm >= 11.5.0 (arborist 9.1.3) prunes nodes that are both peer and optional from the ideal tree, so it never writes them to the lockfile — while npm <= 11.4 still demands them and fails npm ci with \"Missing: @emnapi/core@1.11.3 from lock file\". Declaring them here is the remedy arborist's own source comment names: an optional peer stops being optional once a root dependency requires it, so both npm generations then agree. Verified: npm ci passes on npm 10 and 11.6, and npm install no longer strips the entries. Remove once @napi-rs/wasm-runtime declares them in peerDependenciesMeta as optional, or once npm >= 11.5 is the floor everywhere (GitHub Actions still runs Node 20 = npm 10).",
|
|
82
|
+
"@emnapi/runtime": "Same as @emnapi/core."
|
|
83
|
+
},
|
|
80
84
|
"devDependencies": {
|
|
85
|
+
"@emnapi/core": "^1.11.3",
|
|
86
|
+
"@emnapi/runtime": "^1.11.3",
|
|
81
87
|
"@lenne.tech/eslint-config-ts": "2.3.0",
|
|
82
88
|
"@lenne.tech/npm-package-helper": "0.0.12",
|
|
83
89
|
"@types/ejs": "3.1.5",
|