@voxgig/sdkgen 4.33.0 → 4.34.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/voxgig-sdkgen +1 -1
- package/dist/action/copies.d.ts +21 -0
- package/dist/action/copies.js +146 -0
- package/dist/action/copies.js.map +1 -0
- package/dist/action/doctor.d.ts +15 -2
- package/dist/action/doctor.js +169 -23
- package/dist/action/doctor.js.map +1 -1
- package/dist/action/edition.js +6 -2
- package/dist/action/edition.js.map +1 -1
- package/dist/action/feature.js +6 -2
- package/dist/action/feature.js.map +1 -1
- package/dist/action/package.js +28 -10
- package/dist/action/package.js.map +1 -1
- package/dist/action/remove.js +2 -0
- package/dist/action/remove.js.map +1 -1
- package/dist/action/resolve.d.ts +1 -0
- package/dist/action/resolve.js +10 -2
- package/dist/action/resolve.js.map +1 -1
- package/dist/action/target.js +6 -2
- package/dist/action/target.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -1
- package/dist/sdkgen.js +36 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/sdkgen-package.json +1 -1
- package/src/action/copies.ts +219 -0
- package/src/action/doctor.ts +247 -21
- package/src/action/edition.ts +10 -2
- package/src/action/feature.ts +11 -2
- package/src/action/package.ts +31 -10
- package/src/action/remove.ts +4 -0
- package/src/action/resolve.ts +16 -3
- package/src/action/target.ts +10 -2
- package/src/sdkgen.ts +45 -5
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import Path from 'node:path'
|
|
2
|
+
|
|
3
|
+
import { createHash } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
import type { ActionContext } from '../types'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
definitionFileName, definitionFolder, indexName,
|
|
9
|
+
} from '../helpers/definition'
|
|
10
|
+
|
|
11
|
+
import { KINDS } from './kind'
|
|
12
|
+
|
|
13
|
+
import type { Source } from './resolve'
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
// What every add last wrote into `.sdk`, by fingerprint, and the version of
|
|
17
|
+
// the package each item came from. It is the only record that tells a copy
|
|
18
|
+
// the project changed from one its source has since moved past.
|
|
19
|
+
const COPIES = 'sdkgen-copies.json'
|
|
20
|
+
|
|
21
|
+
const ABOUT = 'Written by voxgig-sdkgen on every add: what it copied into ' +
|
|
22
|
+
'.sdk, so that doctor, generate and package update can tell an outdated ' +
|
|
23
|
+
'copy from a local edit. Commit it; do not edit it.'
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
type CopyItem = {
|
|
27
|
+
package?: string
|
|
28
|
+
version?: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type CopyRecord = {
|
|
32
|
+
items: Record<string, CopyItem>
|
|
33
|
+
files: Record<string, string>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
function copiesPath(folder: string): string {
|
|
38
|
+
return Path.join(folder, COPIES)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
function itemKey(kind: string, name: string): string {
|
|
43
|
+
return kind + '/' + name
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
// CRLF and LF copies of one file share a fingerprint, as doctor compares
|
|
48
|
+
// them. latin1 maps each byte to one character, so nothing else changes.
|
|
49
|
+
function fingerprint(content: any): string {
|
|
50
|
+
const bytes = Buffer.isBuffer(content) ? content : Buffer.from(String(content))
|
|
51
|
+
const lf = Buffer.from(bytes.toString('latin1').replace(/\r\n/g, '\n'), 'latin1')
|
|
52
|
+
return createHash('sha256').update(lf).digest('hex').slice(0, 16)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// An unreadable record is treated as absent: every copy is then unrecorded,
|
|
57
|
+
// which is exactly the state of a project from before the record existed.
|
|
58
|
+
function readCopies(fs: any, folder: string): CopyRecord {
|
|
59
|
+
const empty: CopyRecord = { items: {}, files: {} }
|
|
60
|
+
const path = copiesPath(folder)
|
|
61
|
+
|
|
62
|
+
if (!fs.existsSync(path)) {
|
|
63
|
+
return empty
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const read = JSON.parse(String(fs.readFileSync(path, 'utf8')))
|
|
68
|
+
return {
|
|
69
|
+
items: { ...(read?.items ?? {}) },
|
|
70
|
+
files: { ...(read?.files ?? {}) },
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err: any) {
|
|
74
|
+
return empty
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
function writeCopies(fs: any, folder: string, record: CopyRecord) {
|
|
80
|
+
const sorted = (obj: Record<string, any>) => Object.fromEntries(
|
|
81
|
+
Object.keys(obj).sort().map((key: string) => [key, obj[key]]))
|
|
82
|
+
|
|
83
|
+
fs.writeFileSync(copiesPath(folder), JSON.stringify({
|
|
84
|
+
about: ABOUT,
|
|
85
|
+
items: sorted(record.items),
|
|
86
|
+
files: sorted(record.files),
|
|
87
|
+
}, null, 2) + '\n')
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
function provenanceOf(source: Source): CopyItem {
|
|
92
|
+
return {
|
|
93
|
+
...(null == source.package ? {} : { package: source.package }),
|
|
94
|
+
...(null == source.version ? {} : { version: source.version }),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
// The trees and definition files an add owns. An index is excluded: every
|
|
100
|
+
// add rewrites it and no source holds a copy to compare it with.
|
|
101
|
+
const TREE_ROOTS = Array.from(new Set(Object.values(KINDS)
|
|
102
|
+
.flatMap((kind: any) => (kind.trees ?? [])
|
|
103
|
+
.map((tree: any) => String(tree.path).split('{name}')[0]))))
|
|
104
|
+
|
|
105
|
+
function isCopy(rel: string): boolean {
|
|
106
|
+
if (TREE_ROOTS.some((root: string) => rel.startsWith(root))) {
|
|
107
|
+
return true
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const dir = Path.posix.dirname(rel)
|
|
111
|
+
const base = Path.posix.basename(rel)
|
|
112
|
+
|
|
113
|
+
return Object.keys(KINDS).some((kind: string) =>
|
|
114
|
+
dir === definitionFolder('', kind).split(Path.sep).join('/') &&
|
|
115
|
+
base !== indexName(kind) &&
|
|
116
|
+
base.endsWith(definitionFileName('')))
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
function relativeTo(folder: string, abs: string): string | undefined {
|
|
121
|
+
const rel = Path.relative(Path.resolve(folder), Path.resolve(abs))
|
|
122
|
+
.split(Path.sep).join('/')
|
|
123
|
+
|
|
124
|
+
return ('' === rel || rel.startsWith('../') || Path.isAbsolute(rel)) ?
|
|
125
|
+
undefined : rel
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
// Called by each add with what its own jostraca run wrote. A dry run writes
|
|
130
|
+
// nothing, so it records nothing.
|
|
131
|
+
function recordCopies(
|
|
132
|
+
actx: ActionContext, jres: any, kind: string, sources: Source[],
|
|
133
|
+
) {
|
|
134
|
+
if (true === actx.opts?.dryrun || null == jres?.files) {
|
|
135
|
+
return
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const fs = actx.fs()
|
|
139
|
+
const folder = actx.folder
|
|
140
|
+
const record = readCopies(fs, folder)
|
|
141
|
+
|
|
142
|
+
const files: string[] = [
|
|
143
|
+
...(jres.files.written ?? []), ...(jres.files.unchanged ?? []),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
for (const abs of files) {
|
|
147
|
+
const rel = relativeTo(folder, abs)
|
|
148
|
+
|
|
149
|
+
if (null != rel && isCopy(rel) && fs.existsSync(abs)) {
|
|
150
|
+
record.files[rel] = fingerprint(fs.readFileSync(abs))
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
for (const source of sources) {
|
|
155
|
+
record.items[itemKey(kind, source.name)] = provenanceOf(source)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pruneMissing(fs, folder, record)
|
|
159
|
+
writeCopies(fs, folder, record)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
// After a remove: the item goes, with every file entry nothing holds now.
|
|
164
|
+
function forgetCopies(actx: ActionContext, kind: string, name: string) {
|
|
165
|
+
const fs = actx.fs()
|
|
166
|
+
const folder = actx.folder
|
|
167
|
+
|
|
168
|
+
if (true === actx.opts?.dryrun || !fs.existsSync(copiesPath(folder))) {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const record = readCopies(fs, folder)
|
|
173
|
+
delete record.items[itemKey(kind, name)]
|
|
174
|
+
pruneMissing(fs, folder, record)
|
|
175
|
+
writeCopies(fs, folder, record)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
function pruneMissing(fs: any, folder: string, record: CopyRecord) {
|
|
180
|
+
for (const rel of Object.keys(record.files)) {
|
|
181
|
+
if (!fs.existsSync(Path.join(folder, ...rel.split('/')))) {
|
|
182
|
+
delete record.files[rel]
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
// Was this project file left exactly as an add wrote it?
|
|
189
|
+
function untouched(
|
|
190
|
+
fs: any, folder: string, record: CopyRecord, rel: string,
|
|
191
|
+
): boolean | undefined {
|
|
192
|
+
const recorded = record.files[rel]
|
|
193
|
+
|
|
194
|
+
if (null == recorded) {
|
|
195
|
+
return undefined
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const abs = Path.join(folder, ...rel.split('/'))
|
|
199
|
+
|
|
200
|
+
return fs.existsSync(abs) && recorded === fingerprint(fs.readFileSync(abs))
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
export type {
|
|
205
|
+
CopyItem,
|
|
206
|
+
CopyRecord,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export {
|
|
210
|
+
COPIES,
|
|
211
|
+
copiesPath,
|
|
212
|
+
itemKey,
|
|
213
|
+
fingerprint,
|
|
214
|
+
readCopies,
|
|
215
|
+
recordCopies,
|
|
216
|
+
forgetCopies,
|
|
217
|
+
untouched,
|
|
218
|
+
isCopy,
|
|
219
|
+
}
|
package/src/action/doctor.ts
CHANGED
|
@@ -37,6 +37,9 @@ import { findFeatureSources } from '../helpers/featureSource'
|
|
|
37
37
|
|
|
38
38
|
import { isJunk } from '../helpers/junk'
|
|
39
39
|
|
|
40
|
+
import { readCopies, untouched, itemKey } from './copies'
|
|
41
|
+
import type { CopyRecord } from './copies'
|
|
42
|
+
|
|
40
43
|
|
|
41
44
|
const IGNORED_RE = /(~|-jostraca-off)$/
|
|
42
45
|
|
|
@@ -97,10 +100,41 @@ type DoctorReport = {
|
|
|
97
100
|
// it is what an alias is FOR.
|
|
98
101
|
aliasedDiff: string[]
|
|
99
102
|
|
|
103
|
+
// Differs from its source, but the copy record shows it untouched since an
|
|
104
|
+
// add wrote it: the source moved on, so refreshing it discards nothing.
|
|
105
|
+
outdated: string[]
|
|
106
|
+
|
|
107
|
+
// The forked and edited findings the copy record has no entry for, so
|
|
108
|
+
// nothing shows whether the project changed them.
|
|
109
|
+
unrecorded: string[]
|
|
110
|
+
|
|
111
|
+
// Per item with findings, keyed `<kind>/<name>`.
|
|
112
|
+
byItem: Record<string, ItemDrift>
|
|
113
|
+
|
|
100
114
|
ok: boolean
|
|
101
115
|
}
|
|
102
116
|
|
|
103
117
|
|
|
118
|
+
type ItemDrift = {
|
|
119
|
+
// The package the item resolves to now, which is what refreshes it.
|
|
120
|
+
package?: string
|
|
121
|
+
|
|
122
|
+
// The version its copies came from, and the version installed now.
|
|
123
|
+
from?: string
|
|
124
|
+
to?: string
|
|
125
|
+
|
|
126
|
+
outdated: number
|
|
127
|
+
|
|
128
|
+
// Forked, edited, missing and stale files.
|
|
129
|
+
changed: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
type Category = 'forked' | 'edited' | 'missing' | 'stale'
|
|
134
|
+
|
|
135
|
+
type Found = (category: Category, label: string) => void
|
|
136
|
+
|
|
137
|
+
|
|
104
138
|
const CMD_MAP: any = {
|
|
105
139
|
check: cmd_doctor_check,
|
|
106
140
|
prune: cmd_doctor_prune,
|
|
@@ -243,9 +277,12 @@ async function doctor(
|
|
|
243
277
|
const report: DoctorReport = {
|
|
244
278
|
forked: [], edited: [], stale: [], missing: [], additive: [],
|
|
245
279
|
superseded: [], unwired: [], orphanModel: [],
|
|
246
|
-
resyncPending: [], aliasedDiff: [],
|
|
280
|
+
resyncPending: [], aliasedDiff: [],
|
|
281
|
+
outdated: [], unrecorded: [], byItem: {}, ok: true,
|
|
247
282
|
}
|
|
248
283
|
|
|
284
|
+
const copies = readCopies(fs, root)
|
|
285
|
+
|
|
249
286
|
report.superseded = supersededFiles(actx)
|
|
250
287
|
|
|
251
288
|
report.orphanModel = orphanModelFiles(actx)
|
|
@@ -285,12 +322,14 @@ async function doctor(
|
|
|
285
322
|
continue
|
|
286
323
|
}
|
|
287
324
|
|
|
325
|
+
const found = noteDrift(actx, report, copies, kind, source)
|
|
326
|
+
|
|
288
327
|
if ('target' === kind) {
|
|
289
|
-
checkTarget(actx, source, report, selected)
|
|
328
|
+
checkTarget(actx, source, report, found, selected)
|
|
290
329
|
}
|
|
291
330
|
|
|
292
331
|
if ('edition' === kind) {
|
|
293
|
-
checkEdition(actx, source, report)
|
|
332
|
+
checkEdition(actx, source, report, found)
|
|
294
333
|
}
|
|
295
334
|
|
|
296
335
|
// Only an ACTIVE feature has source copied out; an inactive one's
|
|
@@ -298,10 +337,10 @@ async function doctor(
|
|
|
298
337
|
if ('feature' === kind &&
|
|
299
338
|
(false !== (model as any)?.main?.[KIT]?.feature?.[name]?.active ||
|
|
300
339
|
true === selected?.includes(name))) {
|
|
301
|
-
checkFeatureSource(actx, source, targets,
|
|
340
|
+
checkFeatureSource(actx, source, targets, found)
|
|
302
341
|
}
|
|
303
342
|
|
|
304
|
-
checkItemModel(actx, kind, source, report)
|
|
343
|
+
checkItemModel(actx, kind, source, report, found)
|
|
305
344
|
}
|
|
306
345
|
}
|
|
307
346
|
|
|
@@ -316,7 +355,7 @@ async function doctor(
|
|
|
316
355
|
// authoritative either way.
|
|
317
356
|
report.ok = 0 === report.forked.length + report.edited.length +
|
|
318
357
|
report.stale.length + report.missing.length + report.superseded.length +
|
|
319
|
-
report.orphanModel.length
|
|
358
|
+
report.orphanModel.length + report.outdated.length
|
|
320
359
|
|
|
321
360
|
for (const [kind, note] of [
|
|
322
361
|
['forked', 'FORKED (will be reverted by `target add`)'],
|
|
@@ -329,12 +368,24 @@ async function doctor(
|
|
|
329
368
|
['orphanModel', 'ORPHAN MODEL FILE (on disk, included by nothing, read by nobody)'],
|
|
330
369
|
['resyncPending', 'RESYNC PENDING (predates provenance; `target add` updates it)'],
|
|
331
370
|
['aliasedDiff', 'aliased model differs from its origin (project-owned, not drift)'],
|
|
371
|
+
['outdated', 'OUTDATED (unchanged since it was copied, but its source has moved on)'],
|
|
332
372
|
] as [keyof DoctorReport, string][]) {
|
|
333
373
|
for (const file of (report[kind] as string[])) {
|
|
334
374
|
log.info({ point: 'doctor-finding', kind, file, note: note + ': ' + file })
|
|
335
375
|
}
|
|
336
376
|
}
|
|
337
377
|
|
|
378
|
+
for (const item of Object.keys(report.byItem).sort()) {
|
|
379
|
+
const drift = report.byItem[item]
|
|
380
|
+
if (0 < drift.outdated) {
|
|
381
|
+
log.info({
|
|
382
|
+
point: 'doctor-outdated', item, ...drift,
|
|
383
|
+
note: outdatedNote(item, drift) + ' - `voxgig-sdkgen ' +
|
|
384
|
+
refreshCommand(item, drift.package) + '` refreshes them'
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
338
389
|
log.info({
|
|
339
390
|
point: 'doctor-end',
|
|
340
391
|
ok: report.ok,
|
|
@@ -348,11 +399,13 @@ async function doctor(
|
|
|
348
399
|
orphanModel: report.orphanModel.length,
|
|
349
400
|
resyncPending: report.resyncPending.length,
|
|
350
401
|
aliasedDiff: report.aliasedDiff.length,
|
|
402
|
+
outdated: report.outdated.length,
|
|
351
403
|
note: report.ok ?
|
|
352
404
|
('.sdk matches the scaffold (' + report.additive.length + ' additive)') :
|
|
353
405
|
('.sdk has drifted: ' + report.forked.length + ' forked, ' +
|
|
354
406
|
report.edited.length + ' edited, ' + report.stale.length + ' stale, ' +
|
|
355
|
-
report.missing.length + ' missing'
|
|
407
|
+
report.missing.length + ' missing, ' + report.outdated.length +
|
|
408
|
+
' outdated')
|
|
356
409
|
})
|
|
357
410
|
|
|
358
411
|
return { report }
|
|
@@ -412,7 +465,7 @@ function resolveDeclared(
|
|
|
412
465
|
|
|
413
466
|
|
|
414
467
|
function checkTarget(
|
|
415
|
-
actx: ActionContext, resolved: Source, report: DoctorReport,
|
|
468
|
+
actx: ActionContext, resolved: Source, report: DoctorReport, found: Found,
|
|
416
469
|
selected?: string[],
|
|
417
470
|
) {
|
|
418
471
|
const tname = resolved.name
|
|
@@ -466,7 +519,7 @@ function checkTarget(
|
|
|
466
519
|
{ log: quietLog(actx.log), fs: () => fs, folder: root, model },
|
|
467
520
|
tfolder, torigname, tname, features)
|
|
468
521
|
|
|
469
|
-
compareTrees(actx, report, trees, {
|
|
522
|
+
compareTrees(actx, report, found, trees, {
|
|
470
523
|
excludes,
|
|
471
524
|
foreign: (kind: string) => 'edited' === kind ?
|
|
472
525
|
foreignFeatureSource(actx, resolved) : [],
|
|
@@ -477,6 +530,7 @@ function checkTarget(
|
|
|
477
530
|
function compareTrees(
|
|
478
531
|
actx: ActionContext,
|
|
479
532
|
report: DoctorReport,
|
|
533
|
+
found: Found,
|
|
480
534
|
trees: TreeCompare[],
|
|
481
535
|
opts?: {
|
|
482
536
|
excludes?: RegExp[],
|
|
@@ -518,7 +572,7 @@ function compareTrees(
|
|
|
518
572
|
|
|
519
573
|
for (const rel of expected) {
|
|
520
574
|
if (!actualSet.has(rel)) {
|
|
521
|
-
|
|
575
|
+
found('missing', label + rel)
|
|
522
576
|
continue
|
|
523
577
|
}
|
|
524
578
|
|
|
@@ -535,7 +589,7 @@ function compareTrees(
|
|
|
535
589
|
|
|
536
590
|
if (differs(fs, from, Path.join(tree.project, rel),
|
|
537
591
|
model, tree.replace, undefined, tree.rewrite)) {
|
|
538
|
-
|
|
592
|
+
found(tree.kind, label + rel)
|
|
539
593
|
}
|
|
540
594
|
}
|
|
541
595
|
|
|
@@ -553,7 +607,7 @@ function compareTrees(
|
|
|
553
607
|
report.additive.push(label + rel)
|
|
554
608
|
}
|
|
555
609
|
else {
|
|
556
|
-
|
|
610
|
+
found('stale', label + rel)
|
|
557
611
|
}
|
|
558
612
|
}
|
|
559
613
|
}
|
|
@@ -561,7 +615,7 @@ function compareTrees(
|
|
|
561
615
|
|
|
562
616
|
|
|
563
617
|
function checkEdition(
|
|
564
|
-
actx: ActionContext, resolved: Source, report: DoctorReport,
|
|
618
|
+
actx: ActionContext, resolved: Source, report: DoctorReport, found: Found,
|
|
565
619
|
) {
|
|
566
620
|
const fs = actx.fs()
|
|
567
621
|
const root = actx.folder
|
|
@@ -593,7 +647,7 @@ function checkEdition(
|
|
|
593
647
|
} as TreeCompare]
|
|
594
648
|
})
|
|
595
649
|
|
|
596
|
-
compareTrees(actx, report, trees)
|
|
650
|
+
compareTrees(actx, report, found, trees)
|
|
597
651
|
}
|
|
598
652
|
|
|
599
653
|
|
|
@@ -677,7 +731,7 @@ function checkFeatureSource(
|
|
|
677
731
|
actx: ActionContext,
|
|
678
732
|
feature: Source,
|
|
679
733
|
targets: Map<string, Source>,
|
|
680
|
-
|
|
734
|
+
found: Found,
|
|
681
735
|
) {
|
|
682
736
|
const fs = actx.fs()
|
|
683
737
|
const root = actx.folder
|
|
@@ -689,14 +743,14 @@ function checkFeatureSource(
|
|
|
689
743
|
const label = 'tm/' + tname + '/' + rel
|
|
690
744
|
|
|
691
745
|
if (!fs.existsSync(project)) {
|
|
692
|
-
|
|
746
|
+
found('missing', label)
|
|
693
747
|
continue
|
|
694
748
|
}
|
|
695
749
|
|
|
696
750
|
// The same map `feature add` copies with — see helpers/stdrep. A
|
|
697
751
|
// different one here would report every substituted file as edited.
|
|
698
752
|
if (differs(fs, from, project, model, templateReplacements(model, tname))) {
|
|
699
|
-
|
|
753
|
+
found('edited', label)
|
|
700
754
|
}
|
|
701
755
|
}
|
|
702
756
|
}
|
|
@@ -705,6 +759,7 @@ function checkFeatureSource(
|
|
|
705
759
|
|
|
706
760
|
function checkItemModel(
|
|
707
761
|
actx: ActionContext, kind: string, source: Source, report: DoctorReport,
|
|
762
|
+
found: Found,
|
|
708
763
|
) {
|
|
709
764
|
const name = source.name
|
|
710
765
|
const origname = source.origname
|
|
@@ -723,7 +778,7 @@ function checkItemModel(
|
|
|
723
778
|
const label = 'model/' + kind + '/' + Path.basename(project)
|
|
724
779
|
|
|
725
780
|
if (!fs.existsSync(project)) {
|
|
726
|
-
|
|
781
|
+
found('missing', label)
|
|
727
782
|
return
|
|
728
783
|
}
|
|
729
784
|
|
|
@@ -746,7 +801,92 @@ function checkItemModel(
|
|
|
746
801
|
return
|
|
747
802
|
}
|
|
748
803
|
|
|
749
|
-
|
|
804
|
+
found('forked', label)
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
// Files every finding through one place: a forked or edited copy the record
|
|
809
|
+
// shows untouched is outdated instead, and every item keeps a tally.
|
|
810
|
+
function noteDrift(
|
|
811
|
+
actx: ActionContext, report: DoctorReport, copies: CopyRecord,
|
|
812
|
+
kind: string, source: Source,
|
|
813
|
+
): Found {
|
|
814
|
+
const fs = actx.fs()
|
|
815
|
+
const item = itemKey(kind, source.name)
|
|
816
|
+
const copied = copies.items[item]
|
|
817
|
+
|
|
818
|
+
return (category: Category, label: string) => {
|
|
819
|
+
let into: Category | 'outdated' = category
|
|
820
|
+
|
|
821
|
+
if ('forked' === category || 'edited' === category) {
|
|
822
|
+
const same = untouched(fs, actx.folder, copies, label)
|
|
823
|
+
|
|
824
|
+
if (true === same) {
|
|
825
|
+
into = 'outdated'
|
|
826
|
+
}
|
|
827
|
+
else if (undefined === same) {
|
|
828
|
+
report.unrecorded.push(label)
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
report[into].push(label)
|
|
833
|
+
|
|
834
|
+
const drift = report.byItem[item] = report.byItem[item] ?? {
|
|
835
|
+
...pkgOf(source.package ?? copied?.package),
|
|
836
|
+
...(null == copied?.version ? {} : { from: copied.version }),
|
|
837
|
+
...(null == source.version ? {} : { to: source.version }),
|
|
838
|
+
outdated: 0,
|
|
839
|
+
changed: 0,
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if ('outdated' === into) {
|
|
843
|
+
drift.outdated++
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
drift.changed++
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
function pkgOf(name?: string): { package?: string } {
|
|
853
|
+
return null == name ? {} : { package: name }
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
// The command that brings an item's copies up to the installed source. The
|
|
858
|
+
// package form checks first and refuses to overwrite a local edit.
|
|
859
|
+
function refreshCommand(item: string, pkg?: string): string {
|
|
860
|
+
if (null != pkg) {
|
|
861
|
+
return 'package update ' + pkg + ' --no-fetch'
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const [kind, ...name] = item.split('/')
|
|
865
|
+
return kind + ' add ' + name.join('/')
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
function outdatedNote(item: string, drift: ItemDrift): string {
|
|
870
|
+
return item.replace('/', ' ') + ': ' + drift.outdated +
|
|
871
|
+
' file(s) are unchanged copies' + sinceCopied(drift.package, [drift])
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
// Names both versions only when they differ: a source that changed under the
|
|
876
|
+
// same version (a linked checkout, say) is still a source that moved on.
|
|
877
|
+
function sinceCopied(pkg: string | undefined, drift: ItemDrift[]): string {
|
|
878
|
+
const froms = Array.from(new Set(drift.map((d: ItemDrift) => d.from)))
|
|
879
|
+
const to = drift[0].to
|
|
880
|
+
const from = 1 === froms.length ? froms[0] : undefined
|
|
881
|
+
const name = pkg ?? 'their source'
|
|
882
|
+
|
|
883
|
+
if (null != from && null != to && from !== to) {
|
|
884
|
+
return ' from ' + name + ' ' + from + ', but ' + name + ' ' + to +
|
|
885
|
+
' is installed'
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return ', but ' + name + (null == to ? '' : ' ' + to) +
|
|
889
|
+
' has changed since they were copied'
|
|
750
890
|
}
|
|
751
891
|
|
|
752
892
|
|
|
@@ -868,13 +1008,19 @@ function renderPair(
|
|
|
868
1008
|
const rawsrc = fs.readFileSync(scaffoldPath, 'utf8')
|
|
869
1009
|
const src = null == rewrite ? rawsrc : rewrite(rawsrc)
|
|
870
1010
|
|
|
1011
|
+
// git may rewrite line endings on checkout (core.autocrlf).
|
|
871
1012
|
return {
|
|
872
|
-
expected: template(src, model, { replace }),
|
|
873
|
-
actual: fs.readFileSync(projectPath, 'utf8'),
|
|
1013
|
+
expected: lfEndings(template(src, model, { replace })),
|
|
1014
|
+
actual: lfEndings(fs.readFileSync(projectPath, 'utf8')),
|
|
874
1015
|
}
|
|
875
1016
|
}
|
|
876
1017
|
|
|
877
1018
|
|
|
1019
|
+
function lfEndings(text: string): string {
|
|
1020
|
+
return text.replace(/\r\n/g, '\n')
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
|
|
878
1024
|
function quietLog(log: any): any {
|
|
879
1025
|
const noop = () => { }
|
|
880
1026
|
const quiet: any = { info: noop, debug: noop, warn: log.warn.bind(log), error: noop, trace: noop, fatal: noop }
|
|
@@ -883,12 +1029,92 @@ function quietLog(log: any): any {
|
|
|
883
1029
|
}
|
|
884
1030
|
|
|
885
1031
|
|
|
1032
|
+
function silentLog(): any {
|
|
1033
|
+
const noop = () => { }
|
|
1034
|
+
const silent: any = { info: noop, debug: noop, warn: noop, error: noop, trace: noop, fatal: noop }
|
|
1035
|
+
silent.child = () => silent
|
|
1036
|
+
return silent
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
// What `generate` checks before it reads the copies: the active items only,
|
|
1041
|
+
// and a warning per refresh command rather than a failure, since a
|
|
1042
|
+
// difference may be a deliberate edit.
|
|
1043
|
+
async function copyCheck(actx: ActionContext): Promise<string[]> {
|
|
1044
|
+
const active = (kind: string, name: string) =>
|
|
1045
|
+
false !== kindCollection(actx.model, kind)?.[name]?.active
|
|
1046
|
+
|
|
1047
|
+
const res: any = await doctor({ ...actx, log: silentLog() }, active)
|
|
1048
|
+
|
|
1049
|
+
return copyWarnings(res.report)
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
function copyWarnings(report: DoctorReport): string[] {
|
|
1054
|
+
const groups = new Map<string, { items: string[], files: number, drift: ItemDrift[] }>()
|
|
1055
|
+
|
|
1056
|
+
const add = (key: string, item: string, files: number, drift: ItemDrift) => {
|
|
1057
|
+
const group = groups.get(key) ?? { items: [], files: 0, drift: [] }
|
|
1058
|
+
group.items.push(item.replace('/', ' '))
|
|
1059
|
+
group.files += files
|
|
1060
|
+
group.drift.push(drift)
|
|
1061
|
+
groups.set(key, group)
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
for (const item of Object.keys(report.byItem).sort()) {
|
|
1065
|
+
const drift = report.byItem[item]
|
|
1066
|
+
const command = refreshCommand(item, drift.package)
|
|
1067
|
+
|
|
1068
|
+
if (0 < drift.outdated) {
|
|
1069
|
+
add('outdated\n' + command, item, drift.outdated, drift)
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (0 < drift.changed) {
|
|
1073
|
+
add('changed\n' + command, item, drift.changed, drift)
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
const lines: string[] = []
|
|
1078
|
+
|
|
1079
|
+
for (const [key, group] of groups) {
|
|
1080
|
+
const [what, command] = key.split('\n')
|
|
1081
|
+
const pkg = group.drift[0].package
|
|
1082
|
+
const to = group.drift[0].to
|
|
1083
|
+
const installed = null == pkg ? 'their source' :
|
|
1084
|
+
(pkg + (null == to ? '' : ' ' + to))
|
|
1085
|
+
const refresh = '`npx voxgig-sdkgen ' + command + '`'
|
|
1086
|
+
|
|
1087
|
+
if ('outdated' === what) {
|
|
1088
|
+
lines.push(group.items.join(', ') + ': ' + group.files +
|
|
1089
|
+
' file(s) in .sdk are unchanged copies' +
|
|
1090
|
+
sinceCopied(pkg, group.drift) + ', so this run generates from ' +
|
|
1091
|
+
'older templates. Run ' + refresh + ' to refresh them.')
|
|
1092
|
+
}
|
|
1093
|
+
else {
|
|
1094
|
+
lines.push(group.items.join(', ') + ': ' + group.files +
|
|
1095
|
+
' file(s) in .sdk differ from what ' + installed + ' installs: ' +
|
|
1096
|
+
'changed in this project, or copied before sdkgen recorded its ' +
|
|
1097
|
+
'copies. `npx voxgig-sdkgen doctor` lists them; ' + refresh +
|
|
1098
|
+
' refreshes them' +
|
|
1099
|
+
(null == pkg ? ', discarding any local edit.' :
|
|
1100
|
+
', and refuses before it overwrites a local edit.'))
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return lines
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
|
|
886
1108
|
export type {
|
|
887
1109
|
DoctorReport,
|
|
888
1110
|
DoctorScope,
|
|
1111
|
+
ItemDrift,
|
|
889
1112
|
}
|
|
890
1113
|
|
|
891
1114
|
export {
|
|
892
1115
|
action_doctor,
|
|
893
1116
|
doctor,
|
|
1117
|
+
copyCheck,
|
|
1118
|
+
copyWarnings,
|
|
1119
|
+
refreshCommand,
|
|
894
1120
|
}
|