@geonosis/doctor 2.10.0 → 2.11.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 +229 -0
- package/README.md +80 -38
- package/bin/geonosis-doctor.mjs +164 -17
- package/dist/{chunk-2ZVCJG5R.js → chunk-2VHAFVWH.js} +759 -489
- package/dist/doctor-cli.js +15 -8
- package/dist/index.d.ts +117 -33
- package/dist/index.js +15 -1
- package/package.json +4 -4
package/bin/geonosis-doctor.mjs
CHANGED
|
@@ -1,14 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
import {
|
|
2
|
+
// Every packages/*/bin shim is packages/cli/bin/geonosis.mjs with its own name and entry: edit that
|
|
3
|
+
// one, then `pnpm generate:shims`. Committed so `pnpm install` can link the bin on a fresh clone,
|
|
4
|
+
// before `pnpm build` has made dist/. In the repo `src/` sits beside `dist/`, and a dist older
|
|
5
|
+
// than src answered for a fix it did not carry once (#62); the published package ships no src, so
|
|
6
|
+
// there the check is skipped.
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readdirSync,
|
|
12
|
+
readFileSync,
|
|
13
|
+
realpathSync,
|
|
14
|
+
statSync,
|
|
15
|
+
writeFileSync
|
|
16
|
+
} from 'node:fs'
|
|
8
17
|
import { dirname, join } from 'node:path'
|
|
9
18
|
import { fileURLToPath } from 'node:url'
|
|
10
19
|
|
|
11
20
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const own = join(here, '..')
|
|
12
22
|
// tsup never reads a test file, so one counting as source made every bin refuse a build that had
|
|
13
23
|
// not gone stale (#399).
|
|
14
24
|
const NOT_SOURCE = (name) =>
|
|
@@ -24,25 +34,162 @@ const newest = (dir, skip) =>
|
|
|
24
34
|
return Math.max(most, entry.isDirectory() ? newest(at, skip) : statSync(at).mtimeMs)
|
|
25
35
|
}, 0)
|
|
26
36
|
: 0
|
|
27
|
-
const
|
|
28
|
-
const
|
|
37
|
+
const digestOf = (dir, hash) => {
|
|
38
|
+
for (const name of readdirSync(dir).toSorted()) {
|
|
39
|
+
if (NOT_SOURCE(name)) continue
|
|
40
|
+
const at = join(dir, name)
|
|
41
|
+
if (statSync(at).isDirectory()) {
|
|
42
|
+
digestOf(at, hash)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
hash.update(name).update('\0').update(readFileSync(at)).update('\0')
|
|
46
|
+
}
|
|
47
|
+
return hash
|
|
48
|
+
}
|
|
49
|
+
const digest = (...dirs) => {
|
|
50
|
+
const hash = createHash('sha256')
|
|
51
|
+
for (const dir of dirs) if (existsSync(dir)) digestOf(dir, hash)
|
|
52
|
+
return hash.digest('hex')
|
|
53
|
+
}
|
|
54
|
+
// mtime says a source file was WRITTEN, not that it CHANGED. A checkout rewrites it with the same
|
|
55
|
+
// content, and the next `pnpm build` is a cache hit that leaves the current dist exactly as it
|
|
56
|
+
// was — so every bin refused with "run pnpm build", the action that had just changed nothing and
|
|
57
|
+
// would change nothing again. So a digest of the sources is kept from the last run that found
|
|
58
|
+
// dist current: the same digest later is the same sources, whatever their timestamps say, and a
|
|
59
|
+
// different one is the staleness this guard exists for. The newest mtime is kept beside it so a
|
|
60
|
+
// run over untouched sources need not hash them again. The second line is the same record for
|
|
61
|
+
// the dists of the workspace dependencies this build read (row 584, below).
|
|
62
|
+
const recordOf = (pkg) => join(pkg, '.turbo', 'geonosis-src-digest')
|
|
63
|
+
const recorded = (pkg) => {
|
|
64
|
+
try {
|
|
65
|
+
const [first = '', second = ''] = readFileSync(recordOf(pkg), 'utf8').split('\n')
|
|
66
|
+
const [seen = '', sum = ''] = first.trim().split(' ')
|
|
67
|
+
const [depsSeen = '', depsSum = ''] = second.trim().split(' ')
|
|
68
|
+
return { depsSeen: Number(depsSeen), depsSum, seen: Number(seen), sum }
|
|
69
|
+
} catch {
|
|
70
|
+
return { depsSeen: 0, depsSum: '', seen: 0, sum: '' }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const remember = (pkg, known) => {
|
|
74
|
+
try {
|
|
75
|
+
mkdirSync(dirname(recordOf(pkg)), { recursive: true })
|
|
76
|
+
writeFileSync(recordOf(pkg), `${known.seen} ${known.sum}\n${known.depsSeen} ${known.depsSum}\n`)
|
|
77
|
+
} catch {
|
|
78
|
+
// A record nothing could write costs one more hash next time, never a wrong answer.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// The refusal names a package for turbo's --filter. A manifest that cannot be read costs the
|
|
82
|
+
// name, never the decision: the first digest shim threw here — exit 1 and a node:fs frame — over a
|
|
83
|
+
// tree with no package.json, and a guard that throws neither answers nor refuses.
|
|
84
|
+
const manifestOf = (pkg) => {
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(readFileSync(join(pkg, 'package.json'), 'utf8'))
|
|
87
|
+
} catch {
|
|
88
|
+
return {}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const refuse = (sentence) => {
|
|
92
|
+
process.stderr.write(`geonosis-doctor: ${sentence}\n`)
|
|
93
|
+
process.exit(2)
|
|
94
|
+
}
|
|
95
|
+
// Whether one package's dist reflects its src: 'unbuilt', 'stale' or 'current'. The same reading
|
|
96
|
+
// for this package and for each workspace dependency, off that package's own record.
|
|
97
|
+
const standing = (pkg) => {
|
|
98
|
+
const src = join(pkg, 'src')
|
|
99
|
+
const dist = join(pkg, 'dist')
|
|
100
|
+
if (!existsSync(src)) return 'current'
|
|
101
|
+
const built = newest(dist)
|
|
102
|
+
if (built === 0) return 'unbuilt'
|
|
103
|
+
const written = newest(src, NOT_SOURCE)
|
|
104
|
+
const known = recorded(pkg)
|
|
105
|
+
if (written > built) {
|
|
106
|
+
const sum = known.seen === written ? known.sum : digest(src)
|
|
107
|
+
if (sum !== known.sum) return 'stale'
|
|
108
|
+
if (known.seen !== written) remember(pkg, { ...known, seen: written, sum })
|
|
109
|
+
} else if (known.seen !== written) {
|
|
110
|
+
remember(pkg, { ...known, seen: written, sum: digest(src) })
|
|
111
|
+
}
|
|
112
|
+
return 'current'
|
|
113
|
+
}
|
|
114
|
+
// Row 584: a dist current against its own src can still have been built against an OLDER
|
|
115
|
+
// dependency's dist — literally, when tsup inlined a workspace devDependency into it — and a
|
|
116
|
+
// dependency whose src has moved past its dist is read by this dist stale, inlined or imported.
|
|
117
|
+
// So every workspace dependency this manifest names (`workspace:`, resolved through the link pnpm
|
|
118
|
+
// made, transitively) is held to the same reading as this package, and this dist must not be
|
|
119
|
+
// older than any of theirs — by content: a dependency rebuilt to the same bytes is the same build,
|
|
120
|
+
// so the `--force --filter` escape below cannot poison the tree it was run in.
|
|
121
|
+
const workspaceDependencies = (pkg, found = new Map()) => {
|
|
122
|
+
const manifest = manifestOf(pkg)
|
|
123
|
+
for (const block of [
|
|
124
|
+
manifest.dependencies,
|
|
125
|
+
manifest.devDependencies,
|
|
126
|
+
manifest.optionalDependencies,
|
|
127
|
+
manifest.peerDependencies
|
|
128
|
+
]) {
|
|
129
|
+
for (const [name, range] of Object.entries(block ?? {})) {
|
|
130
|
+
if (typeof range !== 'string' || !range.startsWith('workspace:')) continue
|
|
131
|
+
let dir
|
|
132
|
+
try {
|
|
133
|
+
dir = realpathSync(join(pkg, 'node_modules', name))
|
|
134
|
+
} catch {
|
|
135
|
+
continue // Not linked: the import will say so far more loudly than this guard can.
|
|
136
|
+
}
|
|
137
|
+
if (found.has(dir)) continue
|
|
138
|
+
found.set(dir, name)
|
|
139
|
+
workspaceDependencies(dir, found)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return found
|
|
143
|
+
}
|
|
29
144
|
// `newest` answers 0 for a directory that is NOT THERE and for one that is merely old, so the
|
|
30
145
|
// two conditions were one comparison and one sentence — and the staleness sentence was printed
|
|
31
146
|
// over trees that had never been built at all, blocking a turn in a fresh git worktree, which
|
|
32
147
|
// has no dist because dist is not committed (#357). Only the second is evidence of a mistake.
|
|
33
|
-
if (existsSync(src)) {
|
|
34
|
-
const built = newest(dist)
|
|
148
|
+
if (existsSync(join(own, 'src'))) {
|
|
149
|
+
const built = newest(join(own, 'dist'))
|
|
35
150
|
if (built === 0) {
|
|
36
|
-
|
|
37
|
-
'
|
|
151
|
+
refuse(
|
|
152
|
+
'this tree has never been built — run pnpm build. No dist here at all, which is what a fresh clone or a linked git worktree starts with; nothing is stale.'
|
|
38
153
|
)
|
|
39
|
-
process.exit(2)
|
|
40
154
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
155
|
+
const name = manifestOf(own).name ?? '<this package>'
|
|
156
|
+
if (standing(own) === 'stale') {
|
|
157
|
+
refuse(
|
|
158
|
+
`dist is older than src — run pnpm build before trusting this bin. If that build just said FULL TURBO, the cache hit left a current dist with its old timestamp and this bin had not yet recorded the sources it was current for: pnpm build --force --filter=${name} rebuilds this one package and clears it.`
|
|
44
159
|
)
|
|
45
|
-
|
|
160
|
+
}
|
|
161
|
+
const dependencies = workspaceDependencies(own)
|
|
162
|
+
const dists = new Map()
|
|
163
|
+
for (const [dir, dependency] of dependencies) {
|
|
164
|
+
const state = standing(dir)
|
|
165
|
+
if (state === 'unbuilt') {
|
|
166
|
+
refuse(
|
|
167
|
+
`${dependency}, which this dist reads, has never been built — run pnpm build. No dist there at all; nothing is stale.`
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
if (state === 'stale') {
|
|
171
|
+
refuse(
|
|
172
|
+
`${dependency}'s dist is older than its src, and this dist reads that dist — run pnpm build before trusting this bin. If that build just said FULL TURBO, the cache hit left that dist current with its old timestamp and nothing had yet recorded the sources it was current for: pnpm build --force --filter=${dependency} rebuilds that one package and clears it.`
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
dists.set(join(dir, 'dist'), dependency)
|
|
176
|
+
}
|
|
177
|
+
if (dists.size > 0) {
|
|
178
|
+
const known = recorded(own)
|
|
179
|
+
const theirs = [...dists.keys()].toSorted()
|
|
180
|
+
const depsNewest = theirs.reduce((most, dist) => Math.max(most, newest(dist)), 0)
|
|
181
|
+
if (depsNewest > built) {
|
|
182
|
+
const depsSum = known.depsSeen === depsNewest ? known.depsSum : digest(...theirs)
|
|
183
|
+
if (depsSum !== known.depsSum) {
|
|
184
|
+
const newer = theirs.filter((dist) => newest(dist) > built).map((dist) => dists.get(dist))
|
|
185
|
+
refuse(
|
|
186
|
+
`dist was built before the current dist of ${newer.join(', ')}, which it reads — run pnpm build before trusting this bin. If that build just said FULL TURBO, the dependency was rebuilt alone and this bin had not yet recorded the build it was current against: pnpm build --force --filter=${name} rebuilds this one package against it.`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
if (known.depsSeen !== depsNewest) remember(own, { ...known, depsSeen: depsNewest, depsSum })
|
|
190
|
+
} else if (known.depsSeen !== depsNewest) {
|
|
191
|
+
remember(own, { ...known, depsSeen: depsNewest, depsSum: digest(...theirs) })
|
|
192
|
+
}
|
|
46
193
|
}
|
|
47
194
|
}
|
|
48
195
|
|