@lightnet/cli 4.8.2 → 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,21 @@
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
+
9
+ ## 4.9.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#469](https://github.com/LightNetDev/LightNet/pull/469) [`a0961c4`](https://github.com/LightNetDev/LightNet/commit/a0961c40fdc30a93d3d5c814343f9c12ec26b2ef) - Add repeatable --exclude to check-translations for space-separated exact keys to ignore during completeness checks.
14
+
15
+ ### Patch Changes
16
+
17
+ - [#469](https://github.com/LightNetDev/LightNet/pull/469) [`a0961c4`](https://github.com/LightNetDev/LightNet/commit/a0961c40fdc30a93d3d5c814343f9c12ec26b2ef) - Warn about user-defined ln.* translation overrides that no longer match a built-in English key. Include source filenames and check unused keys without failing check-translations for stale overrides alone.
18
+
3
19
  ## 4.8.2
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -8,6 +8,19 @@ Command-line tools for working with LightNet sites. The CLI currently includes t
8
8
  pnpm add --D @lightnet/cli
9
9
  ```
10
10
 
11
+ ## Exclude translation keys
12
+
13
+ Pass a space-separated list of exact keys to skip their completeness checks:
14
+
15
+ ```bash
16
+ pnpm exec lightnet check-translations --exclude ln.details.edit custom.title
17
+ ```
18
+
19
+ You can repeat `--exclude` to add more keys. Matching is case-sensitive and does
20
+ not support wildcards. Exclusions apply to built-in keys, user keys, and inline
21
+ translation-map paths. Stale override warnings are still reported for excluded
22
+ keys. Use `--build` to refresh the cache or `--no-build` to check the last build.
23
+
11
24
  ## LightNet CLI reference
12
25
 
13
26
  See command-line tools reference documentation at:
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.8.2",
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 {
@@ -27,6 +27,7 @@ import { cancelPrompt } from "./support/prompt-cancel.js"
27
27
  /**
28
28
  * @typedef {{
29
29
  * build?: boolean
30
+ * exclude?: string[]
30
31
  * }} CheckTranslationsOptions
31
32
  */
32
33
 
@@ -88,10 +89,24 @@ export async function checkTranslations(options = {}, runtime = {}) {
88
89
 
89
90
  const translations = await readTranslations(lightnetCachePath, output)
90
91
  const languages = await readLanguages(lightnetCachePath, output)
92
+ const staleOverrides = await readStaleOverrides(lightnetCachePath)
93
+ if (staleOverrides.length > 0) {
94
+ output.warn("Stale LightNet translation overrides")
95
+ for (const { key, path } of staleOverrides.toSorted(
96
+ (a, b) => a.key.localeCompare(b.key) || a.path.localeCompare(b.path),
97
+ )) {
98
+ output.message(`• ${key} — ${path}`)
99
+ }
100
+ output.message(
101
+ "Action: Remove these overrides or rename custom keys without the reserved ln. prefix.",
102
+ )
103
+ }
91
104
  if (!translations || !languages || translations.length === 0) {
92
105
  return false
93
106
  }
107
+ const excludedKeys = new Set(options.exclude ?? [])
94
108
  const incompleteTranslations = translations
109
+ .filter((translation) => !excludedKeys.has(translation.key))
95
110
  .map((translation) => ({
96
111
  ...translation,
97
112
  missingLocales: getMissingLocales(translation, languages),
@@ -99,7 +114,11 @@ export async function checkTranslations(options = {}, runtime = {}) {
99
114
  .filter((translation) => translation.missingLocales.length > 0)
100
115
 
101
116
  if (incompleteTranslations.length === 0) {
102
- output.outro("No issues found. 🎉")
117
+ output.outro(
118
+ staleOverrides.length > 0
119
+ ? "No missing translations. Stale overrides found."
120
+ : "No issues found. 🎉",
121
+ )
103
122
  return true
104
123
  }
105
124
 
@@ -236,6 +255,32 @@ function getMissingLocales(translation, languages) {
236
255
  return languages.locales.filter((locale) => !translation.values[locale])
237
256
  }
238
257
 
258
+ /**
259
+ * @param {string} lightnetCachePath
260
+ * @returns {Promise<{key: string, path: string}[]>}
261
+ */
262
+ async function readStaleOverrides(lightnetCachePath) {
263
+ try {
264
+ return JSON.parse(
265
+ await readFile(
266
+ resolve(lightnetCachePath, "stale-translation-overrides.json"),
267
+ "utf-8",
268
+ ),
269
+ )
270
+ } catch (error) {
271
+ // Older builds do not include stale override metadata.
272
+ if (
273
+ error &&
274
+ typeof error === "object" &&
275
+ "code" in error &&
276
+ error.code === "ENOENT"
277
+ ) {
278
+ return []
279
+ }
280
+ throw error
281
+ }
282
+ }
283
+
239
284
  /**
240
285
  * @param {string} lightnetCachePath
241
286
  * @param {ReturnType<typeof createOutput>} output
package/src/index.js CHANGED
@@ -25,6 +25,11 @@ program
25
25
  .description("check if last build has been missing any translations")
26
26
  .option("--build", "run pnpm build before checking translations")
27
27
  .option("--no-build", "skip the build prompt and use the latest dist/ output")
28
+ .option(
29
+ "--exclude <keys...>",
30
+ "exclude exact translation keys from completeness checks (repeatable)",
31
+ [],
32
+ )
28
33
  .action(async (options) => {
29
34
  try {
30
35
  const checkSuccessful = await checkTranslations(options)