carrick 0.3.53
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/LICENSE.md +99 -0
- package/README.md +79 -0
- package/bin/carrick.mjs +168 -0
- package/dist/channel.d.ts +16 -0
- package/dist/channel.js +37 -0
- package/dist/channel.js.map +1 -0
- package/dist/cli.d.ts +29 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract.d.ts +154 -0
- package/dist/contract.js +141 -0
- package/dist/contract.js.map +1 -0
- package/dist/diagnostics.d.ts +54 -0
- package/dist/diagnostics.js +199 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/hook/post-edit.d.ts +13 -0
- package/dist/hook/post-edit.js +76 -0
- package/dist/hook/post-edit.js.map +1 -0
- package/dist/hook/session-start.d.ts +2 -0
- package/dist/hook/session-start.js +44 -0
- package/dist/hook/session-start.js.map +1 -0
- package/dist/init/identity.d.ts +20 -0
- package/dist/init/identity.js +60 -0
- package/dist/init/identity.js.map +1 -0
- package/dist/init/repos.d.ts +19 -0
- package/dist/init/repos.js +46 -0
- package/dist/init/repos.js.map +1 -0
- package/dist/init/run.d.ts +11 -0
- package/dist/init/run.js +228 -0
- package/dist/init/run.js.map +1 -0
- package/dist/init/settings.d.ts +50 -0
- package/dist/init/settings.js +138 -0
- package/dist/init/settings.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.js +35 -0
- package/dist/log.js.map +1 -0
- package/dist/native.d.ts +48 -0
- package/dist/native.js +127 -0
- package/dist/native.js.map +1 -0
- package/dist/render.d.ts +65 -0
- package/dist/render.js +334 -0
- package/dist/render.js.map +1 -0
- package/dist/root.d.ts +30 -0
- package/dist/root.js +82 -0
- package/dist/root.js.map +1 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +255 -0
- package/dist/server.js.map +1 -0
- package/dist/templates.d.ts +18 -0
- package/dist/templates.js +66 -0
- package/dist/templates.js.map +1 -0
- package/package.json +72 -0
- package/plugin/.claude-plugin/plugin.json +6 -0
- package/plugin/.lsp.json +14 -0
- package/plugin/hooks/hooks.json +27 -0
- package/sidecar/dist/src/bundler.d.ts +141 -0
- package/sidecar/dist/src/bundler.js +680 -0
- package/sidecar/dist/src/capture/anchors.d.ts +61 -0
- package/sidecar/dist/src/capture/anchors.js +1132 -0
- package/sidecar/dist/src/capture/api.d.ts +378 -0
- package/sidecar/dist/src/capture/api.js +10 -0
- package/sidecar/dist/src/capture/augmentations.d.ts +20 -0
- package/sidecar/dist/src/capture/augmentations.js +60 -0
- package/sidecar/dist/src/capture/check-classify.d.ts +58 -0
- package/sidecar/dist/src/capture/check-classify.js +189 -0
- package/sidecar/dist/src/capture/check-deep.d.ts +32 -0
- package/sidecar/dist/src/capture/check-deep.js +91 -0
- package/sidecar/dist/src/capture/check-poison.d.ts +40 -0
- package/sidecar/dist/src/capture/check-poison.js +155 -0
- package/sidecar/dist/src/capture/check-probe.d.ts +70 -0
- package/sidecar/dist/src/capture/check-probe.js +147 -0
- package/sidecar/dist/src/capture/check-scrub.d.ts +31 -0
- package/sidecar/dist/src/capture/check-scrub.js +71 -0
- package/sidecar/dist/src/capture/check-workspace.d.ts +52 -0
- package/sidecar/dist/src/capture/check-workspace.js +218 -0
- package/sidecar/dist/src/capture/check.d.ts +39 -0
- package/sidecar/dist/src/capture/check.js +450 -0
- package/sidecar/dist/src/capture/deep-walk.d.ts +62 -0
- package/sidecar/dist/src/capture/deep-walk.js +243 -0
- package/sidecar/dist/src/capture/index.d.ts +36 -0
- package/sidecar/dist/src/capture/index.js +477 -0
- package/sidecar/dist/src/capture/lockfile.d.ts +48 -0
- package/sidecar/dist/src/capture/lockfile.js +490 -0
- package/sidecar/dist/src/capture/machinery.d.ts +59 -0
- package/sidecar/dist/src/capture/machinery.js +160 -0
- package/sidecar/dist/src/capture/node-builder.d.ts +37 -0
- package/sidecar/dist/src/capture/node-builder.js +123 -0
- package/sidecar/dist/src/capture/paths-rewrite.d.ts +34 -0
- package/sidecar/dist/src/capture/paths-rewrite.js +100 -0
- package/sidecar/dist/src/capture/self-check.d.ts +38 -0
- package/sidecar/dist/src/capture/self-check.js +317 -0
- package/sidecar/dist/src/capture/specifiers.d.ts +39 -0
- package/sidecar/dist/src/capture/specifiers.js +56 -0
- package/sidecar/dist/src/definition-resolver.d.ts +57 -0
- package/sidecar/dist/src/definition-resolver.js +153 -0
- package/sidecar/dist/src/index.d.ts +14 -0
- package/sidecar/dist/src/index.js +564 -0
- package/sidecar/dist/src/monorepo-builder.d.ts +129 -0
- package/sidecar/dist/src/monorepo-builder.js +584 -0
- package/sidecar/dist/src/project-loader.d.ts +130 -0
- package/sidecar/dist/src/project-loader.js +399 -0
- package/sidecar/dist/src/type-inferrer.d.ts +940 -0
- package/sidecar/dist/src/type-inferrer.js +3540 -0
- package/sidecar/dist/src/type-structural-expander.d.ts +61 -0
- package/sidecar/dist/src/type-structural-expander.js +283 -0
- package/sidecar/dist/src/type-text-canonicalizer.d.ts +40 -0
- package/sidecar/dist/src/type-text-canonicalizer.js +297 -0
- package/sidecar/dist/src/types.d.ts +669 -0
- package/sidecar/dist/src/types.js +5 -0
- package/sidecar/dist/src/validators.d.ts +2214 -0
- package/sidecar/dist/src/validators.js +336 -0
- package/sidecar/package.json +6 -0
- package/templates/carrick.json +7 -0
- package/templates/carrick.yml +45 -0
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact-version dependency pins for the stub package, from two sources:
|
|
3
|
+
*
|
|
4
|
+
* 1. `installedVersions` — the repo's own node_modules, when installed.
|
|
5
|
+
* This is the version the repo actually resolves against, so it wins
|
|
6
|
+
* over the lockfile, and it covers repos whose lockfiles we do not
|
|
7
|
+
* parse at all (yarn classic v1, binary bun.lockb).
|
|
8
|
+
* 2. `lockfileVersions` — parsed lockfile fallback for bare checkouts
|
|
9
|
+
* (npm v1/v2/v3, pnpm v6/v9, yarn-berry v2+, and bun's text bun.lock
|
|
10
|
+
* in scope).
|
|
11
|
+
*
|
|
12
|
+
* Pin selection prefers the DIRECT (top-level) dependency version: a package
|
|
13
|
+
* present at multiple versions must pin to the version the emitted tree
|
|
14
|
+
* resolves against — the hoisted/direct install — never to whichever nested
|
|
15
|
+
* copy happens to appear first in the lockfile (npm sorts
|
|
16
|
+
* `node_modules/a/node_modules/b` before `node_modules/b`, so first-match
|
|
17
|
+
* picks a nested version the tree never sees). When a lockfile offers
|
|
18
|
+
* multiple versions and no direct install can be identified, the package
|
|
19
|
+
* stays UNPINNED (fail-closed abstain): a wrong pin fails or corrupts the
|
|
20
|
+
* synthetic-workspace typecheck, no pin merely abstains.
|
|
21
|
+
*/
|
|
22
|
+
import * as fs from 'node:fs';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
/** package-lock.json (v2/v3 `packages` map, v1 `dependencies` fallback). */
|
|
25
|
+
function npmLockfileVersions(lockPath) {
|
|
26
|
+
const versions = new Map();
|
|
27
|
+
const nested = new Map();
|
|
28
|
+
try {
|
|
29
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
30
|
+
if (lock.packages && typeof lock.packages === 'object') {
|
|
31
|
+
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
32
|
+
const idx = key.lastIndexOf('node_modules/');
|
|
33
|
+
if (idx === -1 || !entry?.version)
|
|
34
|
+
continue;
|
|
35
|
+
const name = key.slice(idx + 'node_modules/'.length);
|
|
36
|
+
// Direct dependency: the top-level install path, exactly
|
|
37
|
+
// `node_modules/<name>` (idx === 0 means no nesting prefix).
|
|
38
|
+
if (idx === 0) {
|
|
39
|
+
if (!versions.has(name))
|
|
40
|
+
versions.set(name, entry.version);
|
|
41
|
+
}
|
|
42
|
+
else if (!nested.has(name)) {
|
|
43
|
+
nested.set(name, entry.version);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// Nested copies only pin packages with no top-level install at all.
|
|
47
|
+
for (const [name, version] of nested) {
|
|
48
|
+
if (!versions.has(name))
|
|
49
|
+
versions.set(name, version);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (lock.dependencies && typeof lock.dependencies === 'object') {
|
|
53
|
+
for (const [name, entry] of Object.entries(lock.dependencies)) {
|
|
54
|
+
if (entry?.version)
|
|
55
|
+
versions.set(name, entry.version);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Unparseable lockfile: every external ends up in unpinned_externals.
|
|
61
|
+
}
|
|
62
|
+
return versions;
|
|
63
|
+
}
|
|
64
|
+
/** Strip a pnpm peer suffix / quoting from a resolved version value. */
|
|
65
|
+
function cleanPnpmVersion(raw) {
|
|
66
|
+
return raw.trim().replace(/^['"]|['"]$/g, '').split('(')[0].trim();
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* pnpm-lock.yaml. Two tiers, both via a targeted line parse (no YAML dep):
|
|
70
|
+
*
|
|
71
|
+
* 1. `importers:` — every importer's dependencies/devDependencies/
|
|
72
|
+
* optionalDependencies with their RESOLVED `version:` values. These are
|
|
73
|
+
* the direct (top-level) installs and win.
|
|
74
|
+
* 2. `packages:` — the flat closure. Keys look like `/zod@3.23.0:` (v6),
|
|
75
|
+
* `zod@3.23.0:` (v9), `/@scope/pkg@1.2.3(peer@x)…:`. Fallback for
|
|
76
|
+
* packages that are not a direct dependency of any importer; the
|
|
77
|
+
* packages map is name-sorted, so first-match here would otherwise pin
|
|
78
|
+
* the LOWEST version of a multi-version package.
|
|
79
|
+
*/
|
|
80
|
+
function pnpmLockfileVersions(lockPath) {
|
|
81
|
+
const direct = new Map();
|
|
82
|
+
const fallback = new Map();
|
|
83
|
+
try {
|
|
84
|
+
const text = fs.readFileSync(lockPath, 'utf8');
|
|
85
|
+
const lines = text.split('\n');
|
|
86
|
+
let inPackages = false;
|
|
87
|
+
let inImporters = false;
|
|
88
|
+
let inDepsSection = false;
|
|
89
|
+
let currentDep;
|
|
90
|
+
for (const line of lines) {
|
|
91
|
+
if (/^\S/.test(line)) {
|
|
92
|
+
// New top-level key.
|
|
93
|
+
inPackages = /^packages:\s*$/.test(line);
|
|
94
|
+
inImporters = /^importers:\s*$/.test(line);
|
|
95
|
+
inDepsSection = false;
|
|
96
|
+
currentDep = undefined;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (inImporters) {
|
|
100
|
+
// ` <importer>:` (2-space) resets; ` dependencies:` (4-space)
|
|
101
|
+
// opens a deps section; ` <name>:` (6-space) names a dep;
|
|
102
|
+
// ` version: <v>` (8-space) resolves it.
|
|
103
|
+
if (/^ {2}\S/.test(line)) {
|
|
104
|
+
inDepsSection = false;
|
|
105
|
+
currentDep = undefined;
|
|
106
|
+
}
|
|
107
|
+
else if (/^ {4}\S/.test(line)) {
|
|
108
|
+
inDepsSection = /^ {4}(dependencies|devDependencies|optionalDependencies):\s*$/.test(line);
|
|
109
|
+
currentDep = undefined;
|
|
110
|
+
}
|
|
111
|
+
else if (inDepsSection && /^ {6}\S/.test(line)) {
|
|
112
|
+
const m = /^ {6}['"]?([^'":]+)['"]?:\s*(\S.*)?$/.exec(line);
|
|
113
|
+
currentDep = m?.[1];
|
|
114
|
+
// Old inline form: ` zod: 3.23.8`.
|
|
115
|
+
if (m?.[1] && m[2] && !direct.has(m[1])) {
|
|
116
|
+
const v = cleanPnpmVersion(m[2]);
|
|
117
|
+
if (/^\d/.test(v))
|
|
118
|
+
direct.set(m[1], v);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
else if (inDepsSection && currentDep && /^ {8}version:/.test(line)) {
|
|
122
|
+
const v = cleanPnpmVersion(line.slice(line.indexOf(':') + 1));
|
|
123
|
+
if (v && !direct.has(currentDep) && /^\d/.test(v))
|
|
124
|
+
direct.set(currentDep, v);
|
|
125
|
+
currentDep = undefined;
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (inPackages) {
|
|
130
|
+
// Two-space-indented keys only (package entries), e.g.
|
|
131
|
+
// /zod@3.23.0: | 'zod@3.23.0': | /@scope/pkg@1.2.3(peer@1.0.0):
|
|
132
|
+
const m = /^ {2}['"]?\/?((?:@[^/@'"]+\/)?[^/@'"]+)@([^('":]+)/.exec(line);
|
|
133
|
+
if (!m)
|
|
134
|
+
continue;
|
|
135
|
+
const [, name, version] = m;
|
|
136
|
+
if (!fallback.has(name))
|
|
137
|
+
fallback.set(name, version.trim());
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Unparseable lockfile: every external ends up in unpinned_externals.
|
|
143
|
+
}
|
|
144
|
+
const versions = new Map(direct);
|
|
145
|
+
for (const [name, version] of fallback) {
|
|
146
|
+
if (!versions.has(name))
|
|
147
|
+
versions.set(name, version);
|
|
148
|
+
}
|
|
149
|
+
return versions;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Published-semver gate for node_modules pins. Rejects anything a registry
|
|
153
|
+
* install could not satisfy: workspace/link protocol leakage
|
|
154
|
+
* (`workspace:*`, `link:...`) and yarn-berry's `0.0.0-use.local` local
|
|
155
|
+
* sentinel. A rejected version means "unpinned" (fail-closed abstain
|
|
156
|
+
* downstream), never a pin that would fail the synthetic-workspace install.
|
|
157
|
+
*/
|
|
158
|
+
function isPublishedSemver(version) {
|
|
159
|
+
return (/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version) &&
|
|
160
|
+
!version.startsWith('0.0.0-use.'));
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Direct-dependency ranges declared by the manifest sitting next to a
|
|
164
|
+
* lockfile: the workspace root's dependencies/devDependencies/
|
|
165
|
+
* optionalDependencies. Used to disambiguate multi-version packages in
|
|
166
|
+
* lockfiles that key entries by requested range (yarn-berry).
|
|
167
|
+
*/
|
|
168
|
+
function rootPackageJsonRanges(pkgJsonPath) {
|
|
169
|
+
const ranges = new Map();
|
|
170
|
+
try {
|
|
171
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
172
|
+
for (const section of ['dependencies', 'devDependencies', 'optionalDependencies']) {
|
|
173
|
+
const deps = pkg?.[section];
|
|
174
|
+
if (!deps || typeof deps !== 'object')
|
|
175
|
+
continue;
|
|
176
|
+
for (const [name, range] of Object.entries(deps)) {
|
|
177
|
+
if (typeof range === 'string' && !ranges.has(name))
|
|
178
|
+
ranges.set(name, range);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// No/unreadable root manifest: multi-version packages abstain.
|
|
184
|
+
}
|
|
185
|
+
return ranges;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* yarn-berry (v2+) yarn.lock. Detected by its `__metadata:` block; a yarn
|
|
189
|
+
* CLASSIC (v1) lockfile has no such block and a different entry syntax, so
|
|
190
|
+
* it bails to an empty map here (classic is not parsed — `installedVersions`
|
|
191
|
+
* covers installed classic checkouts).
|
|
192
|
+
*
|
|
193
|
+
* Entries key a comma-separated descriptor set on the resolved package:
|
|
194
|
+
*
|
|
195
|
+
* "ms@npm:2.0.0": → version: 2.0.0
|
|
196
|
+
* "ms@npm:^2.1.3": → version: 2.1.3
|
|
197
|
+
* "app@workspace:.": → version: 0.0.0-use.local
|
|
198
|
+
*
|
|
199
|
+
* Only `npm:` protocol descriptors are considered (`workspace:`, `patch:`,
|
|
200
|
+
* `portal:`, `link:`, git — none registry-installable as the bare name).
|
|
201
|
+
* npm-alias descriptors (`foo@npm:bar@^1.0.0`) resolve a DIFFERENT package
|
|
202
|
+
* than the install name, so they never pin either.
|
|
203
|
+
*
|
|
204
|
+
* Multi-version selection: the lockfile keys by requested range, not by
|
|
205
|
+
* install position, so the direct install is identified through the
|
|
206
|
+
* workspace-root package.json — the entry whose descriptor set contains
|
|
207
|
+
* `<name>@npm:<root-declared-range>` wins. Multiple versions and no direct
|
|
208
|
+
* match → unpinned (abstain).
|
|
209
|
+
*/
|
|
210
|
+
function yarnBerryLockfileVersions(lockPath) {
|
|
211
|
+
const versions = new Map();
|
|
212
|
+
try {
|
|
213
|
+
const text = fs.readFileSync(lockPath, 'utf8');
|
|
214
|
+
if (!/^__metadata:/m.test(text))
|
|
215
|
+
return versions;
|
|
216
|
+
// name → (requested range → resolved version), npm: descriptors only.
|
|
217
|
+
const byName = new Map();
|
|
218
|
+
let current = [];
|
|
219
|
+
for (const line of text.split('\n')) {
|
|
220
|
+
if (line.startsWith('#'))
|
|
221
|
+
continue;
|
|
222
|
+
const keyMatch = /^(\S.*):\s*$/.exec(line);
|
|
223
|
+
if (keyMatch) {
|
|
224
|
+
// New top-level entry: a descriptor set (or `__metadata`).
|
|
225
|
+
current = [];
|
|
226
|
+
if (keyMatch[1] === '__metadata')
|
|
227
|
+
continue;
|
|
228
|
+
for (const rawDesc of keyMatch[1].split(',')) {
|
|
229
|
+
const desc = rawDesc.trim().replace(/^['"]|['"]$/g, '');
|
|
230
|
+
const at = desc.indexOf('@', 1); // 1: skip a scope's leading @
|
|
231
|
+
if (at <= 0)
|
|
232
|
+
continue;
|
|
233
|
+
const name = desc.slice(0, at);
|
|
234
|
+
const protocolAndRange = desc.slice(at + 1);
|
|
235
|
+
if (!protocolAndRange.startsWith('npm:'))
|
|
236
|
+
continue;
|
|
237
|
+
const range = protocolAndRange.slice('npm:'.length);
|
|
238
|
+
if (range.includes('@'))
|
|
239
|
+
continue; // npm alias: foo@npm:bar@^1.0.0
|
|
240
|
+
current.push({ name, range });
|
|
241
|
+
}
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const versionMatch = /^ {2}version:\s*(.+?)\s*$/.exec(line);
|
|
245
|
+
if (versionMatch && current.length > 0) {
|
|
246
|
+
const version = versionMatch[1].replace(/^['"]|['"]$/g, '');
|
|
247
|
+
for (const { name, range } of current) {
|
|
248
|
+
let ranges = byName.get(name);
|
|
249
|
+
if (!ranges)
|
|
250
|
+
byName.set(name, (ranges = new Map()));
|
|
251
|
+
ranges.set(range, version);
|
|
252
|
+
}
|
|
253
|
+
current = [];
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
const rootRanges = rootPackageJsonRanges(path.join(path.dirname(lockPath), 'package.json'));
|
|
257
|
+
for (const [name, ranges] of byName) {
|
|
258
|
+
const distinct = new Set(ranges.values());
|
|
259
|
+
let resolved;
|
|
260
|
+
if (distinct.size === 1) {
|
|
261
|
+
resolved = distinct.values().next().value;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const declared = rootRanges.get(name);
|
|
265
|
+
if (declared !== undefined) {
|
|
266
|
+
const expected = declared.startsWith('npm:')
|
|
267
|
+
? declared.slice('npm:'.length)
|
|
268
|
+
: declared;
|
|
269
|
+
resolved = ranges.get(expected);
|
|
270
|
+
}
|
|
271
|
+
// No direct match among multiple versions: abstain.
|
|
272
|
+
}
|
|
273
|
+
if (resolved !== undefined && isPublishedSemver(resolved)) {
|
|
274
|
+
versions.set(name, resolved);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
// Unparseable lockfile: every external ends up in unpinned_externals.
|
|
280
|
+
}
|
|
281
|
+
return versions;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Strip JSONC affordances (line/block comments, trailing commas) so bun's
|
|
285
|
+
* text lockfile can be JSON.parsed. String-aware: `//` inside a string
|
|
286
|
+
* (registry URLs) is content, not a comment.
|
|
287
|
+
*/
|
|
288
|
+
function stripJsonc(text) {
|
|
289
|
+
let noComments = '';
|
|
290
|
+
let inString = false;
|
|
291
|
+
for (let i = 0; i < text.length; i++) {
|
|
292
|
+
const c = text[i];
|
|
293
|
+
if (inString) {
|
|
294
|
+
noComments += c;
|
|
295
|
+
if (c === '\\') {
|
|
296
|
+
noComments += text[i + 1] ?? '';
|
|
297
|
+
i++;
|
|
298
|
+
}
|
|
299
|
+
else if (c === '"') {
|
|
300
|
+
inString = false;
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (c === '"') {
|
|
305
|
+
inString = true;
|
|
306
|
+
noComments += c;
|
|
307
|
+
}
|
|
308
|
+
else if (c === '/' && text[i + 1] === '/') {
|
|
309
|
+
while (i < text.length && text[i] !== '\n')
|
|
310
|
+
i++;
|
|
311
|
+
i--;
|
|
312
|
+
}
|
|
313
|
+
else if (c === '/' && text[i + 1] === '*') {
|
|
314
|
+
i += 2;
|
|
315
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/'))
|
|
316
|
+
i++;
|
|
317
|
+
i++;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
noComments += c;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
let out = '';
|
|
324
|
+
inString = false;
|
|
325
|
+
for (let i = 0; i < noComments.length; i++) {
|
|
326
|
+
const c = noComments[i];
|
|
327
|
+
if (inString) {
|
|
328
|
+
out += c;
|
|
329
|
+
if (c === '\\') {
|
|
330
|
+
out += noComments[i + 1] ?? '';
|
|
331
|
+
i++;
|
|
332
|
+
}
|
|
333
|
+
else if (c === '"') {
|
|
334
|
+
inString = false;
|
|
335
|
+
}
|
|
336
|
+
continue;
|
|
337
|
+
}
|
|
338
|
+
if (c === '"') {
|
|
339
|
+
inString = true;
|
|
340
|
+
}
|
|
341
|
+
else if (c === ',') {
|
|
342
|
+
let j = i + 1;
|
|
343
|
+
while (j < noComments.length && /\s/.test(noComments[j]))
|
|
344
|
+
j++;
|
|
345
|
+
if (noComments[j] === '}' || noComments[j] === ']')
|
|
346
|
+
continue; // trailing
|
|
347
|
+
}
|
|
348
|
+
out += c;
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* bun's TEXT bun.lock (JSONC). The binary bun.lockb is out of scope. The
|
|
354
|
+
* `packages` map is keyed by hoisted install path — `"ms"` is the top-level
|
|
355
|
+
* install, `"debug/ms"` a copy nested under debug — with the resolution as
|
|
356
|
+
* the first tuple element (`"ms@2.1.3"`, `"@probe/member@workspace:…"`).
|
|
357
|
+
*
|
|
358
|
+
* Mirrors the npm parser's direct-first rule: the top-level key (key ===
|
|
359
|
+
* package name) is what the emitted tree resolves against and wins. A
|
|
360
|
+
* top-level entry that fails the published-semver gate (workspace member,
|
|
361
|
+
* git, tarball) blocks nested fallback entirely — the tree resolves to the
|
|
362
|
+
* unpinnable thing, so pinning a nested registry copy would be wrong.
|
|
363
|
+
* Nested-only packages pin solely when every nested copy agrees on one
|
|
364
|
+
* version; several distinct nested versions → unpinned (abstain). Alias
|
|
365
|
+
* installs (key name ≠ resolved package name) never pin.
|
|
366
|
+
*/
|
|
367
|
+
function bunLockfileVersions(lockPath) {
|
|
368
|
+
const versions = new Map();
|
|
369
|
+
try {
|
|
370
|
+
const lock = JSON.parse(stripJsonc(fs.readFileSync(lockPath, 'utf8')));
|
|
371
|
+
const packages = lock?.packages;
|
|
372
|
+
if (!packages || typeof packages !== 'object')
|
|
373
|
+
return versions;
|
|
374
|
+
const directNames = new Set();
|
|
375
|
+
const nested = new Map();
|
|
376
|
+
for (const [key, entry] of Object.entries(packages)) {
|
|
377
|
+
if (!Array.isArray(entry) || typeof entry[0] !== 'string')
|
|
378
|
+
continue;
|
|
379
|
+
const spec = entry[0]; // "<name>@<resolution>"
|
|
380
|
+
const at = spec.indexOf('@', 1); // 1: skip a scope's leading @
|
|
381
|
+
if (at <= 0)
|
|
382
|
+
continue;
|
|
383
|
+
const name = spec.slice(0, at);
|
|
384
|
+
const resolved = spec.slice(at + 1);
|
|
385
|
+
if (key === name) {
|
|
386
|
+
directNames.add(name);
|
|
387
|
+
if (isPublishedSemver(resolved))
|
|
388
|
+
versions.set(name, resolved);
|
|
389
|
+
}
|
|
390
|
+
else if (key.endsWith(`/${name}`)) {
|
|
391
|
+
let copies = nested.get(name);
|
|
392
|
+
if (!copies)
|
|
393
|
+
nested.set(name, (copies = new Set()));
|
|
394
|
+
copies.add(resolved);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
for (const [name, copies] of nested) {
|
|
398
|
+
if (directNames.has(name) || copies.size !== 1)
|
|
399
|
+
continue;
|
|
400
|
+
const version = copies.values().next().value;
|
|
401
|
+
if (version !== undefined && isPublishedSemver(version))
|
|
402
|
+
versions.set(name, version);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// Unparseable lockfile: every external ends up in unpinned_externals.
|
|
407
|
+
}
|
|
408
|
+
return versions;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Resolve exact versions from the repo's installed node_modules by reading
|
|
412
|
+
* `node_modules/<pkg>/package.json` for each referenced external. On an
|
|
413
|
+
* installed checkout this is ground truth — the versions the repo actually
|
|
414
|
+
* runs against — and it needs no lockfile parser, which is what covers the
|
|
415
|
+
* formats without one (yarn classic v1, binary bun.lockb).
|
|
416
|
+
*
|
|
417
|
+
* Workspace-member guard (fail-closed): package managers link workspace
|
|
418
|
+
* siblings into node_modules as symlinks (`node_modules/@ws/member ->
|
|
419
|
+
* ../../packages/member`). Those carry unpublished versions; pinning one
|
|
420
|
+
* would make the whole synthetic-workspace install fail at check time. So a
|
|
421
|
+
* pin is taken only when the entry's realpath still lives under a
|
|
422
|
+
* node_modules directory inside the repo. pnpm's own
|
|
423
|
+
* `node_modules/<pkg> -> node_modules/.pnpm/<pkg>@<v>/node_modules/<pkg>`
|
|
424
|
+
* links pass that test and stay pinnable; realpaths inside the repo but
|
|
425
|
+
* outside every node_modules (workspace members) and realpaths outside the
|
|
426
|
+
* repo entirely (npm/yarn link to a dev checkout) are skipped — the
|
|
427
|
+
* external stays unpinned unless the lockfile supplies a version.
|
|
428
|
+
*/
|
|
429
|
+
export function installedVersions(repoRoot, names) {
|
|
430
|
+
const versions = new Map();
|
|
431
|
+
const nodeModules = path.join(path.resolve(repoRoot), 'node_modules');
|
|
432
|
+
let realRepoRoot;
|
|
433
|
+
try {
|
|
434
|
+
realRepoRoot = fs.realpathSync(path.resolve(repoRoot));
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
return versions;
|
|
438
|
+
}
|
|
439
|
+
for (const name of names) {
|
|
440
|
+
// Package names come from bare import specifiers; anything with empty
|
|
441
|
+
// or dot segments cannot be a node_modules entry (and must not escape).
|
|
442
|
+
const segments = name.split('/');
|
|
443
|
+
if (segments.some((s) => s === '' || s === '.' || s === '..'))
|
|
444
|
+
continue;
|
|
445
|
+
const pkgDir = path.join(nodeModules, ...segments);
|
|
446
|
+
try {
|
|
447
|
+
const real = fs.realpathSync(pkgDir);
|
|
448
|
+
const rel = path.relative(realRepoRoot, real);
|
|
449
|
+
const inRepo = rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
450
|
+
if (!inRepo || !rel.split(path.sep).includes('node_modules'))
|
|
451
|
+
continue;
|
|
452
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8'));
|
|
453
|
+
if (typeof manifest?.version === 'string' && isPublishedSemver(manifest.version)) {
|
|
454
|
+
versions.set(name, manifest.version);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Missing dir, dangling symlink, unreadable or corrupt package.json:
|
|
459
|
+
// the external stays unpinned here (lockfile fallback or abstain).
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return versions;
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Resolve exact versions for the repo. Walks up from repoRoot so monorepo
|
|
466
|
+
* members with a hoisted root lockfile still pin (nearest lockfile wins;
|
|
467
|
+
* package-lock.json > pnpm-lock.yaml > yarn.lock > bun.lock at the same
|
|
468
|
+
* level).
|
|
469
|
+
*/
|
|
470
|
+
export function lockfileVersions(repoRoot) {
|
|
471
|
+
let dir = path.resolve(repoRoot);
|
|
472
|
+
for (;;) {
|
|
473
|
+
const npmLock = path.join(dir, 'package-lock.json');
|
|
474
|
+
if (fs.existsSync(npmLock))
|
|
475
|
+
return npmLockfileVersions(npmLock);
|
|
476
|
+
const pnpmLock = path.join(dir, 'pnpm-lock.yaml');
|
|
477
|
+
if (fs.existsSync(pnpmLock))
|
|
478
|
+
return pnpmLockfileVersions(pnpmLock);
|
|
479
|
+
const yarnLock = path.join(dir, 'yarn.lock');
|
|
480
|
+
if (fs.existsSync(yarnLock))
|
|
481
|
+
return yarnBerryLockfileVersions(yarnLock);
|
|
482
|
+
const bunLock = path.join(dir, 'bun.lock');
|
|
483
|
+
if (fs.existsSync(bunLock))
|
|
484
|
+
return bunLockfileVersions(bunLock);
|
|
485
|
+
const parent = path.dirname(dir);
|
|
486
|
+
if (parent === dir)
|
|
487
|
+
return new Map();
|
|
488
|
+
dir = parent;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-machinery detection for the v2 capture path (carrick#371).
|
|
3
|
+
*
|
|
4
|
+
* A producer response anchor whose resolved type IS or CONTAINS HTTP transport
|
|
5
|
+
* machinery — a fetch/DOM `Response`/`Request`, a Node `http.ServerResponse`, a
|
|
6
|
+
* wrapper envelope `{ response: Response; error?: undefined } | { ...; error }`,
|
|
7
|
+
* or a wrapper function `(req: Request) => Promise<Response>` — must never be
|
|
8
|
+
* emitted as a comparable contract. Comparing it against the consumer's real
|
|
9
|
+
* payload manufactures a false compat mismatch (the bug this module fixes).
|
|
10
|
+
*
|
|
11
|
+
* This is the raw-`ts` seam mirror of `type-inferrer.ts`'s
|
|
12
|
+
* `typeIsOrContainsResponseMachinery` (ts-morph): the capture/ seam forbids
|
|
13
|
+
* importing a module from outside it, so the canonical indicator set and the
|
|
14
|
+
* detection shape are duplicated here in lockstep — the same pattern by which
|
|
15
|
+
* `BUILTIN_ANCHOR_SYMBOLS` mirrors `socket_io.rs`. Detection is STRUCTURAL and
|
|
16
|
+
* framework-agnostic: no framework NAME appears, only the shared HTTP-message
|
|
17
|
+
* member surface, gated by a lib / `node_modules` declaration origin so a user
|
|
18
|
+
* payload that merely shares a member name can never trip it.
|
|
19
|
+
*/
|
|
20
|
+
import ts from 'typescript';
|
|
21
|
+
/**
|
|
22
|
+
* Strongly-discriminating member names of HTTP transport machinery. Kept
|
|
23
|
+
* identical to `MACHINERY_MEMBER_INDICATORS` in `type-inferrer.ts`. These are
|
|
24
|
+
* the names no JSON payload carries (`ok`, `redirected`, `bodyUsed`,
|
|
25
|
+
* `writeHead`, ...), so the origin gate + threshold never fire on real data.
|
|
26
|
+
* Exported so a drift-guard test (`machinery-indicator-mirror.test.ts`) asserts
|
|
27
|
+
* it stays equal to the `type-inferrer.ts` copy — nothing else enforces the
|
|
28
|
+
* lockstep, and if the two drift one path silently stops abstaining.
|
|
29
|
+
*/
|
|
30
|
+
export declare const MACHINERY_MEMBER_INDICATORS: Set<string>;
|
|
31
|
+
/**
|
|
32
|
+
* True when `type`, resolved against `node`, IS or CONTAINS framework
|
|
33
|
+
* machinery.
|
|
34
|
+
*
|
|
35
|
+
* DETECTS, exactly:
|
|
36
|
+
* 1. the type itself is machinery (`isFrameworkMachinery`);
|
|
37
|
+
* 2. a union/intersection member is machinery (the envelope union);
|
|
38
|
+
* 3. a DIRECT property whose type is machinery (`{ response: Response }`),
|
|
39
|
+
* ONE level of descent only;
|
|
40
|
+
* 4. a CALL SIGNATURE whose AWAITED return type is machinery — the wrapper
|
|
41
|
+
* function `(req) => Promise<Response>` the Infer fallback resolves to.
|
|
42
|
+
* (Only the call return is awaited; property types below are not.)
|
|
43
|
+
*
|
|
44
|
+
* DELIBERATELY NOT DETECTED — stated so this comment never overstates the
|
|
45
|
+
* guarantee (mirrors the same list in `type-inferrer.ts`). Each is a
|
|
46
|
+
* non-regression (pre-existing verdict unchanged, never a new wrong one), a
|
|
47
|
+
* tracked follow-up:
|
|
48
|
+
* - machinery nested deeper than one property level;
|
|
49
|
+
* - a PROPERTY typed `Promise<Response>` (property types are not awaited /
|
|
50
|
+
* Promise-unwrapped before the check — only call-signature returns are);
|
|
51
|
+
* - an array element type: `Response[]` is not descended to its element;
|
|
52
|
+
* - `interface X extends Response` declared in USER source — the origin gate
|
|
53
|
+
* is lib/`node_modules` only, so a user-declared subtype reads as a real
|
|
54
|
+
* contract.
|
|
55
|
+
*
|
|
56
|
+
* The depth cap + origin gate keep a legitimate payload that merely references a
|
|
57
|
+
* machinery type far inside from over-abstaining.
|
|
58
|
+
*/
|
|
59
|
+
export declare function typeIsOrContainsMachinery(checker: ts.TypeChecker, type: ts.Type, node: ts.Node): boolean;
|