@askrjs/cli 0.0.7 → 0.0.8

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/dist/add.js CHANGED
@@ -226,9 +226,18 @@ function renderServerActionRegistry(actions) {
226
226
  ...actions.map((action) => `import { ${action.handlerName} } from './actions/${action.slug}.js';`),
227
227
  "import type { AppDependencies } from './dependencies.js';",
228
228
  "",
229
+ "function csrfSecret(): string {",
230
+ " const secret = process.env.CSRF_SECRET;",
231
+ " if (process.env.NODE_ENV !== 'production') return secret ?? 'development-only-secret';",
232
+ " if (!secret || secret.length < 32 || new Set(secret).size < 12) {",
233
+ " throw new Error('Production requires a strong CSRF_SECRET (at least 32 varied characters).');",
234
+ " }",
235
+ " return secret;",
236
+ "}",
237
+ "",
229
238
  "export function createActions(deps: AppDependencies) {",
230
239
  " return defineServerActions({ dependencies: deps,",
231
- " csrf: { secret: process.env.CSRF_SECRET ?? 'development-only-secret' },",
240
+ " csrf: { secret: csrfSecret() },",
232
241
  " },",
233
242
  ...actions.map((action) => ` handleAction(${action.descriptorName}, ${action.handlerName}),`),
234
243
  " );",
@@ -1,5 +1,4 @@
1
- import { t as analyzeRange } from "./range-YUs9eimn.js";
2
- import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
1
+ import { n as analyzeRange, t as parseDependencySpecification } from "./specification-BSlq_n9A.js";
3
2
  import fs from "node:fs/promises";
4
3
  import path from "node:path";
5
4
  import { load } from "js-yaml";
package/dist/generate.js CHANGED
@@ -1,6 +1,9 @@
1
- import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
2
- import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
1
+ import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
+ import { lookup } from "node:dns/promises";
5
+ import { request } from "node:https";
6
+ import { BlockList, isIP } from "node:net";
4
7
  import { load } from "js-yaml";
5
8
  //#region src/generate/generator.ts
6
9
  const OWNED = [
@@ -119,30 +122,202 @@ function parseOpenApiDocument(contents, source) {
119
122
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new GenerationError(`OpenAPI document must be an object: ${source}`);
120
123
  return parsed;
121
124
  }
122
- async function readSource(uri) {
123
- if (uri.startsWith("http://") || uri.startsWith("https://")) {
124
- const response = await fetch(uri);
125
- if (!response.ok) throw new GenerationError(`Unable to fetch OpenAPI reference ${uri}: ${response.status} ${response.statusText}`);
125
+ const blockedAddresses = new BlockList();
126
+ for (const [network, prefix] of [
127
+ ["0.0.0.0", 8],
128
+ ["10.0.0.0", 8],
129
+ ["100.64.0.0", 10],
130
+ ["127.0.0.0", 8],
131
+ ["169.254.0.0", 16],
132
+ ["172.16.0.0", 12],
133
+ ["192.0.0.0", 24],
134
+ ["192.0.2.0", 24],
135
+ ["192.168.0.0", 16],
136
+ ["198.18.0.0", 15],
137
+ ["198.51.100.0", 24],
138
+ ["203.0.113.0", 24],
139
+ ["224.0.0.0", 4],
140
+ ["240.0.0.0", 4]
141
+ ]) blockedAddresses.addSubnet(network, prefix, "ipv4");
142
+ for (const [network, prefix] of [
143
+ ["::", 128],
144
+ ["::1", 128],
145
+ ["fc00::", 7],
146
+ ["fe80::", 10],
147
+ ["ff00::", 8],
148
+ ["2001:db8::", 32],
149
+ ["2001:2::", 48]
150
+ ]) blockedAddresses.addSubnet(network, prefix, "ipv6");
151
+ function privateAddress(address) {
152
+ const family = isIP(address);
153
+ if (family === 4) return blockedAddresses.check(address, "ipv4");
154
+ if (family !== 6) return true;
155
+ const mappedSuffix = /^::ffff:(.+)$/i.exec(address)?.[1];
156
+ const mapped = mappedSuffix?.includes(".") ? mappedSuffix : mappedSuffix && /^[0-9a-f]{1,4}:[0-9a-f]{1,4}$/i.test(mappedSuffix) ? mappedSuffix.split(":").flatMap((part) => {
157
+ const value = Number.parseInt(part, 16);
158
+ return [value >> 8, value & 255];
159
+ }).join(".") : void 0;
160
+ return mapped ? privateAddress(mapped) : blockedAddresses.check(address, "ipv6");
161
+ }
162
+ function remaining(deadline, uri) {
163
+ const value = deadline - Date.now();
164
+ if (value <= 0) throw new GenerationError(`Timed out fetching OpenAPI reference ${uri}`);
165
+ return value;
166
+ }
167
+ async function withDeadline(promise, deadline, uri) {
168
+ let timer;
169
+ try {
170
+ return await Promise.race([promise, new Promise((_, reject) => {
171
+ timer = setTimeout(() => reject(new GenerationError(`Timed out fetching OpenAPI reference ${uri}`)), remaining(deadline, uri));
172
+ })]);
173
+ } finally {
174
+ if (timer) clearTimeout(timer);
175
+ }
176
+ }
177
+ async function vettedAddresses(url, options, deadline) {
178
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
179
+ const answers = isIP(hostname) ? [{
180
+ address: hostname,
181
+ family: isIP(hostname)
182
+ }] : await withDeadline(lookup(hostname, {
183
+ all: true,
184
+ verbatim: true
185
+ }), deadline, url.href);
186
+ if (answers.length === 0 || !options.allowPrivateHosts && answers.some(({ address }) => privateAddress(address))) throw new GenerationError(`OpenAPI reference resolves to a private, reserved, or link-local address: ${url.origin}`);
187
+ return answers.map(({ address, family }) => ({
188
+ address,
189
+ family
190
+ }));
191
+ }
192
+ function retryableConnectionError(error) {
193
+ if (!(error instanceof Error)) return false;
194
+ const code = error.code ?? "";
195
+ return !/^ERR_TLS_|^CERT_|SELF_SIGNED|CERTIFICATE/.test(code) && [
196
+ "ECONNREFUSED",
197
+ "ECONNRESET",
198
+ "EHOSTUNREACH",
199
+ "ENETUNREACH",
200
+ "ETIMEDOUT",
201
+ "EPIPE"
202
+ ].includes(code);
203
+ }
204
+ function requestAddress(url, vetted, deadline) {
205
+ return new Promise((resolve, reject) => {
206
+ let receivedHeaders = false;
207
+ const request$1 = request(url, {
208
+ method: "GET",
209
+ headers: {
210
+ accept: "application/json, application/yaml, text/yaml, */*",
211
+ "accept-encoding": "identity"
212
+ },
213
+ servername: url.hostname,
214
+ lookup: (_hostname, _options, callback) => callback(null, vetted.address, vetted.family)
215
+ }, (message) => {
216
+ receivedHeaders = true;
217
+ resolve({
218
+ status: message.statusCode ?? 0,
219
+ statusText: message.statusMessage ?? "",
220
+ headers: message.headers,
221
+ message
222
+ });
223
+ });
224
+ request$1.setTimeout(remaining(deadline, url.href), () => {
225
+ request$1.destroy(Object.assign(/* @__PURE__ */ new Error("connection timed out"), { code: "ETIMEDOUT" }));
226
+ });
227
+ request$1.on("error", (error) => reject(Object.assign(error, { receivedHeaders })));
228
+ request$1.end();
229
+ });
230
+ }
231
+ async function requestVetted(url, addresses, deadline) {
232
+ let lastError;
233
+ for (const address of addresses) try {
234
+ return await withDeadline(requestAddress(url, address, deadline), deadline, url.href);
235
+ } catch (error) {
236
+ lastError = error;
237
+ if (!retryableConnectionError(error)) throw error;
238
+ }
239
+ throw lastError;
240
+ }
241
+ async function responseText(response, uri, maxBytes, deadline) {
242
+ const declared = Number(response.headers["content-length"]);
243
+ if (Number.isFinite(declared) && declared > maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`);
244
+ const encoding = response.headers["content-encoding"];
245
+ if (encoding && encoding !== "identity") throw new GenerationError(`OpenAPI reference returned unsupported content encoding: ${encoding}`);
246
+ const chunks = [];
247
+ let size = 0;
248
+ try {
249
+ await withDeadline(new Promise((resolve, reject) => {
250
+ response.message.on("data", (value) => {
251
+ size += value.byteLength;
252
+ if (size > maxBytes) {
253
+ response.message.destroy();
254
+ reject(new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`));
255
+ return;
256
+ }
257
+ chunks.push(value);
258
+ });
259
+ response.message.on("end", resolve);
260
+ response.message.on("error", reject);
261
+ response.message.on("aborted", () => reject(new GenerationError(`OpenAPI response body was aborted: ${uri}`)));
262
+ }), deadline, uri);
263
+ } catch (error) {
264
+ response.message.destroy();
265
+ throw error;
266
+ }
267
+ if (size > maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${maxBytes} bytes: ${uri}`);
268
+ return Buffer.concat(chunks).toString("utf8");
269
+ }
270
+ async function fetchSource(uri, options) {
271
+ let current = new URL(uri);
272
+ const deadline = Date.now() + options.timeoutMs;
273
+ for (let redirects = 0;; redirects += 1) {
274
+ if (current.protocol !== "https:") throw new GenerationError(`Remote OpenAPI references must use HTTPS: ${current.href}`);
275
+ if (current.username || current.password) throw new GenerationError(`Remote OpenAPI references must not include credentials: ${current.origin}`);
276
+ if (current.origin !== options.remoteRootOrigin && !options.allowedReferenceOrigins.has(current.origin)) throw new GenerationError(`Cross-origin OpenAPI reference is not allowed: ${current.origin}`);
277
+ const addresses = await vettedAddresses(current, options, deadline);
278
+ const response = await requestVetted(current, addresses, deadline);
279
+ if (response.status >= 300 && response.status < 400) {
280
+ if (redirects >= options.maxRedirects) throw new GenerationError(`Too many redirects while fetching OpenAPI reference ${uri}`);
281
+ response.message.resume();
282
+ const location = response.headers.location;
283
+ if (!location) throw new GenerationError(`OpenAPI redirect is missing Location: ${current.href}`);
284
+ current = new URL(location, current);
285
+ continue;
286
+ }
287
+ if (response.status < 200 || response.status >= 300) {
288
+ response.message.resume();
289
+ throw new GenerationError(`Unable to fetch OpenAPI reference ${current.href}: ${response.status} ${response.statusText}`);
290
+ }
126
291
  return {
127
- uri,
128
- document: parseOpenApiDocument(await response.text(), uri)
292
+ uri: current.href,
293
+ document: parseOpenApiDocument(await responseText(response, current.href, options.maxBytes, deadline), current.href)
129
294
  };
130
295
  }
296
+ }
297
+ async function readSource(uri, options) {
298
+ if (uri.startsWith("http://") || uri.startsWith("https://")) return fetchSource(uri, options);
131
299
  const path = fileURLToPath(uri);
300
+ const canonical = await realpath(path);
301
+ const relativePath = options.localRootDirectory ? relative(options.localRootDirectory, canonical) : "..";
302
+ if (!options.localRootDirectory || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) throw new GenerationError(`Local OpenAPI reference escapes the specification directory: ${path}`);
303
+ const contents = await readFile(canonical);
304
+ if (contents.byteLength > options.maxBytes) throw new GenerationError(`OpenAPI reference exceeds ${options.maxBytes} bytes: ${path}`);
132
305
  return {
133
- uri,
134
- document: parseOpenApiDocument(await readFile(path, "utf8"), path)
306
+ uri: pathToFileURL(canonical).href,
307
+ document: parseOpenApiDocument(contents.toString("utf8"), canonical)
135
308
  };
136
309
  }
137
310
  var OpenApiBundler = class {
138
311
  root;
139
312
  rootUri;
313
+ options;
140
314
  sources = /* @__PURE__ */ new Map();
141
315
  aliases = /* @__PURE__ */ new Map();
142
316
  names = /* @__PURE__ */ new Map();
143
- constructor(root, rootUri) {
317
+ constructor(root, rootUri, options) {
144
318
  this.root = root;
145
319
  this.rootUri = rootUri;
320
+ this.options = options;
146
321
  }
147
322
  async bundle() {
148
323
  if (!this.root.components) this.root.components = {};
@@ -153,7 +328,7 @@ var OpenApiBundler = class {
153
328
  async source(uri) {
154
329
  let pending = this.sources.get(uri);
155
330
  if (!pending) {
156
- pending = readSource(uri);
331
+ pending = readSource(uri, this.options);
157
332
  this.sources.set(uri, pending);
158
333
  }
159
334
  return pending;
@@ -175,6 +350,7 @@ var OpenApiBundler = class {
175
350
  for (const [key, child] of Object.entries(value)) value[key] = await this.expand(child, uri, document, stack);
176
351
  }
177
352
  async expand(value, uri, document, stack) {
353
+ if (stack.size > this.options.maxDepth) throw new GenerationError(`OpenAPI reference depth exceeds ${this.options.maxDepth}`);
178
354
  if (Array.isArray(value)) return Promise.all(value.map((item) => this.expand(item, uri, document, stack)));
179
355
  if (!value || typeof value !== "object") return value;
180
356
  const object = value;
@@ -215,9 +391,36 @@ var OpenApiBundler = class {
215
391
  return this.expand(target, targetSource.uri, targetSource.document, new Set(stack).add(canonical));
216
392
  }
217
393
  };
218
- async function loadOpenApi(input) {
219
- const source = await readSource(/^https?:\/\//.test(input) ? input : pathToFileURL(resolve(input)).href);
220
- return new OpenApiBundler(source.document, source.uri).bundle();
394
+ async function loadOpenApi(input, options = {}) {
395
+ const remote = /^https?:\/\//.test(input);
396
+ const localPath = remote ? void 0 : await realpath(resolve(input));
397
+ const uri = remote ? new URL(input).href : pathToFileURL(localPath).href;
398
+ const rootUrl = new URL(uri);
399
+ if (remote && rootUrl.protocol !== "https:") throw new GenerationError("Remote OpenAPI roots must use HTTPS.");
400
+ const positiveOption = (name, value) => {
401
+ if (!Number.isSafeInteger(value) || value <= 0) throw new GenerationError(`${name} must be a positive safe integer.`);
402
+ return value;
403
+ };
404
+ const resolved = {
405
+ allowedReferenceOrigins: new Set((options.allowedReferenceOrigins ?? []).map((origin) => {
406
+ let parsed;
407
+ try {
408
+ parsed = new URL(origin);
409
+ } catch {
410
+ throw new GenerationError(`Allowed reference origin must be an HTTPS origin: ${origin}`);
411
+ }
412
+ if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.pathname !== "/" || parsed.search || parsed.hash || parsed.href !== parsed.origin + "/") throw new GenerationError(`Allowed reference origin must be an HTTPS origin: ${origin}`);
413
+ return parsed.origin;
414
+ })),
415
+ allowPrivateHosts: options.allowPrivateHosts ?? false,
416
+ maxBytes: positiveOption("maxBytes", options.maxBytes ?? 5 * 1024 * 1024),
417
+ maxDepth: positiveOption("maxDepth", options.maxDepth ?? 32),
418
+ maxRedirects: positiveOption("maxRedirects", options.maxRedirects ?? 5),
419
+ timeoutMs: positiveOption("timeoutMs", options.timeoutMs ?? 1e4),
420
+ ...remote ? { remoteRootOrigin: rootUrl.origin } : { localRootDirectory: await realpath(dirname(localPath)) }
421
+ };
422
+ const source = await readSource(uri, resolved);
423
+ return new OpenApiBundler(source.document, source.uri, resolved).bundle();
221
424
  }
222
425
  function generateFiles(document) {
223
426
  const version = String(document.openapi ?? "");
@@ -392,13 +595,13 @@ async function writeGenerated(directory, files, check) {
392
595
  });
393
596
  }
394
597
  }
395
- async function generate(input, output, check = false) {
598
+ async function generate(input, output, check = false, loadOptions = {}) {
396
599
  if (!/^https?:\/\//.test(input)) {
397
600
  const resolvedInput = resolve(input);
398
601
  const inputWithinOutput = relative(resolve(output), resolvedInput);
399
602
  if (!inputWithinOutput.startsWith("..") && !isAbsolute(inputWithinOutput)) throw new GenerationError("Generated output must not contain its OpenAPI input document");
400
603
  }
401
- const files = generateFiles(await loadOpenApi(input));
604
+ const files = generateFiles(await loadOpenApi(input, loadOptions));
402
605
  await mkdir(dirname(resolve(output)), { recursive: true });
403
606
  await writeGenerated(output, files, check);
404
607
  }
@@ -410,13 +613,65 @@ async function runGenerateCli(args, io = console) {
410
613
  let check = false;
411
614
  let json = false;
412
615
  let help = false;
616
+ const allowedReferenceOrigins = [];
617
+ const referenceLimits = {};
413
618
  const inputs = [];
414
619
  for (let i = 0; i < args.length; i++) {
415
620
  const arg = args[i];
416
621
  if (arg === "--check") check = true;
417
622
  else if (arg === "--json") json = true;
418
623
  else if (arg === "--help" || arg === "-h") help = true;
419
- else if (arg === "-o" || arg === "--output") {
624
+ else if (arg === "--allow-ref-origin") {
625
+ const value = args[i + 1];
626
+ if (!value || value.startsWith("-")) {
627
+ const message = `${arg} requires an HTTPS origin`;
628
+ io.error(wantsJson ? JSON.stringify({
629
+ status: "error",
630
+ error: message
631
+ }) : message);
632
+ return 1;
633
+ }
634
+ allowedReferenceOrigins.push(value);
635
+ i += 1;
636
+ } else if (arg.startsWith("--allow-ref-origin=")) allowedReferenceOrigins.push(arg.slice(19));
637
+ else if (arg === "--ref-timeout-ms" || arg === "--ref-max-bytes" || arg === "--ref-max-depth" || arg === "--ref-max-redirects") {
638
+ const value = args[i + 1];
639
+ const parsed = Number(value);
640
+ if (!value || value.startsWith("-") || !Number.isSafeInteger(parsed) || parsed <= 0) {
641
+ const message = `${arg} requires a positive safe integer`;
642
+ io.error(wantsJson ? JSON.stringify({
643
+ status: "error",
644
+ error: message
645
+ }) : message);
646
+ return 1;
647
+ }
648
+ const key = {
649
+ "--ref-timeout-ms": "timeoutMs",
650
+ "--ref-max-bytes": "maxBytes",
651
+ "--ref-max-depth": "maxDepth",
652
+ "--ref-max-redirects": "maxRedirects"
653
+ }[arg];
654
+ referenceLimits[key] = parsed;
655
+ i += 1;
656
+ } else if (/^--ref-(?:timeout-ms|max-bytes|max-depth|max-redirects)=/.test(arg)) {
657
+ const [flag, value = ""] = arg.split("=", 2);
658
+ const parsed = Number(value);
659
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) {
660
+ const message = `${flag} requires a positive safe integer`;
661
+ io.error(wantsJson ? JSON.stringify({
662
+ status: "error",
663
+ error: message
664
+ }) : message);
665
+ return 1;
666
+ }
667
+ const key = {
668
+ "--ref-timeout-ms": "timeoutMs",
669
+ "--ref-max-bytes": "maxBytes",
670
+ "--ref-max-depth": "maxDepth",
671
+ "--ref-max-redirects": "maxRedirects"
672
+ }[flag];
673
+ referenceLimits[key] = parsed;
674
+ } else if (arg === "-o" || arg === "--output") {
420
675
  const value = args[i + 1];
421
676
  if (!value || value.startsWith("-")) {
422
677
  const message = `${arg} requires a path`;
@@ -438,7 +693,7 @@ async function runGenerateCli(args, io = console) {
438
693
  return 1;
439
694
  } else inputs.push(arg);
440
695
  }
441
- const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json]";
696
+ const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json] [--allow-ref-origin <https-origin>] [--ref-timeout-ms <n>] [--ref-max-bytes <n>] [--ref-max-depth <n>] [--ref-max-redirects <n>]";
442
697
  if (help) {
443
698
  io.log(usage);
444
699
  return 0;
@@ -451,7 +706,10 @@ async function runGenerateCli(args, io = console) {
451
706
  return 1;
452
707
  }
453
708
  try {
454
- await generate(inputs[0], output, check);
709
+ await generate(inputs[0], output, check, {
710
+ allowedReferenceOrigins,
711
+ ...referenceLimits
712
+ });
455
713
  io.log(json ? JSON.stringify({
456
714
  status: "ok",
457
715
  action: check ? "checked" : "generated",
@@ -1,5 +1,4 @@
1
- import { n as isBreakingChange, r as rewriteRange, t as analyzeRange } from "./range-YUs9eimn.js";
2
- import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
1
+ import { i as rewriteRange, n as analyzeRange, r as isBreakingChange, t as parseDependencySpecification } from "./specification-BSlq_n9A.js";
3
2
  import semver from "semver";
4
3
  //#region src/update/planner.ts
5
4
  const STATUS_PRIORITY = {
@@ -123,12 +122,13 @@ function planOne(occurrence, packument, failure, tag, mode, chosen, blocker) {
123
122
  reason: blocker
124
123
  }
125
124
  };
126
- if (semver.satisfies(selected, occurrence.currentSpecification) || !semver.gt(selected, allowed)) return {
125
+ const declared = semver.minVersion(occurrence.currentSpecification)?.version ?? allowed;
126
+ if (!semver.gt(selected, declared)) return {
127
127
  targetVersion: target,
128
128
  occurrence: {
129
129
  ...withVersions,
130
130
  status: "current",
131
- reason: blocker ?? "selected version is already covered by the current specification"
131
+ reason: blocker ?? "the declared version is current"
132
132
  }
133
133
  };
134
134
  const analysis = analyzeRange(occurrence.currentSpecification);
@@ -149,11 +149,20 @@ function planOne(occurrence, packument, failure, tag, mode, chosen, blocker) {
149
149
  reason: "breaking update is available via askr upgrade"
150
150
  }
151
151
  };
152
+ const proposedSpecification = rewriteRange(analysis.shape, selected, breaking);
153
+ if (proposedSpecification === occurrence.currentSpecification) return {
154
+ targetVersion: target,
155
+ occurrence: {
156
+ ...withVersions,
157
+ status: "current",
158
+ reason: "the selected version cannot further rebase this range style"
159
+ }
160
+ };
152
161
  return {
153
162
  targetVersion: target,
154
163
  occurrence: {
155
164
  ...withVersions,
156
- proposedSpecification: rewriteRange(analysis.shape, selected, breaking),
165
+ proposedSpecification,
157
166
  status: breaking ? "breaking" : "safe",
158
167
  reason: selected === target ? "selected tag target is eligible" : `compatible version ${selected} selected below ${tag}@${target}`
159
168
  }
@@ -185,10 +194,8 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
185
194
  }
186
195
  const variables = [...states.entries()].filter(([, state]) => state.selected && state.candidates.length > 0).sort(([a], [b]) => a.localeCompare(b));
187
196
  const fixed = new Map([...states].flatMap(([name, state]) => state.selected ? [] : state.current ? [[name, state.current]] : []));
188
- let best = null;
189
- let bestChanged = -1;
190
- let firstFailure = "no jointly peer-compatible published version set exists";
191
- const validate = (choices) => {
197
+ const currentChoices = new Map(variables.flatMap(([name, state]) => state.current ? [[name, state.current]] : []));
198
+ const validate = (choices, domains) => {
192
199
  const installed = new Map([...fixed, ...choices]);
193
200
  for (const [name, version] of installed) {
194
201
  const packument = packuments.get(name);
@@ -198,10 +205,12 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
198
205
  for (const [peer, requirement] of Object.entries(meta.peerDependencies ?? {}).sort(([a], [b]) => a.localeCompare(b))) {
199
206
  if (typeof requirement !== "string") continue;
200
207
  const providerChanged = states.get(name)?.current !== version;
201
- const peerChanged = states.get(peer)?.selected && states.get(peer)?.current !== installed.get(peer);
208
+ const assignedPeer = choices.get(peer);
209
+ const peerChanged = assignedPeer !== void 0 && states.get(peer)?.current !== assignedPeer;
202
210
  if (!providerChanged && !peerChanged) continue;
203
- const peerVersion = installed.get(peer) ?? localVersions.get(peer);
211
+ const peerVersion = assignedPeer ?? (states.get(peer)?.selected ? void 0 : installed.get(peer));
204
212
  if (!peerVersion) {
213
+ if ((domains?.get(peer))?.some((candidate) => semver.satisfies(candidate, requirement, { includePrerelease: true }))) continue;
205
214
  if (optionalPeer(meta, peer)) continue;
206
215
  return `${name}@${version} requires missing peer ${peer}@${requirement}`;
207
216
  }
@@ -210,31 +219,99 @@ function solveWorkspace(workspace, selectedOccurrences, context, packuments, tag
210
219
  }
211
220
  return null;
212
221
  };
213
- const visit = (index, choices) => {
214
- if (index === variables.length) {
215
- const failure = validate(choices);
222
+ const edges = new Map(variables.map(([name]) => [name, /* @__PURE__ */ new Set()]));
223
+ for (const [name, state] of variables) for (const version of state.candidates) {
224
+ const peers = metadata(packuments.get(name), version)?.peerDependencies ?? {};
225
+ for (const peer of Object.keys(peers)) {
226
+ if (!edges.has(peer)) continue;
227
+ edges.get(name).add(peer);
228
+ edges.get(peer).add(name);
229
+ }
230
+ }
231
+ const components = [];
232
+ const unseen = new Set(edges.keys());
233
+ while (unseen.size > 0) {
234
+ const start = [...unseen].sort()[0];
235
+ const component = [];
236
+ const pending = [start];
237
+ unseen.delete(start);
238
+ while (pending.length > 0) {
239
+ const name = pending.pop();
240
+ component.push(name);
241
+ for (const neighbor of [...edges.get(name)].sort().reverse()) {
242
+ if (!unseen.delete(neighbor)) continue;
243
+ pending.push(neighbor);
244
+ }
245
+ }
246
+ components.push(component.sort());
247
+ }
248
+ const choices = new Map(currentChoices);
249
+ const blockers = /* @__PURE__ */ new Map();
250
+ for (const component of components) {
251
+ const componentSet = new Set(component);
252
+ const baseChoices = new Map([...choices].filter(([name]) => !componentSet.has(name)));
253
+ const domains = new Map(component.map((name) => [name, states.get(name).candidates]));
254
+ let best = null;
255
+ let bestChanged = -1;
256
+ let bestVector = [];
257
+ let statesVisited = 0;
258
+ let exhausted = false;
259
+ let firstFailure = "no jointly peer-compatible published version set exists";
260
+ const memo = /* @__PURE__ */ new Set();
261
+ const visit = (assigned) => {
262
+ if (exhausted) return;
263
+ statesVisited += 1;
264
+ if (statesVisited > 5e4) {
265
+ exhausted = true;
266
+ return;
267
+ }
268
+ const merged = new Map([...baseChoices, ...assigned]);
269
+ const remaining = component.filter((name) => !assigned.has(name));
270
+ const changed = component.filter((name) => assigned.has(name) && assigned.get(name) !== states.get(name).current).length;
271
+ if (changed + remaining.length < bestChanged) return;
272
+ const pruned = /* @__PURE__ */ new Map();
273
+ for (const name of remaining) {
274
+ const viable = domains.get(name).filter((version) => validate(new Map([...merged, [name, version]]), domains) === null);
275
+ if (viable.length === 0) {
276
+ firstFailure = validate(merged, domains) ?? firstFailure;
277
+ return;
278
+ }
279
+ pruned.set(name, viable);
280
+ }
281
+ const failure = validate(merged, new Map([...domains, ...pruned]));
216
282
  if (failure) {
217
283
  firstFailure = failure;
218
284
  return;
219
285
  }
220
- const changed = variables.filter(([name, state]) => choices.get(name) !== state.current).length;
221
- if (changed > bestChanged) {
222
- best = new Map(choices);
223
- bestChanged = changed;
286
+ if (remaining.length === 0) {
287
+ const vector = component.map((name) => states.get(name).candidates.indexOf(assigned.get(name)));
288
+ const newer = vector.some((value, index) => value < (bestVector[index] ?? Number.POSITIVE_INFINITY) && vector.slice(0, index).every((prior, priorIndex) => prior === bestVector[priorIndex]));
289
+ if (changed > bestChanged || changed === bestChanged && newer) {
290
+ best = new Map(assigned);
291
+ bestChanged = changed;
292
+ bestVector = vector;
293
+ }
294
+ return;
224
295
  }
225
- return;
226
- }
227
- const [name, state] = variables[index];
228
- for (const version of state.candidates) {
229
- choices.set(name, version);
230
- visit(index + 1, choices);
231
- }
232
- };
233
- visit(0, /* @__PURE__ */ new Map());
234
- const blockers = /* @__PURE__ */ new Map();
235
- if (!best) for (const [name] of variables) blockers.set(name, firstFailure);
236
- const choices = best ?? new Map(variables.flatMap(([name, state]) => state.current ? [[name, state.current]] : []));
296
+ const next = remaining.map((name) => [name, pruned.get(name)]).sort(([leftName, left], [rightName, right]) => left.length - right.length || leftName.localeCompare(rightName))[0];
297
+ const signature = component.map((name) => `${name}=${assigned.get(name) ?? (pruned.get(name) ?? []).join(",")}`).join("|");
298
+ if (memo.has(signature)) return;
299
+ memo.add(signature);
300
+ for (const version of next[1]) {
301
+ assigned.set(next[0], version);
302
+ visit(assigned);
303
+ assigned.delete(next[0]);
304
+ }
305
+ };
306
+ visit(/* @__PURE__ */ new Map());
307
+ if (exhausted) {
308
+ const reason = "peer compatibility search exceeded the 50,000-state budget; resolve this component manually";
309
+ for (const name of component) blockers.set(name, reason);
310
+ } else if (!best) for (const name of component) blockers.set(name, firstFailure);
311
+ else for (const [name, version] of best) choices.set(name, version);
312
+ }
237
313
  for (const [name, state] of variables) {
314
+ if (blockers.has(name)) continue;
238
315
  if (choices.get(name) !== state.current || state.candidates[0] === state.current) continue;
239
316
  const attempted = new Map(choices).set(name, state.candidates[0]);
240
317
  blockers.set(name, validate(attempted) ?? "no jointly peer-compatible update advances this dependency");
@@ -0,0 +1,95 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import Config from "@npmcli/config";
4
+ import npmDefinitions from "@npmcli/config/lib/definitions/index.js";
5
+ import registryFetch from "npm-registry-fetch";
6
+ //#region src/update/registry.ts
7
+ function executionEnvironment(env) {
8
+ if (env === process.env) return { ...process.env };
9
+ const inherited = { ...process.env };
10
+ for (const name of Object.keys(inherited)) if (/^npm_config_/i.test(name)) delete inherited[name];
11
+ return {
12
+ ...inherited,
13
+ ...env
14
+ };
15
+ }
16
+ async function loadNpmConfiguration(root, env = process.env) {
17
+ const effectiveEnvironment = executionEnvironment(env);
18
+ const { definitions, flatten, shorthands } = npmDefinitions;
19
+ const configuration = new Config({
20
+ npmPath: path.dirname(fileURLToPath(import.meta.resolve("@npmcli/config/package.json"))),
21
+ definitions,
22
+ flatten,
23
+ shorthands,
24
+ argv: [process.execPath, "askr"],
25
+ cwd: root,
26
+ env: effectiveEnvironment,
27
+ execPath: process.execPath,
28
+ warn: false
29
+ });
30
+ await configuration.load();
31
+ configuration.validate();
32
+ return {
33
+ cwd: root,
34
+ env: effectiveEnvironment,
35
+ options: { ...configuration.flat }
36
+ };
37
+ }
38
+ function isRecord(value) {
39
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+ function isPackument(value) {
42
+ return isRecord(value) && isRecord(value["dist-tags"]) && isRecord(value.versions);
43
+ }
44
+ async function fetchPackage(packageName, configuration, _specifications) {
45
+ return registryFetch.json(packageName.replace("/", "%2f"), {
46
+ ...configuration.options,
47
+ spec: packageName,
48
+ headers: {
49
+ ...isRecord(configuration.options.headers) ? configuration.options.headers : {},
50
+ accept: "application/vnd.npm.install-v1+json"
51
+ }
52
+ });
53
+ }
54
+ function sanitizeRegistryError(error) {
55
+ const value = error && typeof error === "object" ? error : {};
56
+ if (value.statusCode === 404 || value.code === "E404") return "package was not found in the selected registry";
57
+ if (value.statusCode === 401 || value.statusCode === 403 || value.code === "E401" || value.code === "E403") return "registry authentication or authorization failed";
58
+ const code = typeof value.code === "string" ? value.code.toUpperCase() : "";
59
+ if ([
60
+ "ETIMEDOUT",
61
+ "ETIMEOUT",
62
+ "FETCH_ERROR",
63
+ "ECONNRESET"
64
+ ].includes(code)) return "registry request timed out or was interrupted";
65
+ if (/^[A-Z][A-Z0-9_]{1,30}$/.test(code)) return `registry request failed (${code})`;
66
+ return "registry request failed";
67
+ }
68
+ async function fetchPackuments(packageNames, configuration, options = {}) {
69
+ const names = [...new Set(packageNames)].sort((left, right) => left.localeCompare(right));
70
+ const packuments = /* @__PURE__ */ new Map();
71
+ const failures = /* @__PURE__ */ new Map();
72
+ const viewPackage = options.viewPackage ?? fetchPackage;
73
+ const configuredSockets = Number(configuration.options.maxSockets ?? 15);
74
+ const concurrency = Math.max(1, Math.min(names.length, Number.isFinite(configuredSockets) ? configuredSockets : 15));
75
+ let nextIndex = 0;
76
+ const worker = async () => {
77
+ while (nextIndex < names.length) {
78
+ const packageName = names[nextIndex++];
79
+ try {
80
+ const result = await viewPackage(packageName, configuration, options.requirements?.specifications?.get(packageName) ?? []);
81
+ if (isPackument(result)) packuments.set(packageName, result);
82
+ else failures.set(packageName, "registry returned malformed package metadata");
83
+ } catch (error) {
84
+ failures.set(packageName, sanitizeRegistryError(error));
85
+ }
86
+ }
87
+ };
88
+ await Promise.all(Array.from({ length: concurrency }, worker));
89
+ return {
90
+ packuments,
91
+ failures
92
+ };
93
+ }
94
+ //#endregion
95
+ export { fetchPackuments, loadNpmConfiguration };
@@ -106,12 +106,11 @@ function rewriteXRange(shape, target) {
106
106
  if (count === 1) return String(parsed.major);
107
107
  return `${parsed.major}.${parsed.minor}`;
108
108
  }
109
- function rewriteAtomic(shape, target, forcedBreaking) {
109
+ function rewriteAtomic(shape, target, _forcedBreaking) {
110
110
  if (shape.kind === "exact") return target;
111
111
  if (shape.kind === "operator") return `${shape.operator}${target}`;
112
112
  if (shape.kind === "x") return rewriteXRange(shape, target);
113
- const boundary = nextBreakingBoundary(target);
114
- return forcedBreaking ? `>=${target} <${boundary}` : `${shape.lower} <${boundary}`;
113
+ return `>=${target} <${nextBreakingBoundary(target)}`;
115
114
  }
116
115
  function rewriteRange(shape, target, forcedBreaking) {
117
116
  if (shape.kind !== "union") return rewriteAtomic(shape, target, forcedBreaking);
@@ -122,4 +121,51 @@ function rewriteRange(shape, target, forcedBreaking) {
122
121
  return rewritten.join(" || ");
123
122
  }
124
123
  //#endregion
125
- export { isBreakingChange as n, rewriteRange as r, analyzeRange as t };
124
+ //#region src/update/specification.ts
125
+ const WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
126
+ const HOSTED_GIT = /^(?:bitbucket|gist|github|gitlab):/i;
127
+ const GIT_URL = /^(?:git(?:\+[^:]+)?:|git@|ssh:)/i;
128
+ const GIT_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/;
129
+ const REGISTRY_TAG = /^[A-Za-z][A-Za-z0-9._-]*$/;
130
+ const SCOPED_PATH = /^@[^/]+\/[^/]+$/;
131
+ function parseDependencySpecification(specification) {
132
+ const rawSpec = specification.trim();
133
+ if (!rawSpec) return {
134
+ type: "unsupported",
135
+ rawSpec
136
+ };
137
+ if (/^npm:/i.test(rawSpec)) return {
138
+ type: "alias",
139
+ rawSpec
140
+ };
141
+ if (/^(?:file|link|workspace):/i.test(rawSpec) || rawSpec.startsWith("./") || rawSpec.startsWith("../") || rawSpec.startsWith("/") || rawSpec.startsWith("~/") || /\.(?:tar\.gz|tgz)$/i.test(rawSpec) || WINDOWS_PATH.test(rawSpec) || /^[A-Za-z]:/.test(rawSpec) || SCOPED_PATH.test(rawSpec)) return {
142
+ type: "file",
143
+ rawSpec
144
+ };
145
+ if (HOSTED_GIT.test(rawSpec) || GIT_URL.test(rawSpec) || GIT_SHORTHAND.test(rawSpec)) return {
146
+ type: "git",
147
+ rawSpec
148
+ };
149
+ if (/^https?:/i.test(rawSpec) || /^[a-z][a-z0-9+.-]*:\/\//i.test(rawSpec)) return {
150
+ type: "remote",
151
+ rawSpec
152
+ };
153
+ if (semver.valid(rawSpec)) return {
154
+ type: "version",
155
+ rawSpec
156
+ };
157
+ if (semver.validRange(rawSpec)) return {
158
+ type: "range",
159
+ rawSpec
160
+ };
161
+ if (REGISTRY_TAG.test(rawSpec)) return {
162
+ type: "tag",
163
+ rawSpec
164
+ };
165
+ return {
166
+ type: "unsupported",
167
+ rawSpec
168
+ };
169
+ }
170
+ //#endregion
171
+ export { rewriteRange as i, analyzeRange as n, isBreakingChange as r, parseDependencySpecification as t };
@@ -7,7 +7,18 @@ import { telemetry } from "../telemetry";
7
7
  import { createActionHandlers } from "./action-registry";
8
8
  import type { AppDependencies } from "./dependencies";
9
9
 
10
+ function csrfSecret(): string {
11
+ const secret = process.env.CSRF_SECRET;
12
+ if (process.env.NODE_ENV !== "production") return secret ?? "development-only-secret";
13
+ if (!secret || secret.length < 32 || new Set(secret).size < 12) {
14
+ throw new Error("Production requires a strong CSRF_SECRET (at least 32 varied characters).");
15
+ }
16
+ return secret;
17
+ }
18
+
10
19
  export function createApp(deps: AppDependencies) {
20
+ const secret = csrfSecret();
21
+ const development = process.env.NODE_ENV !== "production";
11
22
  return createAskrApp({
12
23
  name: "{{appName}}",
13
24
  version: "1.0.0",
@@ -37,26 +48,33 @@ export function createApp(deps: AppDependencies) {
37
48
  .summary("Create a message")
38
49
  .tags("Messages")
39
50
  .use(
40
- csrf({ secret: process.env.CSRF_SECRET ?? "development-only-secret" }),
41
- rateLimit({ store: deps.rateLimits, limit: 30, windowMs: 60_000 }),
51
+ csrf({ secret }),
52
+ rateLimit({
53
+ store: deps.rateLimits,
54
+ limit: 30,
55
+ windowMs: 60_000,
56
+ key: (ctx) => `message:${ctx.auth.session?.id ?? "anonymous"}`,
57
+ }),
42
58
  )
43
59
  .created(Message)
44
60
  .badRequest()
45
61
  .unprocessableEntity()
46
62
  .tooManyRequests();
47
- api
48
- .post("/session", (ctx) => {
49
- const response = ctx.redirect("/", 303);
50
- return ctx.setCookie(response, "askr-session", "demo-session", {
51
- httpOnly: true,
52
- sameSite: "lax",
53
- secure: ctx.url.protocol === "https:",
54
- path: "/",
55
- });
56
- })
57
- .operationId("createSession")
58
- .summary("Create a demo session")
59
- .seeOther();
63
+ if (development) {
64
+ api
65
+ .post("/session", (ctx) => {
66
+ const response = ctx.redirect("/", 303);
67
+ return ctx.setCookie(response, "askr-session", "demo-session", {
68
+ httpOnly: true,
69
+ sameSite: "lax",
70
+ secure: ctx.url.protocol === "https:",
71
+ path: "/",
72
+ });
73
+ })
74
+ .operationId("createSession")
75
+ .summary("Create a development-only demo session")
76
+ .seeOther();
77
+ }
60
78
  },
61
79
  },
62
80
  auth: {
@@ -68,7 +86,7 @@ export function createApp(deps: AppDependencies) {
68
86
  },
69
87
  actions: {
70
88
  handlers: createActionHandlers(),
71
- csrf: { secret: process.env.CSRF_SECRET ?? "development-only-secret" },
89
+ csrf: { secret },
72
90
  },
73
91
  middleware: [requestId(), securityHeaders()],
74
92
  telemetry,
@@ -29,11 +29,16 @@ export function createDependencies(): AppDependencies {
29
29
  const counters = new Map<string, { count: number; reset: number }>();
30
30
  return {
31
31
  sessions: {
32
- get: async (id) => (id === "demo-session" ? { id, subject: "demo-user" } : null),
32
+ get: async (id) =>
33
+ process.env.NODE_ENV !== "production" && id === "demo-session"
34
+ ? { id, subject: "demo-user" }
35
+ : null,
33
36
  },
34
37
  principals: {
35
38
  get: async (subject) =>
36
- subject === "demo-user" ? { id: subject, subject, permissions: ["messages:create"] } : null,
39
+ process.env.NODE_ENV !== "production" && subject === "demo-user"
40
+ ? { id: subject, subject, permissions: ["messages:create"] }
41
+ : null,
37
42
  },
38
43
  actions: { record: async () => undefined },
39
44
  rateLimits: {
package/dist/update.js CHANGED
@@ -193,7 +193,7 @@ function collectEdits(decisions) {
193
193
  }] : []));
194
194
  }
195
195
  async function defaultRegistry(root, packageNames, requirements) {
196
- const { fetchPackuments, loadNpmConfiguration } = await import("./registry-D2APi6x0.js");
196
+ const { fetchPackuments, loadNpmConfiguration } = await import("./registry-BVliEbUv.js");
197
197
  return fetchPackuments(packageNames, await loadNpmConfiguration(root), { requirements });
198
198
  }
199
199
  async function runDependencyCli(command, args, io = console, runtime = {}) {
@@ -217,7 +217,7 @@ async function runDependencyCli(command, args, io = console, runtime = {}) {
217
217
  let root = null;
218
218
  let selectedWorkspaces = [];
219
219
  try {
220
- const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-Djq8TxJu.js"), import("./planner-ZAX9SFjY.js")]);
220
+ const [{ discoverProject }, { planUpdates }] = await Promise.all([import("./discovery-BX-lnFRK.js"), import("./planner-DsUWndjl.js")]);
221
221
  const project = await discoverProject({
222
222
  cwd: parsed.cwd,
223
223
  packagePatterns: parsed.packagePatterns,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/cli",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "Unified CLI for the Askr platform",
5
5
  "homepage": "https://github.com/askrjs/askr-cli#readme",
6
6
  "bugs": {
@@ -44,13 +44,17 @@
44
44
  "test:publint": "publint",
45
45
  "pack:check": "npm pack --ignore-scripts --dry-run --json",
46
46
  "test:templates": "node scripts/verify-packed-templates.mjs",
47
+ "bench": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate",
48
+ "bench:json": "npm run build --silent && node --import tsx benchmarks/cli.mjs --gate --json",
47
49
  "check": "npm run lint && npm run typecheck && npm run test:coverage && npm run build && npm run test:publint && npm run pack:check",
48
50
  "prepack": "npm run build",
49
51
  "prepublishOnly": "npm run check && npm run test:templates"
50
52
  },
51
53
  "dependencies": {
54
+ "@npmcli/config": "^10.12.0",
52
55
  "js-yaml": "^5.2.1",
53
56
  "minimatch": "^10.2.5",
57
+ "npm-registry-fetch": "^19.1.1",
54
58
  "semver": "^7.8.5",
55
59
  "tsx": "^4.23.1"
56
60
  },
@@ -1,228 +0,0 @@
1
- import { t as parseDependencySpecification } from "./specification-DXnDOC-0.js";
2
- import path from "node:path";
3
- import { existsSync } from "node:fs";
4
- import { execFile } from "node:child_process";
5
- import semver from "semver";
6
- //#region src/update/registry.ts
7
- function installedNpmCli(executable) {
8
- const directories = /* @__PURE__ */ new Set([path.dirname(process.execPath)]);
9
- if (executable && path.isAbsolute(executable)) directories.add(path.dirname(executable));
10
- for (const directory of directories) for (const candidate of [path.join(directory, "node_modules", "npm", "bin", "npm-cli.js"), path.resolve(directory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js")]) if (existsSync(candidate)) return candidate;
11
- return null;
12
- }
13
- function npmInvocation(env) {
14
- const executable = env.npm_execpath;
15
- const executableName = executable ? path.basename(executable).toLowerCase() : "";
16
- const isNpmExecutable = executableName === "npm" || executableName === "npm.cmd" || /^npm(?:-cli)?\.(?:cjs|js|mjs)$/.test(executableName);
17
- if (executable && isNpmExecutable) {
18
- const extension = path.extname(executable).toLowerCase();
19
- if ([
20
- ".cjs",
21
- ".js",
22
- ".mjs"
23
- ].includes(extension)) return {
24
- executable: process.execPath,
25
- prefix: [executable]
26
- };
27
- const npmCli = installedNpmCli(executable);
28
- return npmCli ? {
29
- executable: process.execPath,
30
- prefix: [npmCli]
31
- } : {
32
- executable,
33
- prefix: []
34
- };
35
- }
36
- const npmCli = installedNpmCli();
37
- if (npmCli) return {
38
- executable: process.execPath,
39
- prefix: [npmCli]
40
- };
41
- return {
42
- executable: process.platform === "win32" ? "npm.cmd" : "npm",
43
- prefix: []
44
- };
45
- }
46
- function executionEnvironment(env) {
47
- if (env === process.env) return { ...process.env };
48
- const inherited = { ...process.env };
49
- for (const name of Object.keys(inherited)) if (/^npm_config_/i.test(name)) delete inherited[name];
50
- return {
51
- ...inherited,
52
- ...env
53
- };
54
- }
55
- async function loadNpmConfiguration(root, env = process.env) {
56
- const effectiveEnvironment = executionEnvironment(env);
57
- return {
58
- cwd: root,
59
- env: effectiveEnvironment,
60
- invocation: npmInvocation(effectiveEnvironment)
61
- };
62
- }
63
- function npmError(error, stderr) {
64
- const details = `${String(error.code ?? "")}\n${stderr}`;
65
- const safe = /* @__PURE__ */ new Error("npm registry lookup failed");
66
- safe.code = (details.match(/\bE(?:401|403|404|CONNRESET|TIMEDOUT|TIMEOUT)\b/i)?.[0])?.toUpperCase() ?? (typeof error.code === "string" ? error.code : "");
67
- if (safe.code === "E401") safe.statusCode = 401;
68
- if (safe.code === "E403") safe.statusCode = 403;
69
- if (safe.code === "E404") safe.statusCode = 404;
70
- return safe;
71
- }
72
- function executeNpmJson(args, configuration, preferOnline) {
73
- const [command, ...positionals] = args;
74
- return new Promise((resolve, reject) => {
75
- execFile(configuration.invocation.executable, [
76
- ...configuration.invocation.prefix,
77
- command,
78
- "--json",
79
- preferOnline ? "--prefer-online" : "--prefer-offline",
80
- "--offline=false",
81
- "--loglevel=error",
82
- "--update-notifier=false",
83
- "--",
84
- ...positionals
85
- ], {
86
- cwd: configuration.cwd,
87
- encoding: "utf8",
88
- env: configuration.env,
89
- maxBuffer: 64 * 1024 * 1024,
90
- windowsHide: true
91
- }, (error, stdout, stderr) => {
92
- if (error) {
93
- reject(npmError(error, stderr));
94
- return;
95
- }
96
- try {
97
- resolve(JSON.parse(stdout));
98
- } catch {
99
- reject(/* @__PURE__ */ new Error("npm registry returned malformed JSON"));
100
- }
101
- });
102
- });
103
- }
104
- function isRecord(value) {
105
- return Boolean(value) && typeof value === "object" && !Array.isArray(value);
106
- }
107
- function basePackument(value) {
108
- if (!isRecord(value) || !Array.isArray(value.versions) || !isRecord(value["dist-tags"])) return null;
109
- const versions = value.versions.filter((version) => typeof version === "string");
110
- if (versions.length === 0) return null;
111
- return {
112
- "dist-tags": value["dist-tags"],
113
- versions: Object.fromEntries(versions.map((version) => [version, { version }]))
114
- };
115
- }
116
- function selectedVersions(packument, specifications) {
117
- const versions = Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null);
118
- const selected = new Set(Object.values(packument["dist-tags"] ?? {}).filter((version) => typeof version === "string" && semver.valid(version) !== null && versions.includes(version)));
119
- for (const specification of specifications) {
120
- const parsed = parseDependencySpecification(specification);
121
- if (parsed.type === "tag") {
122
- const tagged = packument["dist-tags"]?.[parsed.rawSpec];
123
- if (typeof tagged === "string" && versions.includes(tagged)) selected.add(tagged);
124
- } else if (parsed.type === "version") {
125
- if (versions.includes(parsed.rawSpec)) selected.add(parsed.rawSpec);
126
- } else if (parsed.type === "range") {
127
- const matching = semver.maxSatisfying(versions, parsed.rawSpec);
128
- if (matching) selected.add(matching);
129
- }
130
- }
131
- return [...selected].sort(semver.compare);
132
- }
133
- function candidateVersions(packument, specifications) {
134
- const versions = Object.keys(packument.versions ?? {}).filter((version) => semver.valid(version) !== null);
135
- const selected = selectedVersions(packument, specifications);
136
- const lowerBounds = specifications.flatMap((specification) => {
137
- const parsed = parseDependencySpecification(specification);
138
- if (parsed.type === "version") return semver.valid(parsed.rawSpec) ? [parsed.rawSpec] : [];
139
- if (parsed.type === "range") {
140
- const current = semver.maxSatisfying(versions, parsed.rawSpec);
141
- return current ? [current] : [];
142
- }
143
- return [];
144
- });
145
- if (lowerBounds.length === 0 || selected.length === 0) return selected;
146
- const floor = lowerBounds.sort(semver.compare)[0];
147
- const ceiling = selected.sort(semver.compare)[selected.length - 1];
148
- return versions.filter((version) => semver.gte(version, floor) && semver.lte(version, ceiling));
149
- }
150
- function mergeVersionMetadata(packument, value, selected) {
151
- const entries = Array.isArray(value) ? value : [value];
152
- const merged = /* @__PURE__ */ new Set();
153
- for (const entry of entries) {
154
- if (typeof entry === "string") {
155
- if (packument.versions?.[entry]) merged.add(entry);
156
- continue;
157
- }
158
- if (!isRecord(entry) || typeof entry.version !== "string") continue;
159
- const metadata = packument.versions?.[entry.version];
160
- if (!isRecord(metadata)) continue;
161
- if (isRecord(entry.peerDependencies)) metadata.peerDependencies = entry.peerDependencies;
162
- if (isRecord(entry.peerDependenciesMeta)) metadata.peerDependenciesMeta = entry.peerDependenciesMeta;
163
- merged.add(entry.version);
164
- }
165
- return selected.every((version) => merged.has(version));
166
- }
167
- async function executeNpmView(packageName, configuration, specifications) {
168
- const packument = basePackument(await executeNpmJson([
169
- "view",
170
- packageName,
171
- "versions",
172
- "dist-tags"
173
- ], configuration, true));
174
- if (!packument) return null;
175
- const versions = candidateVersions(packument, specifications);
176
- if (versions.length === 0) return packument;
177
- return mergeVersionMetadata(packument, await executeNpmJson([
178
- "view",
179
- `${packageName}@${versions.join(" || ")}`,
180
- "version",
181
- "peerDependencies",
182
- "peerDependenciesMeta"
183
- ], configuration, false), versions) ? packument : null;
184
- }
185
- function isPackument(value) {
186
- return isRecord(value) && isRecord(value["dist-tags"]) && isRecord(value.versions);
187
- }
188
- function sanitizeRegistryError(error) {
189
- const value = error && typeof error === "object" ? error : {};
190
- if (value.statusCode === 404 || value.code === "E404") return "package was not found in the selected registry";
191
- if (value.statusCode === 401 || value.statusCode === 403 || value.code === "E401" || value.code === "E403") return "registry authentication or authorization failed";
192
- const code = typeof value.code === "string" ? value.code.toUpperCase() : "";
193
- if ([
194
- "ETIMEDOUT",
195
- "ETIMEOUT",
196
- "FETCH_ERROR",
197
- "ECONNRESET"
198
- ].includes(code)) return "registry request timed out or was interrupted";
199
- if (/^[A-Z][A-Z0-9_]{1,30}$/.test(code)) return `registry request failed (${code})`;
200
- return "registry request failed";
201
- }
202
- async function fetchPackuments(packageNames, configuration, options = {}) {
203
- const names = [...new Set(packageNames)].sort((left, right) => left.localeCompare(right));
204
- const packuments = /* @__PURE__ */ new Map();
205
- const failures = /* @__PURE__ */ new Map();
206
- const viewPackage = options.viewPackage ?? executeNpmView;
207
- let nextIndex = 0;
208
- const worker = async () => {
209
- while (nextIndex < names.length) {
210
- const packageName = names[nextIndex];
211
- nextIndex += 1;
212
- try {
213
- const result = await viewPackage(packageName, configuration, options.requirements?.specifications?.get(packageName) ?? []);
214
- if (isPackument(result)) packuments.set(packageName, result);
215
- else failures.set(packageName, "registry returned malformed package metadata");
216
- } catch (error) {
217
- failures.set(packageName, sanitizeRegistryError(error));
218
- }
219
- }
220
- };
221
- await Promise.all(Array.from({ length: Math.min(8, names.length) }, worker));
222
- return {
223
- packuments,
224
- failures
225
- };
226
- }
227
- //#endregion
228
- export { fetchPackuments, loadNpmConfiguration };
@@ -1,49 +0,0 @@
1
- import semver from "semver";
2
- //#region src/update/specification.ts
3
- const WINDOWS_PATH = /^[A-Za-z]:[\\/]/;
4
- const HOSTED_GIT = /^(?:bitbucket|gist|github|gitlab):/i;
5
- const GIT_URL = /^(?:git(?:\+[^:]+)?:|git@|ssh:)/i;
6
- const GIT_SHORTHAND = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:#.*)?$/;
7
- const REGISTRY_TAG = /^[A-Za-z][A-Za-z0-9._-]*$/;
8
- const SCOPED_PATH = /^@[^/]+\/[^/]+$/;
9
- function parseDependencySpecification(specification) {
10
- const rawSpec = specification.trim();
11
- if (!rawSpec) return {
12
- type: "unsupported",
13
- rawSpec
14
- };
15
- if (/^npm:/i.test(rawSpec)) return {
16
- type: "alias",
17
- rawSpec
18
- };
19
- if (/^(?:file|link|workspace):/i.test(rawSpec) || rawSpec.startsWith("./") || rawSpec.startsWith("../") || rawSpec.startsWith("/") || rawSpec.startsWith("~/") || /\.(?:tar\.gz|tgz)$/i.test(rawSpec) || WINDOWS_PATH.test(rawSpec) || /^[A-Za-z]:/.test(rawSpec) || SCOPED_PATH.test(rawSpec)) return {
20
- type: "file",
21
- rawSpec
22
- };
23
- if (HOSTED_GIT.test(rawSpec) || GIT_URL.test(rawSpec) || GIT_SHORTHAND.test(rawSpec)) return {
24
- type: "git",
25
- rawSpec
26
- };
27
- if (/^https?:/i.test(rawSpec) || /^[a-z][a-z0-9+.-]*:\/\//i.test(rawSpec)) return {
28
- type: "remote",
29
- rawSpec
30
- };
31
- if (semver.valid(rawSpec)) return {
32
- type: "version",
33
- rawSpec
34
- };
35
- if (semver.validRange(rawSpec)) return {
36
- type: "range",
37
- rawSpec
38
- };
39
- if (REGISTRY_TAG.test(rawSpec)) return {
40
- type: "tag",
41
- rawSpec
42
- };
43
- return {
44
- type: "unsupported",
45
- rawSpec
46
- };
47
- }
48
- //#endregion
49
- export { parseDependencySpecification as t };