-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck-primordials.ts
More file actions
313 lines (295 loc) · 10.5 KB
/
check-primordials.ts
File metadata and controls
313 lines (295 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
/**
* @file `socket-lib check primordials` handler. Loads a JSON config from disk
* (default `primordials-coverage.config.json` at the repo root, override with
* `--config <path>`), runs the drift check, and renders the result.
* socket-lib check primordials socket-lib check primordials --config
* ./primordials.config.json socket-lib check primordials --json #
* machine-readable output socket-lib check primordials --explain # one
* detailed line per finding socket-lib check primordials --silent # silent on
* success Exit codes: 0 — no drift 1 — drift detected (or config / lookup
* error)
*/
import { existsSync, readFileSync } from 'node:fs'
import path from 'node:path'
import process from 'node:process'
import { errorMessage } from '../errors/message'
import { getDefaultLogger } from '../logger/default'
import { ArrayIsArray } from '../primordials/array'
import { ErrorCtor } from '../primordials/error'
import { JSONParse, JSONStringify } from '../primordials/json'
import { ObjectEntries } from '../primordials/object'
import {
DEFAULT_ALIAS_MAP,
DEFAULT_NODE_INTERNAL_ONLY,
} from '../checks/primordials-defaults'
import { checkPrimordials } from '../checks/primordials'
import type {
PrimordialsCheckConfig,
PrimordialsCheckResult,
PrimordialsFinding,
} from '../checks/primordials'
import { parseArgs as parseLibArgs } from '../argv/parse'
import { MapCtor, SetCtor } from '../primordials/map-set'
const logger = getDefaultLogger()
// Default config name. We accept both the root-level dotfile (the
// canonical `.<tool>rc.json` shape) and the `.config/`-rooted variant
// (the fleet pattern for tooling configs). Look up in this order when
// `--config` was not explicitly passed, falling through to the next
// candidate if the file is missing — first hit wins. One file per
// repo for all socket-lib checks; section per check.
// At the repo root we use the canonical `.<tool>.json` dotfile shape.
// Inside `.config/`, the directory itself is already hidden, so the
// fleet convention drops the leading dot — every existing file under
// `.config/` (taze.config.mts, vitest.config.mts, tsconfig.base.json,
// ...) is bare-named. Match that.
const DEFAULT_CONFIG_PATH = '.socket-lib.json'
const FALLBACK_CONFIG_PATHS: readonly string[] = [
'.socket-lib.json',
'.config/socket-lib.json',
]
const CONFIG_SECTION = 'primordials'
export interface ParsedArgs {
readonly config: string | undefined
readonly json: boolean
readonly explain: boolean
readonly silent: boolean
readonly help: boolean
}
export interface RawConfig {
scanDirs?: unknown | undefined
aliasMap?: unknown | undefined
nodeInternalOnly?: unknown | undefined
socketLibPrimordialsPath?: unknown | undefined
}
export interface SerializedFinding {
kind: PrimordialsFinding['kind']
name: string
files: readonly string[]
hint: string
}
export function loadConfig(configPath: string): PrimordialsCheckConfig {
if (!existsSync(configPath)) {
throw new ErrorCtor(`config file not found: ${configPath}`)
}
let parsed: unknown
try {
parsed = JSONParse(readFileSync(configPath, 'utf8'))
} catch (e) {
throw new ErrorCtor(`config file is not valid JSON: ${errorMessage(e)}`)
}
// The fleet convention is `.socket-lib.json` with a section per
// check (primordials, paths, public-surface, ...). When the file
// has the section, use it; otherwise treat the whole file as the
// primordials config (back-compat with single-check setups).
if (typeof parsed !== 'object' || parsed === null || ArrayIsArray(parsed)) {
throw new ErrorCtor('config root must be an object')
}
const root = parsed as Record<string, unknown>
const sectional = root[CONFIG_SECTION]
const raw = (sectional !== undefined ? sectional : root) as RawConfig
// Validate shape with concrete error messages — config files are
// hand-edited and a misspelling here is the most common failure
// mode. Don't let a wrong type slip through to the check engine.
if (!ArrayIsArray(raw.scanDirs)) {
throw new ErrorCtor(
`config.scanDirs must be an array of strings (got ${typeof raw.scanDirs})`,
)
}
for (const [i, v] of raw.scanDirs.entries()) {
if (typeof v !== 'string') {
throw new ErrorCtor(`config.scanDirs[${i}] must be a string`)
}
}
if (
raw.aliasMap !== undefined &&
(typeof raw.aliasMap !== 'object' ||
raw.aliasMap === null ||
ArrayIsArray(raw.aliasMap))
) {
throw new ErrorCtor('config.aliasMap must be an object of source→target')
}
if (
raw.nodeInternalOnly !== undefined &&
!ArrayIsArray(raw.nodeInternalOnly)
) {
throw new ErrorCtor('config.nodeInternalOnly must be an array of strings')
}
// Merge the fleet-canonical defaults with the user's config. The user
// map overlays the defaults — any key the user defines wins, but they
// don't have to repeat the 26-entry boilerplate that every fleet repo
// shares. The Map constructor consumes the entries in order, so the
// user's later entries naturally overwrite the earlier defaults.
const aliasMap = new MapCtor<string, string>([
...ObjectEntries(DEFAULT_ALIAS_MAP),
...ObjectEntries((raw.aliasMap ?? {}) as Record<string, string>),
])
const nodeInternalOnly = new SetCtor<string>([
...DEFAULT_NODE_INTERNAL_ONLY,
...((raw.nodeInternalOnly ?? []) as string[]).filter(
x => typeof x === 'string',
),
])
// repoRoot is where scanDirs are resolved from. Default to cwd —
// the user runs `socket-lib check prim` from their repo root.
// Override via config if the config lives somewhere unusual.
return {
scanDirs: raw.scanDirs as string[],
aliasMap,
nodeInternalOnly,
socketLibPrimordialsPath:
typeof raw.socketLibPrimordialsPath === 'string'
? raw.socketLibPrimordialsPath
: undefined,
repoRoot: process.cwd(),
}
}
export function parseArgs(argv: readonly string[]): ParsedArgs {
const { values } = parseLibArgs({
args: argv,
strict: false,
options: {
config: { type: 'string', short: 'c' },
explain: { type: 'boolean' },
help: { type: 'boolean', short: 'h' },
json: { type: 'boolean' },
silent: { type: 'boolean' },
},
})
// `config` is left undefined when neither `--config` nor `-c` was
// passed, so the resolver below can fall back to the search list.
// An explicit value short-circuits the search.
const explicitConfig = values['config']
return {
config: typeof explicitConfig === 'string' ? explicitConfig : undefined,
json: Boolean(values['json']),
explain: Boolean(values['explain']),
silent: Boolean(values['silent']),
help: Boolean(values['help']),
}
}
export function printHelp(): void {
logger.log('socket-lib check primordials — primordials drift check')
logger.log('')
logger.log('Usage:')
logger.log(' socket-lib check primordials [opts]')
logger.log(' socket-lib check prim [opts] # short alias')
logger.log('')
logger.log('Options:')
logger.log(
` --config, -c <path> Config file. Default: ${DEFAULT_CONFIG_PATH}`,
)
logger.log(` (falls back to .config/socket-lib.json)`)
logger.log(' --explain Print one detailed line per finding.')
logger.log(' --json Machine-readable JSON output.')
logger.log(' --silent Silent on success.')
logger.log(' --help, -h Print this help.')
logger.log('')
logger.log('Config (.socket-lib.json — primordials section):')
logger.log(' {')
logger.log(' "primordials": {')
logger.log(
' "scanDirs": ["src", "additions/source-patched/lib"]',
)
logger.log(' }')
logger.log(' }')
logger.log('')
logger.log('Only `scanDirs` is required. `aliasMap` and `nodeInternalOnly`')
logger.log('default to the fleet-canonical sets and only need entries when')
logger.log('your repo extends or overrides them.')
logger.log('')
logger.log('A bare object (no `primordials` section) is also accepted for')
logger.log('repos that only run this one check.')
}
export function renderHuman(
result: PrimordialsCheckResult,
args: ParsedArgs,
): void {
if (result.findings.length === 0) {
if (!args.silent) {
logger.success(
`Primordials coverage OK — ${result.used.size} names used, all accounted for.`,
)
}
return
}
logger.error(
`Primordials drift detected — ${result.findings.length} unaccounted name(s):`,
)
for (const f of result.findings) {
logger.error(` ${f.name}`)
if (args.explain) {
logger.error(` ${f.hint}`)
if (f.files.length > 0) {
logger.error(` files: ${f.files.join(', ')}`)
}
}
}
if (!args.explain) {
logger.error('')
logger.error('Run with --explain for fix instructions and file references.')
}
}
/**
* Pick the config file. Returns the explicit `--config` argument when given
* (even if it doesn't exist — the caller will surface the error with the path
* they typed). Otherwise probes the fallback list in order and returns the
* first hit. Returns the head of the list when none exist, so the caller's
* "config file not found" error message names the canonical default.
*/
export function resolveConfigPath(explicit: string | undefined): string {
if (explicit !== undefined) {
return explicit
}
for (let i = 0, { length } = FALLBACK_CONFIG_PATHS; i < length; i += 1) {
const candidate = FALLBACK_CONFIG_PATHS[i]!
if (existsSync(path.resolve(candidate))) {
return candidate
}
}
return FALLBACK_CONFIG_PATHS[0]!
}
export async function runCheckPrimordials(
argv: readonly string[],
): Promise<number> {
const args = parseArgs(argv)
if (args.help) {
printHelp()
return 0
}
let config: PrimordialsCheckConfig
try {
config = loadConfig(path.resolve(resolveConfigPath(args.config)))
} catch (e) {
logger.error(`socket-lib check primordials: ${errorMessage(e)}`)
return 1
}
let result: PrimordialsCheckResult
try {
result = checkPrimordials(config)
} catch (e) {
logger.error(`socket-lib check primordials: ${errorMessage(e)}`)
return 1
}
if (args.json) {
logger.log(JSONStringify(serialize(result), null, 2))
} else {
renderHuman(result, args)
}
return result.findings.length === 0 ? 0 : 1
}
export function serialize(result: PrimordialsCheckResult): {
ok: boolean
used: number
findings: SerializedFinding[]
} {
return {
ok: result.findings.length === 0,
used: result.used.size,
findings: result.findings.map(f => ({
kind: f.kind,
name: f.name,
files: f.files,
hint: f.hint,
})),
}
}