@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 +16 -0
- package/README.md +13 -0
- package/package.json +1 -1
- package/src/check-links.js +39 -15
- package/src/check-translations.js +46 -1
- package/src/index.js +5 -0
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
package/src/check-links.js
CHANGED
|
@@ -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
|
|
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 (
|
|
368
|
-
return
|
|
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:
|
|
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(
|
|
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)
|