@lightnet/cli 4.7.0 → 4.8.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/CHANGELOG.md +20 -0
- package/package.json +8 -2
- package/src/check-files.js +1 -1
- package/src/check-links.js +145 -9
- package/src/check-translations.js +99 -30
- package/src/index.js +9 -1
- package/src/r2.js +115 -2
- package/src/support/r2.js +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @lightnet/cli
|
|
2
2
|
|
|
3
|
+
## 4.8.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#449](https://github.com/LightNetDev/LightNet/pull/449) [`b4b1ba9`](https://github.com/LightNetDev/LightNet/commit/b4b1ba9e591af8f6870f2b3eb04029820a008393) - Add repeatable `--exclude` glob exclusions to `check-links` for explicitly exempt media URLs.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- [#449](https://github.com/LightNetDev/LightNet/pull/449) [`b4b1ba9`](https://github.com/LightNetDev/LightNet/commit/b4b1ba9e591af8f6870f2b3eb04029820a008393) - Update dependencies
|
|
12
|
+
|
|
13
|
+
- [#449](https://github.com/LightNetDev/LightNet/pull/449) [`b4b1ba9`](https://github.com/LightNetDev/LightNet/commit/b4b1ba9e591af8f6870f2b3eb04029820a008393) - Support non interactive usage of check-translations cli script.
|
|
14
|
+
|
|
15
|
+
- [#449](https://github.com/LightNetDev/LightNet/pull/449) [`b4b1ba9`](https://github.com/LightNetDev/LightNet/commit/b4b1ba9e591af8f6870f2b3eb04029820a008393) - Internal fix typings after dependency update.
|
|
16
|
+
|
|
17
|
+
## 4.7.1
|
|
18
|
+
|
|
19
|
+
### Patch Changes
|
|
20
|
+
|
|
21
|
+
- [#447](https://github.com/LightNetDev/LightNet/pull/447) [`0d8fece`](https://github.com/LightNetDev/LightNet/commit/0d8fece3527a67a21c6272b422c62152948b32a1) - Only prompt for `lightnet r2 cp` and `lightnet r2 mv` when files would actually be overwritten, and list the destination files before confirmation.
|
|
22
|
+
|
|
3
23
|
## 4.7.0
|
|
4
24
|
|
|
5
25
|
### 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.
|
|
6
|
+
"version": "4.8.0",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
9
9
|
},
|
|
@@ -25,10 +25,16 @@
|
|
|
25
25
|
"CHANGELOG.md"
|
|
26
26
|
],
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@clack/prompts": "^1.
|
|
28
|
+
"@clack/prompts": "^1.8.0",
|
|
29
29
|
"commander": "^15.0.0"
|
|
30
30
|
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"vitest": "^5.0.0"
|
|
33
|
+
},
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "vitest"
|
|
33
39
|
}
|
|
34
40
|
}
|
package/src/check-files.js
CHANGED
package/src/check-links.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { posix, resolve } from "node:path"
|
|
3
|
+
import { matchesGlob, posix, resolve } from "node:path"
|
|
4
4
|
import { cwd as processCwd, stdin, stdout } from "node:process"
|
|
5
5
|
|
|
6
6
|
import { intro, isCancel, log, outro, progress, text } from "@clack/prompts"
|
|
@@ -28,6 +28,7 @@ const requestHeaders = {
|
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
30
|
* @typedef {{
|
|
31
|
+
* exclude?: string[]
|
|
31
32
|
* timeout?: string
|
|
32
33
|
* }} CheckLinksOptions
|
|
33
34
|
*/
|
|
@@ -49,6 +50,14 @@ const requestHeaders = {
|
|
|
49
50
|
* }} LinkReference
|
|
50
51
|
*/
|
|
51
52
|
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {{
|
|
55
|
+
* displayUrl: string
|
|
56
|
+
* patterns: Set<string>
|
|
57
|
+
* sources: Set<string>
|
|
58
|
+
* }} ExcludedLinkReference
|
|
59
|
+
*/
|
|
60
|
+
|
|
52
61
|
/**
|
|
53
62
|
* @typedef {{
|
|
54
63
|
* ok: boolean
|
|
@@ -73,6 +82,7 @@ export async function checkLinks(options, runtime = {}) {
|
|
|
73
82
|
"--timeout",
|
|
74
83
|
defaultTimeoutMs,
|
|
75
84
|
)
|
|
85
|
+
const excludePatterns = parseExcludePatterns(options.exclude)
|
|
76
86
|
|
|
77
87
|
intro("check-links")
|
|
78
88
|
|
|
@@ -82,17 +92,28 @@ export async function checkLinks(options, runtime = {}) {
|
|
|
82
92
|
const mediaItems = await collections.getMediaItems()
|
|
83
93
|
const siteUrl = await initSiteUrl({
|
|
84
94
|
cwd,
|
|
95
|
+
excludePatterns,
|
|
85
96
|
interactive,
|
|
86
97
|
mediaItems,
|
|
87
98
|
promptText,
|
|
88
99
|
})
|
|
89
|
-
const references = collectLinkReferences(
|
|
100
|
+
const { excludedReferences, references } = collectLinkReferences(
|
|
101
|
+
mediaItems,
|
|
102
|
+
siteUrl,
|
|
103
|
+
excludePatterns,
|
|
104
|
+
)
|
|
90
105
|
log.message(
|
|
91
106
|
`Checking ${references.length} unique links from ${mediaItems.length} media items.`,
|
|
92
107
|
)
|
|
93
108
|
|
|
109
|
+
printExcludedLinks(excludedReferences)
|
|
110
|
+
|
|
94
111
|
if (references.length === 0) {
|
|
95
|
-
log.warn(
|
|
112
|
+
log.warn(
|
|
113
|
+
excludedReferences.length > 0
|
|
114
|
+
? "No media content URLs require checking."
|
|
115
|
+
: "No media content URLs found.",
|
|
116
|
+
)
|
|
96
117
|
outro("No issues found. 🎉")
|
|
97
118
|
return true
|
|
98
119
|
}
|
|
@@ -179,14 +200,25 @@ async function checkReferencesWithProgress(references, options) {
|
|
|
179
200
|
/**
|
|
180
201
|
* @param {{
|
|
181
202
|
* cwd: string
|
|
203
|
+
* excludePatterns: string[]
|
|
182
204
|
* interactive: boolean
|
|
183
205
|
* mediaItems: MediaItem[]
|
|
184
206
|
* promptText: (message: string) => Promise<string>
|
|
185
207
|
* }} args
|
|
186
208
|
*/
|
|
187
|
-
async function initSiteUrl({
|
|
209
|
+
async function initSiteUrl({
|
|
210
|
+
cwd,
|
|
211
|
+
excludePatterns,
|
|
212
|
+
interactive,
|
|
213
|
+
mediaItems,
|
|
214
|
+
promptText,
|
|
215
|
+
}) {
|
|
188
216
|
const needsSiteUrl = mediaItems.some((item) =>
|
|
189
|
-
item.content.some(
|
|
217
|
+
item.content.some(
|
|
218
|
+
(contentItem) =>
|
|
219
|
+
isRootRelativeUrl(contentItem.url) &&
|
|
220
|
+
findMatchingPatterns(contentItem.url, excludePatterns).length === 0,
|
|
221
|
+
),
|
|
190
222
|
)
|
|
191
223
|
if (!needsSiteUrl) {
|
|
192
224
|
return undefined
|
|
@@ -236,13 +268,36 @@ function parseSiteUrl(config) {
|
|
|
236
268
|
/**
|
|
237
269
|
* @param {MediaItem[]} mediaItems
|
|
238
270
|
* @param {string|undefined} siteUrl
|
|
271
|
+
* @param {string[]} excludePatterns
|
|
239
272
|
*/
|
|
240
|
-
function collectLinkReferences(mediaItems, siteUrl) {
|
|
273
|
+
function collectLinkReferences(mediaItems, siteUrl, excludePatterns) {
|
|
241
274
|
/** @type {Map<string, LinkReference>} */
|
|
242
275
|
const references = new Map()
|
|
276
|
+
/** @type {Map<string, ExcludedLinkReference>} */
|
|
277
|
+
const excludedReferences = new Map()
|
|
243
278
|
for (const item of mediaItems) {
|
|
244
279
|
const sourceFileName = posix.basename(toPosixPath(item.path))
|
|
245
280
|
for (const contentItem of item.content) {
|
|
281
|
+
const matchingPatterns = findMatchingPatterns(
|
|
282
|
+
contentItem.url,
|
|
283
|
+
excludePatterns,
|
|
284
|
+
)
|
|
285
|
+
if (matchingPatterns.length > 0) {
|
|
286
|
+
const current = excludedReferences.get(contentItem.url)
|
|
287
|
+
if (current) {
|
|
288
|
+
current.sources.add(sourceFileName)
|
|
289
|
+
for (const pattern of matchingPatterns) {
|
|
290
|
+
current.patterns.add(pattern)
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
excludedReferences.set(contentItem.url, {
|
|
294
|
+
displayUrl: contentItem.url,
|
|
295
|
+
patterns: new Set(matchingPatterns),
|
|
296
|
+
sources: new Set([sourceFileName]),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
246
301
|
const resolvedUrl = resolveUrl(contentItem.url, siteUrl)
|
|
247
302
|
const current = references.get(resolvedUrl)
|
|
248
303
|
if (current) {
|
|
@@ -259,9 +314,14 @@ function collectLinkReferences(mediaItems, siteUrl) {
|
|
|
259
314
|
}
|
|
260
315
|
}
|
|
261
316
|
}
|
|
262
|
-
return
|
|
263
|
-
|
|
264
|
-
|
|
317
|
+
return {
|
|
318
|
+
excludedReferences: [...excludedReferences.values()].sort((a, b) =>
|
|
319
|
+
a.displayUrl.localeCompare(b.displayUrl),
|
|
320
|
+
),
|
|
321
|
+
references: [...references.values()].sort((a, b) =>
|
|
322
|
+
a.displayUrl.localeCompare(b.displayUrl),
|
|
323
|
+
),
|
|
324
|
+
}
|
|
265
325
|
}
|
|
266
326
|
|
|
267
327
|
/**
|
|
@@ -334,6 +394,25 @@ function printProtectedLinks(links) {
|
|
|
334
394
|
}
|
|
335
395
|
}
|
|
336
396
|
|
|
397
|
+
/**
|
|
398
|
+
* @param {ExcludedLinkReference[]} links
|
|
399
|
+
*/
|
|
400
|
+
function printExcludedLinks(links) {
|
|
401
|
+
if (links.length === 0) {
|
|
402
|
+
return
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
log.info(`Excluded links (${links.length})`)
|
|
406
|
+
for (const link of links) {
|
|
407
|
+
const patterns = [...link.patterns]
|
|
408
|
+
.toSorted()
|
|
409
|
+
.map((pattern) => `"${pattern}"`)
|
|
410
|
+
log.message(
|
|
411
|
+
`• ${link.displayUrl}\n Excluded by: ${patterns.join(", ")}\n${formatReferenceSources(link.sources)}`,
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
337
416
|
/**
|
|
338
417
|
* @param {Set<string>} sources
|
|
339
418
|
*/
|
|
@@ -426,6 +505,63 @@ function parsePositiveInteger(rawValue, optionName, defaultValue) {
|
|
|
426
505
|
return value
|
|
427
506
|
}
|
|
428
507
|
|
|
508
|
+
/**
|
|
509
|
+
* @param {string[]|undefined} patterns
|
|
510
|
+
*/
|
|
511
|
+
function parseExcludePatterns(patterns) {
|
|
512
|
+
return (patterns ?? []).map((pattern) => {
|
|
513
|
+
if (!isValidGlobPattern(pattern)) {
|
|
514
|
+
throw new CliError(`Invalid "--exclude" glob pattern "${pattern}".`)
|
|
515
|
+
}
|
|
516
|
+
return pattern
|
|
517
|
+
})
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* @param {string} url
|
|
522
|
+
* @param {string[]} patterns
|
|
523
|
+
*/
|
|
524
|
+
function findMatchingPatterns(url, patterns) {
|
|
525
|
+
return patterns.filter((pattern) => matchesGlob(url, pattern))
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Node's glob matcher treats unclosed groups as non-matches, so validate them
|
|
530
|
+
* explicitly to turn common malformed patterns into actionable CLI errors.
|
|
531
|
+
*
|
|
532
|
+
* @param {string} pattern
|
|
533
|
+
*/
|
|
534
|
+
function isValidGlobPattern(pattern) {
|
|
535
|
+
/** @type {Partial<Record<string, string>>} */
|
|
536
|
+
const pairs = { "[": "]", "{": "}", "(": ")" }
|
|
537
|
+
/** @type {string[]} */
|
|
538
|
+
const expectedClosers = []
|
|
539
|
+
let escaped = false
|
|
540
|
+
|
|
541
|
+
for (const character of pattern) {
|
|
542
|
+
if (escaped) {
|
|
543
|
+
escaped = false
|
|
544
|
+
continue
|
|
545
|
+
}
|
|
546
|
+
if (character === "\\") {
|
|
547
|
+
escaped = true
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
const closer = pairs[character]
|
|
551
|
+
if (closer) {
|
|
552
|
+
expectedClosers.push(closer)
|
|
553
|
+
continue
|
|
554
|
+
}
|
|
555
|
+
if (character === "]" || character === "}" || character === ")") {
|
|
556
|
+
if (expectedClosers.pop() !== character) {
|
|
557
|
+
return false
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return !escaped && expectedClosers.length === 0
|
|
563
|
+
}
|
|
564
|
+
|
|
429
565
|
/**
|
|
430
566
|
* @param {string} value
|
|
431
567
|
* @param {string} label
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { spawn } from "node:child_process"
|
|
4
4
|
import { readFile } from "node:fs/promises"
|
|
5
5
|
import { resolve } from "node:path"
|
|
6
|
-
import { cwd } from "node:process"
|
|
6
|
+
import { cwd, stdin, stdout } from "node:process"
|
|
7
7
|
|
|
8
8
|
import { confirm, intro, isCancel, log, outro, taskLog } from "@clack/prompts"
|
|
9
9
|
|
|
@@ -30,7 +30,14 @@ import { cancelPrompt } from "./support/prompt-cancel.js"
|
|
|
30
30
|
* }} CheckTranslationsOptions
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* @typedef {{
|
|
35
|
+
* cwd?: string
|
|
36
|
+
* isInteractive?: boolean
|
|
37
|
+
* promptRunBuild?: () => Promise<boolean>
|
|
38
|
+
* write?: (message: string) => void
|
|
39
|
+
* }} CheckTranslationsRuntime
|
|
40
|
+
*/
|
|
34
41
|
|
|
35
42
|
/** @type {{type:Translation["type"], title:string, action:string}[]} */
|
|
36
43
|
const translationSources = [
|
|
@@ -54,18 +61,33 @@ const translationSources = [
|
|
|
54
61
|
|
|
55
62
|
/**
|
|
56
63
|
* @param {CheckTranslationsOptions} [options]
|
|
64
|
+
* @param {CheckTranslationsRuntime} [runtime]
|
|
57
65
|
*/
|
|
58
|
-
export async function checkTranslations(options = {}) {
|
|
59
|
-
|
|
66
|
+
export async function checkTranslations(options = {}, runtime = {}) {
|
|
67
|
+
const lightnetCachePath = resolve(
|
|
68
|
+
runtime.cwd ?? cwd(),
|
|
69
|
+
"node_modules",
|
|
70
|
+
".cache",
|
|
71
|
+
"lightnet",
|
|
72
|
+
)
|
|
73
|
+
const interactive =
|
|
74
|
+
runtime.isInteractive ?? Boolean(stdin.isTTY && stdout.isTTY)
|
|
75
|
+
const output = createOutput(interactive, runtime.write)
|
|
76
|
+
|
|
77
|
+
output.intro("check-translations")
|
|
60
78
|
|
|
61
|
-
const buildAvailable = await runBuild(options.build
|
|
79
|
+
const buildAvailable = await runBuild(options.build, {
|
|
80
|
+
interactive,
|
|
81
|
+
output,
|
|
82
|
+
promptRunBuild: runtime.promptRunBuild,
|
|
83
|
+
})
|
|
62
84
|
if (!buildAvailable) {
|
|
63
|
-
outro("Build failed. 🚧")
|
|
85
|
+
output.outro("Build failed. 🚧")
|
|
64
86
|
return false
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
const translations = await readTranslations()
|
|
68
|
-
const languages = await readLanguages()
|
|
89
|
+
const translations = await readTranslations(lightnetCachePath, output)
|
|
90
|
+
const languages = await readLanguages(lightnetCachePath, output)
|
|
69
91
|
if (!translations || !languages || translations.length === 0) {
|
|
70
92
|
return false
|
|
71
93
|
}
|
|
@@ -77,7 +99,7 @@ export async function checkTranslations(options = {}) {
|
|
|
77
99
|
.filter((translation) => translation.missingLocales.length > 0)
|
|
78
100
|
|
|
79
101
|
if (incompleteTranslations.length === 0) {
|
|
80
|
-
outro("No issues found. 🎉")
|
|
102
|
+
output.outro("No issues found. 🎉")
|
|
81
103
|
return true
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -86,29 +108,42 @@ export async function checkTranslations(options = {}) {
|
|
|
86
108
|
(translation) => translation.type,
|
|
87
109
|
)
|
|
88
110
|
|
|
89
|
-
|
|
111
|
+
output.error("Translation check failed")
|
|
90
112
|
for (const source of translationSources) {
|
|
91
|
-
printMissingTranslations(source, grouped[source.type])
|
|
113
|
+
printMissingTranslations(source, grouped[source.type], output)
|
|
92
114
|
}
|
|
93
115
|
|
|
94
|
-
outro("Issues found. 🚧")
|
|
116
|
+
output.outro("Issues found. 🚧")
|
|
95
117
|
|
|
96
118
|
return false
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/**
|
|
100
|
-
*
|
|
101
122
|
* @param {boolean|undefined} build
|
|
102
|
-
* @
|
|
123
|
+
* @param {{
|
|
124
|
+
* interactive: boolean
|
|
125
|
+
* output: ReturnType<typeof createOutput>
|
|
126
|
+
* promptRunBuild?: () => Promise<boolean>
|
|
127
|
+
* }} runtime
|
|
103
128
|
*/
|
|
104
|
-
async function runBuild(build) {
|
|
105
|
-
const shouldRunBuild =
|
|
129
|
+
async function runBuild(build, { interactive, output, promptRunBuild }) {
|
|
130
|
+
const shouldRunBuild =
|
|
131
|
+
build ??
|
|
132
|
+
(interactive ? await (promptRunBuild ?? defaultPromptRunBuild)() : false)
|
|
106
133
|
if (!shouldRunBuild) {
|
|
107
134
|
return true
|
|
108
135
|
}
|
|
109
|
-
const buildLog =
|
|
110
|
-
title: "Running pnpm build"
|
|
111
|
-
|
|
136
|
+
const buildLog = interactive
|
|
137
|
+
? taskLog({ title: "Running pnpm build" })
|
|
138
|
+
: {
|
|
139
|
+
error: output.error,
|
|
140
|
+
message: output.message,
|
|
141
|
+
success: output.message,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (!interactive) {
|
|
145
|
+
output.message("Running pnpm build")
|
|
146
|
+
}
|
|
112
147
|
|
|
113
148
|
const child = spawn("pnpm", ["build"], {
|
|
114
149
|
shell: process.platform === "win32",
|
|
@@ -154,7 +189,7 @@ async function runBuild(build) {
|
|
|
154
189
|
}
|
|
155
190
|
}
|
|
156
191
|
|
|
157
|
-
async function
|
|
192
|
+
async function defaultPromptRunBuild() {
|
|
158
193
|
const answer = await confirm({
|
|
159
194
|
message:
|
|
160
195
|
"Run pnpm build now? Command requires an up-to-date dist/ directory.",
|
|
@@ -170,13 +205,14 @@ async function promptRunBuild() {
|
|
|
170
205
|
*
|
|
171
206
|
* @param {{title:string, action:string}} source
|
|
172
207
|
* @param {(Translation & {missingLocales:string[]})[]|undefined} translations
|
|
208
|
+
* @param {ReturnType<typeof createOutput>} output
|
|
173
209
|
*/
|
|
174
|
-
function printMissingTranslations(source, translations) {
|
|
210
|
+
function printMissingTranslations(source, translations, output) {
|
|
175
211
|
if (!translations || translations.length === 0) {
|
|
176
212
|
return
|
|
177
213
|
}
|
|
178
214
|
|
|
179
|
-
|
|
215
|
+
output.warn(source.title)
|
|
180
216
|
translations
|
|
181
217
|
.toSorted(
|
|
182
218
|
(t1, t2) =>
|
|
@@ -184,10 +220,10 @@ function printMissingTranslations(source, translations) {
|
|
|
184
220
|
t1.key.localeCompare(t2.key),
|
|
185
221
|
)
|
|
186
222
|
.forEach(({ key, missingLocales }) => {
|
|
187
|
-
|
|
223
|
+
output.message(`• ${key} > Missing: ${missingLocales.join(", ")}`)
|
|
188
224
|
})
|
|
189
225
|
|
|
190
|
-
|
|
226
|
+
output.message(`Action: ${source.action}`)
|
|
191
227
|
}
|
|
192
228
|
|
|
193
229
|
/**
|
|
@@ -201,9 +237,11 @@ function getMissingLocales(translation, languages) {
|
|
|
201
237
|
}
|
|
202
238
|
|
|
203
239
|
/**
|
|
240
|
+
* @param {string} lightnetCachePath
|
|
241
|
+
* @param {ReturnType<typeof createOutput>} output
|
|
204
242
|
* @returns {Promise<Translation[]|undefined>}
|
|
205
243
|
*/
|
|
206
|
-
async function readTranslations() {
|
|
244
|
+
async function readTranslations(lightnetCachePath, output) {
|
|
207
245
|
try {
|
|
208
246
|
const translationsText = await readFile(
|
|
209
247
|
resolve(lightnetCachePath, "translations.jsonl"),
|
|
@@ -214,16 +252,18 @@ async function readTranslations() {
|
|
|
214
252
|
.filter((line) => line.trim())
|
|
215
253
|
.map((line) => JSON.parse(line))
|
|
216
254
|
} catch {
|
|
217
|
-
|
|
218
|
-
|
|
255
|
+
output.error("No translation build cache found.")
|
|
256
|
+
output.error("Action: Run build and try lightnet check-translations again.")
|
|
219
257
|
return undefined
|
|
220
258
|
}
|
|
221
259
|
}
|
|
222
260
|
|
|
223
261
|
/**
|
|
262
|
+
* @param {string} lightnetCachePath
|
|
263
|
+
* @param {ReturnType<typeof createOutput>} output
|
|
224
264
|
* @returns {Promise<Languages|undefined>}
|
|
225
265
|
*/
|
|
226
|
-
async function readLanguages() {
|
|
266
|
+
async function readLanguages(lightnetCachePath, output) {
|
|
227
267
|
try {
|
|
228
268
|
const languagesText = await readFile(
|
|
229
269
|
resolve(lightnetCachePath, "languages.json"),
|
|
@@ -231,8 +271,37 @@ async function readLanguages() {
|
|
|
231
271
|
)
|
|
232
272
|
return JSON.parse(languagesText)
|
|
233
273
|
} catch {
|
|
234
|
-
|
|
235
|
-
|
|
274
|
+
output.error("No language manifest found from the last build.")
|
|
275
|
+
output.error("Action: Run build and try lightnet check-translations again.")
|
|
236
276
|
return undefined
|
|
237
277
|
}
|
|
238
278
|
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* @param {boolean} interactive
|
|
282
|
+
* @param {(message: string) => void | undefined} write
|
|
283
|
+
*/
|
|
284
|
+
function createOutput(
|
|
285
|
+
interactive,
|
|
286
|
+
write = (message) => {
|
|
287
|
+
stdout.write(`${message}\n`)
|
|
288
|
+
},
|
|
289
|
+
) {
|
|
290
|
+
if (interactive) {
|
|
291
|
+
return {
|
|
292
|
+
error: log.error,
|
|
293
|
+
intro,
|
|
294
|
+
message: log.message,
|
|
295
|
+
outro,
|
|
296
|
+
warn: log.warn,
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
error: write,
|
|
302
|
+
intro: write,
|
|
303
|
+
message: write,
|
|
304
|
+
outro: write,
|
|
305
|
+
warn: write,
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
-
import { Command } from "commander"
|
|
4
|
+
import { Command, Option } from "commander"
|
|
5
5
|
|
|
6
6
|
import pkg from "../package.json" with { type: "json" }
|
|
7
7
|
import { checkFiles } from "./check-files.js"
|
|
@@ -62,6 +62,14 @@ program
|
|
|
62
62
|
.command("check-links")
|
|
63
63
|
.description("check media content links in a LightNet site")
|
|
64
64
|
.option("--timeout <ms>", "request timeout per link in milliseconds")
|
|
65
|
+
.addOption(
|
|
66
|
+
new Option(
|
|
67
|
+
"--exclude <pattern>",
|
|
68
|
+
"exclude media content URLs matching a glob pattern (repeatable)",
|
|
69
|
+
)
|
|
70
|
+
.default([])
|
|
71
|
+
.argParser((pattern, patterns) => [...patterns, pattern]),
|
|
72
|
+
)
|
|
65
73
|
.action(async (options) => {
|
|
66
74
|
try {
|
|
67
75
|
const checkSuccessful = await checkLinks(options)
|
package/src/r2.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import { stat } from "node:fs/promises"
|
|
3
|
+
import { readdir, stat } from "node:fs/promises"
|
|
4
4
|
import { basename, join, posix, resolve } from "node:path"
|
|
5
5
|
import {
|
|
6
6
|
cwd as processCwd,
|
|
@@ -78,6 +78,7 @@ const defaultRcloneOptions = {
|
|
|
78
78
|
* isInteractive?: boolean
|
|
79
79
|
* promptConfirm?: (message: string) => Promise<boolean>
|
|
80
80
|
* promptText?: (message: string) => Promise<string>
|
|
81
|
+
* createR2Client?: typeof createR2Client
|
|
81
82
|
* writeStdout?: (message: string) => void
|
|
82
83
|
* }} R2Runtime
|
|
83
84
|
*/
|
|
@@ -285,10 +286,12 @@ export async function copyR2(paths, options = {}, runtime = {}) {
|
|
|
285
286
|
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
286
287
|
|
|
287
288
|
await confirmCopyOverwrite({
|
|
289
|
+
client,
|
|
288
290
|
destination,
|
|
289
291
|
force: options.force === true,
|
|
290
292
|
noClobber: options.noClobber === true,
|
|
291
293
|
runtime,
|
|
294
|
+
sourcePath,
|
|
292
295
|
sourceType,
|
|
293
296
|
targetPath,
|
|
294
297
|
targetType,
|
|
@@ -383,10 +386,12 @@ export async function moveR2(paths, options = {}, runtime = {}) {
|
|
|
383
386
|
(targetType !== "directory" || isDirectoryLikeCopyPath(destinationPath))
|
|
384
387
|
|
|
385
388
|
await confirmCopyOverwrite({
|
|
389
|
+
client,
|
|
386
390
|
destination,
|
|
387
391
|
force: options.force === true,
|
|
388
392
|
noClobber: options.noClobber === true,
|
|
389
393
|
runtime,
|
|
394
|
+
sourcePath,
|
|
390
395
|
sourceType,
|
|
391
396
|
targetPath,
|
|
392
397
|
targetType,
|
|
@@ -426,7 +431,7 @@ export async function moveR2(paths, options = {}, runtime = {}) {
|
|
|
426
431
|
* @param {R2Runtime} runtime
|
|
427
432
|
*/
|
|
428
433
|
function createR2CommandClient(runtime) {
|
|
429
|
-
return createR2Client({
|
|
434
|
+
return (runtime.createR2Client ?? createR2Client)({
|
|
430
435
|
cwd: runtime.cwd ?? processCwd(),
|
|
431
436
|
interactive: getInteractive(runtime),
|
|
432
437
|
promptText: getPromptText(runtime),
|
|
@@ -580,20 +585,24 @@ function getCopyTargetPath({
|
|
|
580
585
|
|
|
581
586
|
/**
|
|
582
587
|
* @param {{
|
|
588
|
+
* client: ReturnType<typeof createR2CommandClient>,
|
|
583
589
|
* destination: string,
|
|
584
590
|
* force: boolean,
|
|
585
591
|
* noClobber: boolean,
|
|
586
592
|
* runtime: R2Runtime,
|
|
593
|
+
* sourcePath: CopyPath,
|
|
587
594
|
* sourceType: CopyPathType,
|
|
588
595
|
* targetPath: CopyPath,
|
|
589
596
|
* targetType: CopyPathType
|
|
590
597
|
* }} args
|
|
591
598
|
*/
|
|
592
599
|
async function confirmCopyOverwrite({
|
|
600
|
+
client,
|
|
593
601
|
destination,
|
|
594
602
|
force,
|
|
595
603
|
noClobber,
|
|
596
604
|
runtime,
|
|
605
|
+
sourcePath,
|
|
597
606
|
sourceType,
|
|
598
607
|
targetPath,
|
|
599
608
|
targetType,
|
|
@@ -617,11 +626,27 @@ async function confirmCopyOverwrite({
|
|
|
617
626
|
if (noClobber) {
|
|
618
627
|
return
|
|
619
628
|
}
|
|
629
|
+
const overwrittenPaths =
|
|
630
|
+
sourceType === "directory"
|
|
631
|
+
? await getDirectoryOverwritePaths({
|
|
632
|
+
client,
|
|
633
|
+
runtime,
|
|
634
|
+
sourcePath,
|
|
635
|
+
targetPath,
|
|
636
|
+
})
|
|
637
|
+
: [formatCopyPath(targetPath)]
|
|
638
|
+
if (overwrittenPaths.length === 0) {
|
|
639
|
+
return
|
|
640
|
+
}
|
|
620
641
|
if (!getInteractive(runtime)) {
|
|
621
642
|
throw new CliError(
|
|
622
643
|
'Transfer would overwrite existing files. Re-run with "--force" in non-interactive environments.',
|
|
623
644
|
)
|
|
624
645
|
}
|
|
646
|
+
log.warn(`Files to overwrite (${overwrittenPaths.length})`)
|
|
647
|
+
for (const overwrittenPath of overwrittenPaths) {
|
|
648
|
+
log.message(`• ${overwrittenPath}`)
|
|
649
|
+
}
|
|
625
650
|
const message =
|
|
626
651
|
sourceType === "directory"
|
|
627
652
|
? `Overwrite any existing files under destination "${destination}"? [y/N] `
|
|
@@ -632,6 +657,94 @@ async function confirmCopyOverwrite({
|
|
|
632
657
|
}
|
|
633
658
|
}
|
|
634
659
|
|
|
660
|
+
/**
|
|
661
|
+
* @param {{
|
|
662
|
+
* client: ReturnType<typeof createR2CommandClient>
|
|
663
|
+
* runtime: R2Runtime
|
|
664
|
+
* sourcePath: CopyPath
|
|
665
|
+
* targetPath: CopyPath
|
|
666
|
+
* }} args
|
|
667
|
+
*/
|
|
668
|
+
async function getDirectoryOverwritePaths({
|
|
669
|
+
client,
|
|
670
|
+
runtime,
|
|
671
|
+
sourcePath,
|
|
672
|
+
targetPath,
|
|
673
|
+
}) {
|
|
674
|
+
const sourceFiles = await listCopyFiles(sourcePath, client, runtime)
|
|
675
|
+
if (sourceFiles.length === 0) {
|
|
676
|
+
return []
|
|
677
|
+
}
|
|
678
|
+
const targetFiles = new Set(await listCopyFiles(targetPath, client, runtime))
|
|
679
|
+
return sourceFiles
|
|
680
|
+
.filter((relativePath) => targetFiles.has(relativePath))
|
|
681
|
+
.map((relativePath) =>
|
|
682
|
+
formatCopyPath({
|
|
683
|
+
...targetPath,
|
|
684
|
+
path: joinCopyPath(targetPath, relativePath),
|
|
685
|
+
}),
|
|
686
|
+
)
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* @param {CopyPath} copyPath
|
|
691
|
+
* @param {ReturnType<typeof createR2CommandClient>} client
|
|
692
|
+
* @param {R2Runtime} runtime
|
|
693
|
+
*/
|
|
694
|
+
async function listCopyFiles(copyPath, client, runtime) {
|
|
695
|
+
if (copyPath.remote) {
|
|
696
|
+
return parseRcloneFileList(await client.list(copyPath.path))
|
|
697
|
+
}
|
|
698
|
+
return listLocalFiles(resolveLocalPath(copyPath.path, runtime))
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* @param {string} root
|
|
703
|
+
* @param {string} [prefix]
|
|
704
|
+
* @returns {Promise<string[]>}
|
|
705
|
+
*/
|
|
706
|
+
async function listLocalFiles(root, prefix = "") {
|
|
707
|
+
const entries = await readdir(root, { withFileTypes: true })
|
|
708
|
+
const files = []
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
const relativePath = prefix
|
|
711
|
+
? joinRelativePath(prefix, entry.name)
|
|
712
|
+
: entry.name
|
|
713
|
+
const absolutePath = join(root, entry.name)
|
|
714
|
+
if (entry.isDirectory()) {
|
|
715
|
+
files.push(...(await listLocalFiles(absolutePath, relativePath)))
|
|
716
|
+
} else if (entry.isFile()) {
|
|
717
|
+
files.push(relativePath)
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return files
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* @param {string} output
|
|
725
|
+
*/
|
|
726
|
+
function parseRcloneFileList(output) {
|
|
727
|
+
return output
|
|
728
|
+
.split(/\r?\n/)
|
|
729
|
+
.map((line) => line.trim())
|
|
730
|
+
.filter(Boolean)
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* @param {string} parent
|
|
735
|
+
* @param {string} child
|
|
736
|
+
*/
|
|
737
|
+
function joinRelativePath(parent, child) {
|
|
738
|
+
return parent ? posix.join(parent, child) : child
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* @param {CopyPath} copyPath
|
|
743
|
+
*/
|
|
744
|
+
function formatCopyPath(copyPath) {
|
|
745
|
+
return copyPath.remote ? `${remotePathPrefix}${copyPath.path}` : copyPath.path
|
|
746
|
+
}
|
|
747
|
+
|
|
635
748
|
/**
|
|
636
749
|
* @param {CopyPath} copyPath
|
|
637
750
|
* @param {ReturnType<typeof createR2CommandClient>} client
|
package/src/support/r2.js
CHANGED
|
@@ -617,9 +617,10 @@ async function promptRequiredSecret({ interactive = true } = {}) {
|
|
|
617
617
|
if (isCancel(value)) {
|
|
618
618
|
cancelPrompt()
|
|
619
619
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
620
|
+
const secret = typeof value === "string" ? value : ""
|
|
621
|
+
if (secret.trim()) {
|
|
622
|
+
processEnv[sessionSecretEnvName] = secret
|
|
623
|
+
return secret
|
|
623
624
|
}
|
|
624
625
|
}
|
|
625
626
|
}
|