create-wxl 3.0.0 → 3.1.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/bin/create-wxl.mjs +2 -0
- package/dist/fork-init.mjs +15 -14
- package/dist/source.json +3 -3
- package/package.json +1 -1
- package/src/archive-rules.mjs +19 -3
- package/src/argv.mjs +59 -2
- package/src/base-version.mjs +10 -0
- package/src/git-named.mjs +43 -0
- package/src/git-runner.mjs +27 -14
- package/src/git-version.mjs +5 -3
- package/src/merge-apply.mjs +156 -0
- package/src/merge-plan.mjs +660 -0
- package/src/messages.mjs +232 -8
- package/src/patterns.mjs +6 -0
- package/src/questions.mjs +3 -3
- package/src/run-shared.mjs +207 -0
- package/src/run.mjs +36 -136
- package/src/text.mjs +47 -0
- package/src/upgrade-report.mjs +134 -0
- package/src/upgrade.mjs +582 -0
package/src/run.mjs
CHANGED
|
@@ -1,43 +1,18 @@
|
|
|
1
|
-
import { dirname,
|
|
1
|
+
import { dirname, resolve as resolvePath } from 'node:path'
|
|
2
2
|
import { createHash } from 'node:crypto'
|
|
3
3
|
import { messages, cwE12FinishingCommandsList } from './messages.mjs'
|
|
4
4
|
import { jsonValue } from './text.mjs'
|
|
5
5
|
import { selectLanguage } from './language.mjs'
|
|
6
|
-
import { parseArchive, ArchiveParseError } from './archive.mjs'
|
|
7
|
-
import { checkEntryRules, ArchiveRuleError } from './archive-rules.mjs'
|
|
8
6
|
import { parseArgvStrict, ArgvError, SETUP_FLAG_NAMES } from './argv.mjs'
|
|
9
|
-
import { checkTarget,
|
|
10
|
-
import { applyChangelog, applyOpenQuestions, applyPackageJson, writeRangeJson, PristineError } from './pristine.mjs'
|
|
7
|
+
import { checkTarget, moveIntoPlace, MoveIntoPlaceError } from './fsops.mjs'
|
|
11
8
|
import { isNodeVersionOk, MINIMUM_NODE_VERSION } from './node-version.mjs'
|
|
12
9
|
import { makeQuestioner, QuestionEofError, buildEngineArgv, missingRequiredFlags, collectInteractiveAnswers } from './questions.mjs'
|
|
13
10
|
import { runGitSteps, orderedPrintedForms } from './git-steps.mjs'
|
|
14
11
|
import { buildNextSteps } from './next-steps.mjs'
|
|
15
12
|
import { createSignalGuard, SignalAbort } from './signals.mjs'
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
/** S-20: the one place every "print a failure line, then remove the temporary directory this run
|
|
21
|
-
* created, then fail" sequence lives (steps 8-10's CW-E09/CW-E10/CW-E04 branches, and the
|
|
22
|
-
* generic bug/signal catch, each used to repeat this pair of calls for itself). */
|
|
23
|
-
async function removeTmpDir(deps, tmpDir) {
|
|
24
|
-
await deps.fs.rm(tmpDir, { recursive: true, force: true })
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function failWithCleanup(io, deps, tmpDir, line) {
|
|
28
|
-
io.stderr.write(line + '\n')
|
|
29
|
-
await removeTmpDir(deps, tmpDir)
|
|
30
|
-
return 1
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** CW-E09 (steps 8-9) vs. CW-E14 (a bug): a real file-system error carries a string `code` that
|
|
34
|
-
* does NOT start with `ERR_` (review finding C-12) -- Node's own argument-validation errors
|
|
35
|
-
* (`ERR_INVALID_ARG_TYPE`, `ERR_INVALID_ARG_VALUE`, `ERR_OUT_OF_RANGE`, ...) also carry a string
|
|
36
|
-
* `code`, but they are a programming bug in this codebase, not an environment problem to report
|
|
37
|
-
* as CW-E09. */
|
|
38
|
-
function isFileSystemError(err) {
|
|
39
|
-
return typeof err?.code === 'string' && !err.code.startsWith('ERR_')
|
|
40
|
-
}
|
|
13
|
+
import { VERSION_RE, COMMIT_RE } from './patterns.mjs'
|
|
14
|
+
import { removeTmpDir, downloadArchive, readArchiveEntries, makeTmpDir, extractInto, applyPristine } from './run-shared.mjs'
|
|
15
|
+
import { runUpgrade } from './upgrade.mjs'
|
|
41
16
|
|
|
42
17
|
/** Step 2's package source record check (D2): `deps.engineFile` is read through `deps.fs` and its
|
|
43
18
|
* SHA-256 compared with `source.engineSha256`. */
|
|
@@ -62,7 +37,8 @@ async function validateSourceRecord(deps) {
|
|
|
62
37
|
/**
|
|
63
38
|
* create-wxl's one exported seam (0005-S1): the Step order table's steps 1-13, every signal
|
|
64
39
|
* window (traceability.md "Signals"), and the full set of failure-path messages (CW-E09,
|
|
65
|
-
* CW-E11, CW-E14, CW-W05).
|
|
40
|
+
* CW-E11, CW-E14, CW-W05). An argv whose first positional is `upgrade` is dispatched to
|
|
41
|
+
* 0006's upgrade path instead (0006-S1, `runUpgrade` in upgrade.mjs).
|
|
66
42
|
*/
|
|
67
43
|
export async function run(argv, io, deps) {
|
|
68
44
|
// traceability.md "Signals": "errors writing the messages are ignored". Without a listener, a
|
|
@@ -143,6 +119,9 @@ async function runBody(argv, io, deps) {
|
|
|
143
119
|
const guard = createSignalGuard(deps.signals, ['SIGINT', 'SIGTERM', 'SIGHUP'])
|
|
144
120
|
const questioner = makeQuestioner(io, deps, guard)
|
|
145
121
|
try {
|
|
122
|
+
if (parsed.mode === 'upgrade') {
|
|
123
|
+
return await runUpgrade(parsed, io, deps, t2, lang2, guard, { hasOverride, overrideRaw })
|
|
124
|
+
}
|
|
146
125
|
return await runSteps(parsed, io, deps, t2, lang2, questioner, guard, { hasOverride, overrideRaw })
|
|
147
126
|
} catch (err) {
|
|
148
127
|
if (err instanceof QuestionEofError) {
|
|
@@ -289,121 +268,42 @@ async function runStepsBody(
|
|
|
289
268
|
if (hasOverride) {
|
|
290
269
|
io.stderr.write(t2['CW-W04']({ commit }) + '\n')
|
|
291
270
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const unsubscribeAbort = guard.onRaise(() => controller.abort())
|
|
299
|
-
let timedOut = false
|
|
300
|
-
let timer
|
|
301
|
-
const timeoutPromise = new Promise((_resolveTimeout, rejectTimeout) => {
|
|
302
|
-
timer = setTimeout(() => {
|
|
303
|
-
timedOut = true
|
|
304
|
-
controller.abort()
|
|
305
|
-
rejectTimeout(new Error())
|
|
306
|
-
}, deps.downloadTimeoutMs)
|
|
307
|
-
})
|
|
308
|
-
|
|
309
|
-
let status, body
|
|
310
|
-
try {
|
|
311
|
-
;({ status, body } = await Promise.race([
|
|
312
|
-
deps.fetchArchive(deps.source.repository, commit, { signal: controller.signal }),
|
|
313
|
-
timeoutPromise,
|
|
314
|
-
]))
|
|
315
|
-
} catch (err) {
|
|
316
|
-
if (guard.raised()) throw new SignalAbort(guard.raised())
|
|
317
|
-
const detail = timedOut ? { kind: 'timeout' } : { kind: 'error', message: err?.message ?? String(err) }
|
|
318
|
-
io.stderr.write(t2['CW-E07']({ url, detail }) + '\n')
|
|
319
|
-
return 1
|
|
320
|
-
} finally {
|
|
321
|
-
clearTimeout(timer)
|
|
322
|
-
// S-17: step 6's own window has closed either way; this `AbortController` is no longer
|
|
323
|
-
// useful to abort, so the callback is removed instead of lingering for the rest of the run.
|
|
324
|
-
unsubscribeAbort()
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
guard.throwIfRaised() // P-17/C-14: a signal already received wins over a non-200 status
|
|
328
|
-
if (status !== 200) {
|
|
329
|
-
io.stderr.write(t2['CW-E07']({ url, detail: { kind: 'status', status } }) + '\n')
|
|
330
|
-
return 1
|
|
331
|
-
}
|
|
271
|
+
// The URL/AbortController/timeout/race/CW-E07 window lives in `run-shared.mjs`'s
|
|
272
|
+
// `downloadArchive`, shared with the upgrade path (traceability.md "Step order" row 7); this
|
|
273
|
+
// call site's own checkpoints and CW-W04 are unchanged.
|
|
274
|
+
const downloaded = await downloadArchive({ io, deps, t2, guard, repository: deps.source.repository, ref: commit })
|
|
275
|
+
if (!downloaded) return 1
|
|
276
|
+
const { body } = downloaded
|
|
332
277
|
|
|
333
278
|
deps.checkpoint('step-7')
|
|
334
279
|
guard.throwIfRaised()
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
io.stderr.write(t2['CW-E08']({ parseError: err.message }) + '\n')
|
|
341
|
-
return 1
|
|
342
|
-
}
|
|
343
|
-
throw err
|
|
344
|
-
}
|
|
345
|
-
let validatedEntries
|
|
346
|
-
try {
|
|
347
|
-
validatedEntries = checkEntryRules(parsedArchive, { commit })
|
|
348
|
-
} catch (err) {
|
|
349
|
-
if (err instanceof ArchiveRuleError) {
|
|
350
|
-
io.stderr.write(t2['CW-E08']({ rule: err.rule, path: err.path, isCommentMarker: err.isCommentMarker }) + '\n')
|
|
351
|
-
return 1
|
|
352
|
-
}
|
|
353
|
-
throw err
|
|
354
|
-
}
|
|
280
|
+
// Parsing, entry-rule validation, mkdtemp, extraction and P1-P4 live in `run-shared.mjs`,
|
|
281
|
+
// shared with the upgrade path; this call site's own checkpoints, `setTmpDir` and argument
|
|
282
|
+
// shape are unchanged.
|
|
283
|
+
const validatedEntries = await readArchiveEntries({ io, t2, body, commit })
|
|
284
|
+
if (!validatedEntries) return 1
|
|
355
285
|
|
|
356
286
|
deps.checkpoint('step-8')
|
|
357
287
|
guard.throwIfRaised()
|
|
358
288
|
const parentDir = dirname(target)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
} catch (err) {
|
|
364
|
-
if (isFileSystemError(err)) {
|
|
365
|
-
io.stderr.write(t2['CW-E09']({ path: parentDir, detail: err.message }) + '\n')
|
|
366
|
-
return 1 // mkdtemp itself failed: nothing was created, nothing to clean up
|
|
367
|
-
}
|
|
368
|
-
throw err
|
|
369
|
-
}
|
|
370
|
-
try {
|
|
371
|
-
// S-19: `{ stopped: true }` (never a silent early return) is the one signal `extractEntries`
|
|
372
|
-
// gives for "a signal arrived mid-extraction"; turned into the same `SignalAbort` every other
|
|
373
|
-
// window throws, right here, instead of a separate check the caller could forget.
|
|
374
|
-
const { stopped } = await extractEntries(deps, validatedEntries, tmpDir, { shouldStop: () => guard.isRaised() })
|
|
375
|
-
if (stopped) throw new SignalAbort(guard.raised())
|
|
376
|
-
} catch (err) {
|
|
377
|
-
if (err instanceof SignalAbort) throw err
|
|
378
|
-
if (isFileSystemError(err)) {
|
|
379
|
-
return failWithCleanup(io, deps, tmpDir, t2['CW-E09']({ path: err.reportPath ?? tmpDir, detail: err.message }))
|
|
380
|
-
}
|
|
381
|
-
throw err
|
|
382
|
-
}
|
|
289
|
+
const tmpDir = await makeTmpDir({ io, deps, t2, parent: parentDir, prefix: '.create-wxl-' })
|
|
290
|
+
if (!tmpDir) return 1 // mkdtemp itself failed: nothing was created, nothing to clean up
|
|
291
|
+
setTmpDir(tmpDir)
|
|
292
|
+
if (!(await extractInto({ io, deps, t2, guard, entries: validatedEntries, tmpDir }))) return 1
|
|
383
293
|
|
|
384
294
|
deps.checkpoint('step-9')
|
|
385
295
|
guard.throwIfRaised()
|
|
386
|
-
|
|
387
|
-
await
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
})
|
|
398
|
-
guard.throwIfRaised() // P-02/C-01: a signal during step 9's last write starts no step-10 work
|
|
399
|
-
} catch (err) {
|
|
400
|
-
if (err instanceof PristineError) {
|
|
401
|
-
return failWithCleanup(io, deps, tmpDir, t2['CW-E10']({ file: err.file, rule: err.rule }))
|
|
402
|
-
}
|
|
403
|
-
if (isFileSystemError(err)) {
|
|
404
|
-
return failWithCleanup(io, deps, tmpDir, t2['CW-E09']({ path: err.reportPath ?? tmpDir, detail: err.message }))
|
|
405
|
-
}
|
|
406
|
-
throw err
|
|
296
|
+
if (
|
|
297
|
+
!(await applyPristine({
|
|
298
|
+
io,
|
|
299
|
+
deps,
|
|
300
|
+
t2,
|
|
301
|
+
guard,
|
|
302
|
+
tmpDir,
|
|
303
|
+
record: { repository: deps.source.repository, version: deps.source.version, commit },
|
|
304
|
+
}))
|
|
305
|
+
) {
|
|
306
|
+
return 1
|
|
407
307
|
}
|
|
408
308
|
|
|
409
309
|
deps.checkpoint('step-10')
|
package/src/text.mjs
CHANGED
|
@@ -45,6 +45,38 @@ export function jsonValue(value) {
|
|
|
45
45
|
return out + '"'
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Candidate 22 (stage 7, security-T-0026): prints `value` through `JSON.stringify` -- unlike
|
|
49
|
+
* `jsonValue`, without wrapping the result in its own extra quotes, so an array or object prints
|
|
50
|
+
* as itself (`["3.1.0"]`, `{"toString":1}`), not through `String(value)` (which calls `toString`,
|
|
51
|
+
* throwing for a value whose own `toString` property is not callable, and collapsing an array
|
|
52
|
+
* like `["3.1.0"]` into the indistinguishable-from-a-real-version `"3.1.0"`). Any DEL/C1
|
|
53
|
+
* character or lone surrogate `JSON.stringify`'s own algorithm leaves unescaped (it already
|
|
54
|
+
* escapes every C0 character and lone surrogate itself, ES2019) is escaped the same way
|
|
55
|
+
* `escapeChar` would (D10). Returns `undefined` when `JSON.stringify` throws (a `BigInt`, a
|
|
56
|
+
* circular structure) or itself returns `undefined` (a bare `function` or `undefined` value) --
|
|
57
|
+
* the caller falls back to a type name in that case. */
|
|
58
|
+
const SHORT_ESCAPE_CODES = { b: '0008', f: '000c', r: '000d', t: '0009' }
|
|
59
|
+
|
|
60
|
+
export function jsonStringifyEscaped(value) {
|
|
61
|
+
let str
|
|
62
|
+
try {
|
|
63
|
+
str = JSON.stringify(value)
|
|
64
|
+
} catch {
|
|
65
|
+
return undefined
|
|
66
|
+
}
|
|
67
|
+
if (typeof str !== 'string') return undefined
|
|
68
|
+
// D10: JSON.stringify's own short escapes for backspace, form feed, CR and tab become `\uXXXX`
|
|
69
|
+
// (a line feed keeps `\n`); matching escape pairs left to right leaves `\\` followed by a
|
|
70
|
+
// letter untouched.
|
|
71
|
+
str = str.replace(/\\(.)/g, (pair, c) => (c in SHORT_ESCAPE_CODES ? `\\u${SHORT_ESCAPE_CODES[c]}` : pair))
|
|
72
|
+
let out = ''
|
|
73
|
+
for (const ch of str) {
|
|
74
|
+
const code = ch.codePointAt(0)
|
|
75
|
+
out += isControlCodePoint(code) || isLoneSurrogate(code) ? `\\u${code.toString(16).padStart(4, '0')}` : ch
|
|
76
|
+
}
|
|
77
|
+
return out
|
|
78
|
+
}
|
|
79
|
+
|
|
48
80
|
const BARE_RE = /^[A-Za-z0-9._\-/@%+=:,]+$/
|
|
49
81
|
|
|
50
82
|
export function posixQuoteWord(word) {
|
|
@@ -78,3 +110,18 @@ export function hasControlChar(str) {
|
|
|
78
110
|
}
|
|
79
111
|
return false
|
|
80
112
|
}
|
|
113
|
+
|
|
114
|
+
/** `\` is Windows' own path separator; comparing after turning `\` into `/` (traceability.md
|
|
115
|
+
* "Step order" row 3, Vocabulary "name on disk") makes a path comparison platform-neutral. Shared
|
|
116
|
+
* by step 3 and P-9 since T-0062. */
|
|
117
|
+
export function toSlash(value) {
|
|
118
|
+
return String(value).replace(/\\/g, '/')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** ASCII-only case folding (`[A-Z]` to lowercase), the way git's own `tolower` and J5's repository
|
|
122
|
+
* comparison fold -- never JavaScript's Unicode-aware lowercasing. Shared by J5 and entry rule 3
|
|
123
|
+
* since T-0062. */
|
|
124
|
+
export function foldAsciiCase(value) {
|
|
125
|
+
return value.replace(/[A-Z]/g, (c) => c.toLowerCase())
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Step 10 (traceability.md "Step order" row 10, "Report"): prints R1-R5, one per line, each
|
|
2
|
+
// starting with its own id, in the run's language (`t2`). R3 renders each attention item through the
|
|
3
|
+
// language table's own UP-A* text. R2 lists every path M6 deleted after the counts (D16). R4
|
|
4
|
+
// (traceability.md "Report" R4) is a fixed body of next-steps advice plus three conditional bullets:
|
|
5
|
+
// D16's `|||||||` line only when R3 holds UP-A1, `pnpm install` only when the run wrote
|
|
6
|
+
// `package.json` or `pnpm-lock.yaml`, and the deploy-tag push only for a range with
|
|
7
|
+
// `.github/workflows/deploy.yml`.
|
|
8
|
+
|
|
9
|
+
import { jsonValue, posixCommand } from './text.mjs'
|
|
10
|
+
|
|
11
|
+
/** D14 (T-0063): the restore commands, in order -- `checkout` first, so a missing or changed
|
|
12
|
+
* ignore file is back before `clean` runs, and `clean` never deletes a file `HEAD`'s rules ignore.
|
|
13
|
+
* R5 and UP-E09 print them, and U70 runs them, from this one list. */
|
|
14
|
+
export const RESTORE_COMMANDS = Object.freeze([
|
|
15
|
+
Object.freeze(['git', 'checkout', '--', '.']),
|
|
16
|
+
Object.freeze(['git', 'clean', '-fd']),
|
|
17
|
+
])
|
|
18
|
+
|
|
19
|
+
/** D23 (T-0066): the restore commands as the one line R5 and UP-E09 print, joined with `&&`, so a
|
|
20
|
+
* `git checkout -- .` that fails never lets `git clean -fd` run (a shell without `&&`, such as
|
|
21
|
+
* Windows PowerShell 5.1, refuses the whole line, and nothing runs). */
|
|
22
|
+
export function restoreCommandLine() {
|
|
23
|
+
return RESTORE_COMMANDS.map((command) => posixCommand(command)).join(' && ')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `ops` carrying a `report` code (M10, or an M12 conflict/UP-A11 outcome) are written, but not
|
|
27
|
+
* counted here (traceability.md "Report" R2's own "M10 is not counted as updated"). */
|
|
28
|
+
function countsFromOps(ops) {
|
|
29
|
+
const counts = { updated: 0, added: 0, deleted: 0 }
|
|
30
|
+
for (const op of ops) {
|
|
31
|
+
if (op.report !== undefined) continue
|
|
32
|
+
if (op.action === 'delete') counts.deleted++
|
|
33
|
+
else if (op.action === 'write') counts[op.existedBefore ? 'updated' : 'added']++
|
|
34
|
+
}
|
|
35
|
+
return counts
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** UP-A12 sorts last among attention lines (traceability.md "Report" R3); every other code sorts
|
|
39
|
+
* by path. */
|
|
40
|
+
function sortAttention(attention) {
|
|
41
|
+
const rest = attention.filter((item) => item.code !== 'UP-A12').sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0))
|
|
42
|
+
const anchor = attention.filter((item) => item.code === 'UP-A12')
|
|
43
|
+
return [...rest, ...anchor]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** T-0027 increment 21 (U26), traceability.md "Report" R2's own "M10 is not counted as updated":
|
|
47
|
+
* unlike `countsFromOps`, above, this does not look at `op.report` at all -- a written
|
|
48
|
+
* `package.json`/`pnpm-lock.yaml` still needs `pnpm install` advice whether or not it was an M10
|
|
49
|
+
* lockfile-conflict write or a plain M5/M12 write. */
|
|
50
|
+
function wroteDependencyFile(ops) {
|
|
51
|
+
return ops.some((op) => op.action === 'write' && (op.path === 'package.json' || op.path === 'pnpm-lock.yaml'))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** R4 (traceability.md "Report"): a fixed body of next-steps advice, plus three conditional
|
|
55
|
+
* bullets (D16's `|||||||` line, `pnpm install`, the `v*` tag), each its own `R4:` line (mirrors
|
|
56
|
+
* R3's own one-line-per-item convention). */
|
|
57
|
+
function buildR4Lines(lang2, { ops, attention, hasDeployWorkflow }) {
|
|
58
|
+
const zh = lang2 === 'zh-TW'
|
|
59
|
+
const lines = [
|
|
60
|
+
zh ? 'R4: 執行 git status 與 git diff' : 'R4: run git status and git diff',
|
|
61
|
+
// Named generically here (never the literal code "UP-A1"), so this fixed bullet never
|
|
62
|
+
// collides with a path-specific assertion elsewhere checking a particular attention code's
|
|
63
|
+
// ABSENCE from the whole run's stdout (e.g. U56, increment 17's own `not.toContain('UP-A1')`).
|
|
64
|
+
zh
|
|
65
|
+
? 'R4: 處理上面的衝突標記(搜尋 <<<<<<<)與其他注意項目'
|
|
66
|
+
: 'R4: resolve the conflict markers above (search for <<<<<<<) and the other attention items',
|
|
67
|
+
]
|
|
68
|
+
// D16 (T-0063): when R3 holds UP-A1, say whose text the `|||||||` section of a conflict block is.
|
|
69
|
+
// Like the bullet above, it never names the code itself.
|
|
70
|
+
if (attention.some((item) => item.code === 'UP-A1')) {
|
|
71
|
+
lines.push(
|
|
72
|
+
zh
|
|
73
|
+
? 'R4: 衝突區塊裡 ||||||| 之後的段落是 R1 所列 base 版本的內容;如果這個 base 不是靶場原本所依據的版本,請先執行 R5 的指令'
|
|
74
|
+
: 'R4: in a conflict block, the section after ||||||| is the text of the base version in R1; ' +
|
|
75
|
+
'if that base is not the version the range came from, run the R5 commands first',
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
if (wroteDependencyFile(ops)) lines.push(zh ? 'R4: 執行 pnpm install' : 'R4: run pnpm install')
|
|
79
|
+
lines.push(zh ? 'R4: build 並測試' : 'R4: build and test')
|
|
80
|
+
lines.push('R4: commit')
|
|
81
|
+
if (hasDeployWorkflow) lines.push(zh ? 'R4: 推送新的 v* tag 以觸發部署' : 'R4: push a new v* tag to deploy')
|
|
82
|
+
return lines
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function buildReport({ lang2, toolVersion, baseVersion, baseCommit, targetVersion, targetCommit, ops, attention, hasDeployWorkflow, t2, baseUnchecked = false }) {
|
|
86
|
+
const zh = lang2 === 'zh-TW'
|
|
87
|
+
const counts = countsFromOps(ops)
|
|
88
|
+
const lines = []
|
|
89
|
+
// D17 (traceability.md "Report" R1; T-0061): a `--from <commit>` base is whatever that commit
|
|
90
|
+
// holds -- R1 says it was not checked against an upstream tag.
|
|
91
|
+
const uncheckedNote = baseUnchecked
|
|
92
|
+
? zh
|
|
93
|
+
? ';base 由 --from <commit> 指定,沒有比對上游的 tag'
|
|
94
|
+
: '; the base was given by --from <commit> and not checked against an upstream tag'
|
|
95
|
+
: ''
|
|
96
|
+
lines.push(
|
|
97
|
+
zh
|
|
98
|
+
? `R1: create-wxl ${jsonValue(toolVersion)};base 版本 ${jsonValue(baseVersion)}(commit ${jsonValue(baseCommit)});` +
|
|
99
|
+
`target 版本 ${jsonValue(targetVersion)}(commit ${jsonValue(targetCommit)})` +
|
|
100
|
+
uncheckedNote
|
|
101
|
+
// Names the commit the same way the zh-TW text above already does.
|
|
102
|
+
: `R1: create-wxl ${jsonValue(toolVersion)}; base ${jsonValue(baseVersion)} (commit ${jsonValue(baseCommit)}); ` +
|
|
103
|
+
`target ${jsonValue(targetVersion)} (commit ${jsonValue(targetCommit)})` +
|
|
104
|
+
uncheckedNote,
|
|
105
|
+
)
|
|
106
|
+
lines.push(
|
|
107
|
+
zh
|
|
108
|
+
? `R2: 更新 ${counts.updated} 新增 ${counts.added} 刪除 ${counts.deleted}`
|
|
109
|
+
: `R2: updated ${counts.updated} added ${counts.added} deleted ${counts.deleted}`,
|
|
110
|
+
)
|
|
111
|
+
// D16 (T-0063): then every path M6 deleted (the only deleting row), sorted by UTF-16 code units,
|
|
112
|
+
// without a code.
|
|
113
|
+
const deletedPaths = ops.filter((op) => op.action === 'delete').map((op) => op.path)
|
|
114
|
+
for (const path of deletedPaths.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))) lines.push(`R2: ${jsonValue(path)}`)
|
|
115
|
+
// T-0027 verification follow-up (item 1/2): each attention line is rendered through `t2`
|
|
116
|
+
// (traceability.md:206 "in the run's language"), the same en/zh-TW UP-A* message table every
|
|
117
|
+
// other code already uses -- UP-A12 (T-0027 increment 20, U59) is not tied to any one path
|
|
118
|
+
// (traceability.md "Fork:init-derived content"), so it prints without one, unlike every other
|
|
119
|
+
// UP-A* line.
|
|
120
|
+
for (const item of sortAttention(attention)) {
|
|
121
|
+
lines.push(item.code === 'UP-A12' ? `R3: ${t2[item.code]()}` : `R3: ${t2[item.code]()} ${jsonValue(item.path)}`)
|
|
122
|
+
}
|
|
123
|
+
lines.push(...buildR4Lines(lang2, { ops, attention, hasDeployWorkflow }))
|
|
124
|
+
// traceability.md "Report" R5; spec.en.md B4: the restore commands undo
|
|
125
|
+
// the WHOLE upgrade, so they are only safe BEFORE any other change -- the fixed bare commands
|
|
126
|
+
// alone, without that warning, let a teacher who edits first lose that edit too.
|
|
127
|
+
// The same one line UP-E09 prints (messages.mjs, D23).
|
|
128
|
+
lines.push(
|
|
129
|
+
zh
|
|
130
|
+
? `R5: 如果要撤銷整個升級,請在做其他變更之前執行 ${restoreCommandLine()}`
|
|
131
|
+
: `R5: to undo the whole upgrade before making other changes, run ${restoreCommandLine()}`,
|
|
132
|
+
)
|
|
133
|
+
return lines.join('\n') + '\n'
|
|
134
|
+
}
|