@kitschpatrol/shared-config 8.12.1 → 8.13.1

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.
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Prints one sorted row per package in a `pnpm list --prod --depth Infinity
3
+ * --json` tree: the name, version, and declared `engines.node`, read from each
4
+ * installed `package.json` instead of trusting the list output. A dependency
5
+ * whose installed directory is missing, typically a platform-specific optional
6
+ * dependency not installed on this host, is reported with `installed: false`
7
+ * and the name and version from the list; review its engines from registry
8
+ * metadata.
9
+ *
10
+ * Usage: `node production-engines.mjs production-tree.json`
11
+ */
12
+ import { existsSync, readFileSync } from 'node:fs'
13
+ import { join } from 'node:path'
14
+ import process from 'node:process'
15
+
16
+ /**
17
+ * Narrows an unknown JSON value to a plain object.
18
+ *
19
+ * @param {unknown} value - Any parsed JSON value.
20
+ *
21
+ * @returns {value is Record<string, unknown>} Whether the value is a non-array
22
+ * object.
23
+ */
24
+ function isRecord(value) {
25
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
26
+ }
27
+
28
+ /**
29
+ * Returns the value when it is a string.
30
+ *
31
+ * @param {unknown} value - Any parsed JSON value.
32
+ *
33
+ * @returns {string | undefined} The string, or undefined for any other type.
34
+ */
35
+ function asString(value) {
36
+ return typeof value === 'string' ? value : undefined
37
+ }
38
+
39
+ const inputPath = process.argv[2]
40
+ if (inputPath === undefined) {
41
+ throw new Error('Usage: node production-engines.mjs <pnpm-list-json-file>')
42
+ }
43
+
44
+ /** @type {unknown} */
45
+ const parsed = JSON.parse(readFileSync(inputPath, 'utf8'))
46
+ if (!Array.isArray(parsed)) {
47
+ throw new TypeError(`Expected a JSON array from pnpm list in ${inputPath}`)
48
+ }
49
+
50
+ /** @type {Set<string>} */
51
+ const visited = new Set()
52
+ /** @type {Set<string>} */
53
+ const rows = new Set()
54
+
55
+ /**
56
+ * Records one tree entry and recurses into its production and optional
57
+ * dependencies, skipping subtrees already walked.
58
+ *
59
+ * @param {Record<string, unknown>} entry - A project or dependency node from
60
+ * the pnpm list output.
61
+ */
62
+ function visit(entry) {
63
+ const installedPath = asString(entry.path)
64
+ if (installedPath === undefined) {
65
+ const label = asString(entry.name) ?? asString(entry.version) ?? 'unnamed entry'
66
+ throw new Error(`Missing path property for ${label}`)
67
+ }
68
+
69
+ if (visited.has(installedPath)) {
70
+ return
71
+ }
72
+
73
+ visited.add(installedPath)
74
+
75
+ const manifestPath = join(installedPath, 'package.json')
76
+ if (existsSync(manifestPath)) {
77
+ /** @type {unknown} */
78
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
79
+ if (!isRecord(manifest)) {
80
+ throw new TypeError(`Expected an object in ${manifestPath}`)
81
+ }
82
+
83
+ const engines = isRecord(manifest.engines) ? manifest.engines : {}
84
+ rows.add(
85
+ JSON.stringify({
86
+ name: asString(manifest.name),
87
+ node: asString(engines.node),
88
+ version: asString(manifest.version),
89
+ }),
90
+ )
91
+ } else {
92
+ rows.add(
93
+ JSON.stringify({
94
+ installed: false,
95
+ name: asString(entry.name) ?? asString(entry.from),
96
+ version: asString(entry.version),
97
+ }),
98
+ )
99
+ }
100
+
101
+ for (const group of ['dependencies', 'optionalDependencies']) {
102
+ const children = entry[group]
103
+ if (isRecord(children)) {
104
+ for (const [name, child] of Object.entries(children)) {
105
+ if (isRecord(child)) {
106
+ visit({ name, ...child })
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+
113
+ for (const root of parsed) {
114
+ if (isRecord(root)) {
115
+ visit(root)
116
+ }
117
+ }
118
+
119
+ process.stdout.write(`${[...rows].toSorted().join('\n')}\n`)
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Release-age helpers for the exemption policy in configuration.md.
3
+ *
4
+ * `inventory` classifies every `minimumReleaseAgeExclude` entry in
5
+ * `pnpm-workspace.yaml` against the lockfile, the registry publish time, and
6
+ * the authored-package list, printing the policy action for each. Run it during
7
+ * Setup before the baseline install and again during step 6 cleanup.
8
+ *
9
+ * `new-versions` lists every package version present in the after-lockfile but
10
+ * not the before-lockfile that is under 24 hours old at the run start, with
11
+ * whether it is authored and when it becomes installable. Run it after each
12
+ * resolution to find authored versions that need an exact exemption and
13
+ * third-party versions that must not get one.
14
+ *
15
+ * Usage, from the workspace root:
16
+ *
17
+ * ```
18
+ * node release-age.mjs inventory [--workspace <file>] [--lockfile <file>] [--run-start <iso>] [--authored <file>]
19
+ * node release-age.mjs new-versions <before-lock> [after-lock] [--run-start <iso>] [--authored <file>]
20
+ * ```
21
+ *
22
+ * `--authored` defaults to `<run directory>/authored-packages.txt`, the
23
+ * comma-separated ownership list from Setup step 5. `--workspace` and
24
+ * `--lockfile` default to the files in the current directory; point them at
25
+ * saved copies to inspect another state without touching the repository. Output
26
+ * is JSON on stdout with a summary on stderr.
27
+ */
28
+ import { existsSync, readFileSync } from 'node:fs'
29
+ import { join } from 'node:path'
30
+ import process from 'node:process'
31
+ import {
32
+ lockedPackages,
33
+ mapLimit,
34
+ MATURITY_MS,
35
+ publishTimes,
36
+ readYamlList,
37
+ runDirectory,
38
+ splitSpec,
39
+ } from './lib.mjs'
40
+
41
+ const [mode, ...rest] = process.argv.slice(2)
42
+ /** @type {string[]} */
43
+ const positional = []
44
+ /** @type {Record<string, string>} */
45
+ const options = {}
46
+ for (let index = 0; index < rest.length; index++) {
47
+ const argument = rest[index] ?? ''
48
+ if (argument.startsWith('--')) {
49
+ options[argument.slice(2)] = rest[++index] ?? ''
50
+ } else {
51
+ positional.push(argument)
52
+ }
53
+ }
54
+
55
+ const runStart = options['run-start'] === undefined ? Date.now() : Date.parse(options['run-start'])
56
+ if (Number.isNaN(runStart)) {
57
+ throw new TypeError(`Invalid --run-start value: ${options['run-start']}`)
58
+ }
59
+
60
+ const authoredPath = options.authored ?? join(runDirectory(), 'authored-packages.txt')
61
+ if (!existsSync(authoredPath)) {
62
+ throw new Error(
63
+ `Authored package list not found at ${authoredPath}. Run Setup step 5 or pass --authored.`,
64
+ )
65
+ }
66
+
67
+ const LIST_SEPARATOR = /[\s,]+/v
68
+ const RANGE_CHARACTERS = /[\s*<=>\|^~]/v
69
+ const authored = new Set(
70
+ readFileSync(authoredPath, 'utf8')
71
+ .split(LIST_SEPARATOR)
72
+ .filter((name) => name !== ''),
73
+ )
74
+
75
+ /**
76
+ * @typedef {object} Age
77
+ * @property {boolean} authored - Whether kitschpatrol owns the package.
78
+ * @property {string} [installableAt] - When the version turns 24 hours old.
79
+ * @property {string} [published] - Registry publish time.
80
+ * @property {boolean} [young] - Under 24 hours old at the run start; undefined
81
+ * when the publish time is unknown.
82
+ */
83
+
84
+ /**
85
+ * Looks up a version's publish time and derived maturity facts.
86
+ *
87
+ * @param {string} name - Package name.
88
+ * @param {string} version - Exact version.
89
+ *
90
+ * @returns {Promise<Age>} The version's age facts.
91
+ */
92
+ async function ageOf(name, version) {
93
+ const times = await publishTimes(name)
94
+ const published = times[version]
95
+ if (published === undefined) {
96
+ return { authored: authored.has(name) }
97
+ }
98
+
99
+ const publishedMs = Date.parse(published)
100
+ return {
101
+ authored: authored.has(name),
102
+ installableAt: new Date(publishedMs + MATURITY_MS).toISOString(),
103
+ published,
104
+ young: publishedMs > runStart - MATURITY_MS,
105
+ }
106
+ }
107
+
108
+ if (mode === 'inventory') {
109
+ const locked = lockedPackages(options.lockfile ?? 'pnpm-lock.yaml')
110
+ const lockedNames = Array.from(locked, (key) => splitSpec(key))
111
+ const entries = readYamlList(
112
+ options.workspace ?? 'pnpm-workspace.yaml',
113
+ 'minimumReleaseAgeExclude',
114
+ )
115
+ const rows = await mapLimit(entries, 8, async (entry) => {
116
+ const spec = splitSpec(entry)
117
+ if (spec === undefined || RANGE_CHARACTERS.test(spec.version) || spec.version === '') {
118
+ const name = spec?.name ?? entry
119
+ return {
120
+ action: 'broad entry: rewrite as exact versions resolved from the lockfile',
121
+ entry,
122
+ lockedVersions: lockedNames
123
+ .filter((part) => part?.name === name)
124
+ .map((part) => part?.version),
125
+ }
126
+ }
127
+
128
+ const age = await ageOf(spec.name, spec.version)
129
+ const isLocked = locked.has(entry)
130
+ /** @type {string} */
131
+ let action
132
+ if (age.published === undefined) {
133
+ action = 'unknown version: check by hand'
134
+ } else if (!age.young) {
135
+ action = 'remove: at least 24 hours old'
136
+ } else if (age.authored) {
137
+ action = 'keep if installed or an accepted target'
138
+ } else if (isLocked) {
139
+ action = `keep only to avoid a downgrade; removable after ${age.installableAt}`
140
+ } else {
141
+ action = 'remove: young third-party version that is not locked'
142
+ }
143
+
144
+ return { action, entry, locked: isLocked, ...age }
145
+ })
146
+ process.stdout.write(`${JSON.stringify(rows, undefined, 2)}\n`)
147
+ for (const row of rows) {
148
+ process.stderr.write(`${row.entry}: ${row.action}\n`)
149
+ }
150
+ } else if (mode === 'new-versions') {
151
+ const [beforePath, afterPath = 'pnpm-lock.yaml'] = positional
152
+ if (beforePath === undefined) {
153
+ throw new Error('Usage: node release-age.mjs new-versions <before-lock> [after-lock]')
154
+ }
155
+
156
+ const before = lockedPackages(beforePath)
157
+ const added = [...lockedPackages(afterPath).difference(before)]
158
+ const rows = await mapLimit(added, 8, async (key) => {
159
+ const spec = splitSpec(key)
160
+ const age = spec === undefined ? { authored: false } : await ageOf(spec.name, spec.version)
161
+ return { key, ...age }
162
+ })
163
+ const flagged = rows.filter((row) => row.young !== false)
164
+ process.stdout.write(`${JSON.stringify(flagged, undefined, 2)}\n`)
165
+ process.stderr.write(`${added.length} new versions, ${flagged.length} young or unknown\n`)
166
+ for (const row of flagged) {
167
+ const kind = row.authored ? 'authored' : 'third-party'
168
+ const when =
169
+ row.installableAt === undefined ? 'publish time unknown' : `installable ${row.installableAt}`
170
+ process.stderr.write(` ${row.key} (${kind}): ${when}\n`)
171
+ }
172
+ } else {
173
+ throw new Error('Usage: node release-age.mjs <inventory|new-versions> [options]')
174
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Runs a command with an exact Node.js release first in PATH, for the floor
3
+ * checks in verification.md, and proves which binary every process used.
4
+ *
5
+ * The release is downloaded once into `<run directory>/node-<version>/` through
6
+ * a `devEngines.runtime` fixture, so pnpm verifies the checksum and the user's
7
+ * default runtime is untouched. A logging `node` shim placed first in PATH
8
+ * records each invocation's version and arguments, so child processes that
9
+ * resolve `node` from PATH, such as package bin shims, are covered too. A child
10
+ * that hard-codes another binary does not appear in the log; treat a
11
+ * suspiciously low invocation count as unverified.
12
+ *
13
+ * Usage:
14
+ *
15
+ * ```
16
+ * node run-at-node.mjs <version> [--cwd <dir>] [--out <dir>] -- <command> [args...]
17
+ * ```
18
+ *
19
+ * Writes `stdout.txt`, `stderr.txt`, `exit-code.txt`, and
20
+ * `node-invocations.log` to `--out` (default `<run
21
+ * directory>/node-<version>/last-run/`), prints a summary, and exits with the
22
+ * command's exit code. The shim needs a POSIX shell, so this does not work on
23
+ * Windows.
24
+ */
25
+ import { execFileSync, spawnSync } from 'node:child_process'
26
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
27
+ import { delimiter, dirname, join, resolve } from 'node:path'
28
+ import process from 'node:process'
29
+ import { runDirectory } from './lib.mjs'
30
+
31
+ const separator = process.argv.indexOf('--')
32
+ const head = process.argv.slice(2, separator === -1 ? undefined : separator)
33
+ const command = separator === -1 ? [] : process.argv.slice(separator + 1)
34
+ const version = head.shift()
35
+ if (version === undefined || !/^\d+\.\d+\.\d+$/v.test(version) || command.length === 0) {
36
+ throw new Error(
37
+ 'Usage: node run-at-node.mjs <x.y.z> [--cwd <dir>] [--out <dir>] -- <command> [args...]',
38
+ )
39
+ }
40
+
41
+ /** @type {Record<string, string>} */
42
+ const options = {}
43
+ for (let index = 0; index < head.length; index += 2) {
44
+ options[(head[index] ?? '').replace(/^--/v, '')] = head[index + 1] ?? ''
45
+ }
46
+
47
+ const runtimeDirectory = join(runDirectory(), `node-${version}`)
48
+ mkdirSync(runtimeDirectory, { recursive: true })
49
+ writeFileSync(
50
+ join(runtimeDirectory, 'package.json'),
51
+ `${JSON.stringify({ devEngines: { runtime: { name: 'node', onFail: 'download', version } }, name: `node-runtime-${version}`, private: true }, undefined, 2)}\n`,
52
+ )
53
+ execFileSync('pnpm', ['install'], { cwd: runtimeDirectory, stdio: ['ignore', 'ignore', 'inherit'] })
54
+ const nodeBinary = execFileSync('pnpm', ['exec', 'node', '-p', 'process.execPath'], {
55
+ cwd: runtimeDirectory,
56
+ encoding: 'utf8',
57
+ }).trim()
58
+ const reported = execFileSync(nodeBinary, ['--version'], { encoding: 'utf8' }).trim()
59
+ if (reported !== `v${version}`) {
60
+ throw new Error(`Expected Node.js v${version} at ${nodeBinary}, got ${reported}`)
61
+ }
62
+
63
+ const out = resolve(options.out ?? join(runtimeDirectory, 'last-run'))
64
+ const shimDirectory = join(out, 'shim')
65
+ const invocations = join(out, 'node-invocations.log')
66
+ mkdirSync(shimDirectory, { recursive: true })
67
+ writeFileSync(invocations, '')
68
+ const shim = join(shimDirectory, 'node')
69
+ writeFileSync(
70
+ shim,
71
+ [
72
+ '#!/bin/sh',
73
+ String.raw`printf '%s %s\n' '${reported}' "$*" | cut -c1-300 >> '${invocations}'`,
74
+ `exec '${nodeBinary}' "$@"`,
75
+ '',
76
+ ].join('\n'),
77
+ )
78
+ chmodSync(shim, 0o755)
79
+
80
+ const [executable = '', ...args] = command
81
+ const childEnvironment = {
82
+ ...process.env,
83
+ // eslint-disable-next-line ts/naming-convention -- Environment variable names are uppercase.
84
+ PATH: [shimDirectory, dirname(nodeBinary), process.env.PATH ?? ''].join(delimiter),
85
+ }
86
+ const result = spawnSync(executable, args, {
87
+ cwd: options.cwd === undefined ? process.cwd() : resolve(options.cwd),
88
+ encoding: 'utf8',
89
+ env: childEnvironment,
90
+ maxBuffer: 1024 ** 3,
91
+ })
92
+ writeFileSync(join(out, 'stdout.txt'), result.stdout)
93
+ writeFileSync(join(out, 'stderr.txt'), result.stderr)
94
+ const exitCode = result.status ?? 1
95
+ writeFileSync(join(out, 'exit-code.txt'), `${exitCode}\n`)
96
+
97
+ const lines = existsSync(invocations)
98
+ ? readFileSync(invocations, 'utf8').split('\n').filter(Boolean)
99
+ : []
100
+ const versions = [...new Set(lines.map((line) => line.split(' ', 1)[0]))]
101
+ process.stdout.write(
102
+ `${JSON.stringify({ exitCode, invocations: lines.length, nodeBinary, out, versions }, undefined, 2)}\n`,
103
+ )
104
+ if (result.error !== undefined) {
105
+ throw result.error
106
+ }
107
+
108
+ process.exitCode = exitCode