@lightnet/cli 4.9.0 → 4.9.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @lightnet/cli
2
2
 
3
+ ## 4.9.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#471](https://github.com/LightNetDev/LightNet/pull/471) [`667909c`](https://github.com/LightNetDev/LightNet/commit/667909c629835b0bf7f697c6d0a92c1ad92a733e) - Check media links using a range GET and verify the first body byte can be read. Retry unsatisfiable ranges with a plain GET to support empty resources.
8
+
3
9
  ## 4.9.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "CLI for managing LightNet projects",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
- "version": "4.9.0",
6
+ "version": "4.9.1",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -18,9 +18,9 @@ import { cancelPrompt } from "./support/prompt-cancel.js"
18
18
  const mediaDir = "src/content/media"
19
19
  const defaultConcurrency = 24
20
20
  const defaultTimeoutMs = 10_000
21
- const headFallbackStatuses = new Set([405, 501])
22
21
  const requestHeaders = {
23
22
  accept: "*/*",
23
+ "accept-encoding": "identity",
24
24
  "user-agent": "@lightnet/cli link checker",
25
25
  }
26
26
 
@@ -359,22 +359,17 @@ function resolveUrl(url, siteUrl) {
359
359
  * @returns {Promise<LinkCheckResult>}
360
360
  */
361
361
  async function checkLink(url, options) {
362
- const headResult = await fetchStatus(url, {
362
+ const rangeResult = await fetchStatus(url, {
363
363
  ...options,
364
- headers: requestHeaders,
365
- method: "HEAD",
364
+ headers: { ...requestHeaders, range: "bytes=0-0" },
366
365
  })
367
- if (headResult.ok || !headFallbackStatuses.has(headResult.status ?? 0)) {
368
- return headResult
366
+ if (rangeResult.status !== 416) {
367
+ return rangeResult
369
368
  }
369
+ // An empty resource has no first byte; retry without the range constraint.
370
370
  return fetchStatus(url, {
371
371
  ...options,
372
- headers: {
373
- ...requestHeaders,
374
- "accept-encoding": "identity",
375
- range: "bytes=0-0",
376
- },
377
- method: "GET",
372
+ headers: requestHeaders,
378
373
  })
379
374
  }
380
375
 
@@ -432,7 +427,6 @@ function formatReferenceSources(sources) {
432
427
  * @param {{
433
428
  * fetch: typeof fetch
434
429
  * headers?: Record<string, string>
435
- * method: "GET"|"HEAD"
436
430
  * timeoutMs: number
437
431
  * }} options
438
432
  * @returns {Promise<LinkCheckResult>}
@@ -443,12 +437,42 @@ async function fetchStatus(url, options) {
443
437
  try {
444
438
  const response = await options.fetch(url, {
445
439
  headers: options.headers,
446
- method: options.method,
440
+ method: "GET",
447
441
  redirect: "follow",
448
442
  signal: controller.signal,
449
443
  })
450
- await response.body?.cancel()
451
444
  const ok = response.status >= 200 && response.status < 400
445
+ if (ok) {
446
+ const expectsBody =
447
+ response.status === 206 ||
448
+ Number(response.headers.get("content-length")) > 0
449
+ const reader = response.body?.getReader()
450
+ let receivedByte = false
451
+ try {
452
+ // A server may ignore Range and stream the full file. Stop as soon as
453
+ // the first nonempty chunk proves that the body can be read.
454
+ if (reader) {
455
+ while (true) {
456
+ const { done, value } = await reader.read()
457
+ if (done) break
458
+ if (value.byteLength > 0) {
459
+ receivedByte = true
460
+ break
461
+ }
462
+ }
463
+ }
464
+ } finally {
465
+ await reader?.cancel()
466
+ }
467
+ if (expectsBody && !receivedByte) {
468
+ return {
469
+ ok: false,
470
+ reason: "Response body ended before the first byte",
471
+ }
472
+ }
473
+ } else {
474
+ await response.body?.cancel()
475
+ }
452
476
  return { ok, status: response.status }
453
477
  } catch (error) {
454
478
  return {