@doccov/api 0.2.1 → 0.2.3
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 +18 -0
- package/api/scan-stream.ts +205 -97
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# @doccov/api
|
|
2
2
|
|
|
3
|
+
## 0.2.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies
|
|
8
|
+
- @openpkg-ts/spec@0.4.0
|
|
9
|
+
|
|
10
|
+
## 0.2.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- feat(cli): add markdown docs impact detection to diff command
|
|
15
|
+
refactor(cli): consolidate fix functionality into check command
|
|
16
|
+
refactor(sdk): reuse detection extraction
|
|
17
|
+
fix(api): bug in api scan
|
|
18
|
+
fix(api): monorepo detection in scan
|
|
19
|
+
fix(api): improve scan-stream reliability and ref support
|
|
20
|
+
|
|
3
21
|
## 0.2.1
|
|
4
22
|
|
|
5
23
|
### Patch Changes
|
package/api/scan-stream.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Writable } from 'node:stream';
|
|
2
2
|
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
3
3
|
import { Sandbox } from '@vercel/sandbox';
|
|
4
|
+
import {
|
|
5
|
+
SandboxFileSystem,
|
|
6
|
+
detectBuildInfo,
|
|
7
|
+
detectMonorepo,
|
|
8
|
+
detectPackageManager,
|
|
9
|
+
getInstallCommand,
|
|
10
|
+
getPrimaryBuildScript,
|
|
11
|
+
} from '@doccov/sdk';
|
|
4
12
|
|
|
5
13
|
export const config = {
|
|
6
14
|
runtime: 'nodejs',
|
|
@@ -13,6 +21,7 @@ interface JobEvent {
|
|
|
13
21
|
message?: string;
|
|
14
22
|
progress?: number;
|
|
15
23
|
result?: ScanResult;
|
|
24
|
+
availablePackages?: string[];
|
|
16
25
|
}
|
|
17
26
|
|
|
18
27
|
interface ScanResult {
|
|
@@ -120,6 +129,42 @@ async function runScanWithProgress(
|
|
|
120
129
|
});
|
|
121
130
|
|
|
122
131
|
try {
|
|
132
|
+
// Create filesystem abstraction for SDK detection functions
|
|
133
|
+
const fs = new SandboxFileSystem(sandbox);
|
|
134
|
+
|
|
135
|
+
// Checkout specific ref if not main/master
|
|
136
|
+
if (options.ref && options.ref !== 'main' && options.ref !== 'master') {
|
|
137
|
+
sendEvent({
|
|
138
|
+
type: 'progress',
|
|
139
|
+
stage: 'cloning',
|
|
140
|
+
message: `Checking out ${options.ref}...`,
|
|
141
|
+
progress: 7,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const checkoutCapture = createCaptureStream();
|
|
145
|
+
const checkoutResult = await sandbox.runCommand({
|
|
146
|
+
cmd: 'git',
|
|
147
|
+
args: ['checkout', options.ref],
|
|
148
|
+
stdout: checkoutCapture.stream,
|
|
149
|
+
stderr: checkoutCapture.stream,
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
if (checkoutResult.exitCode !== 0) {
|
|
153
|
+
// Try fetching the ref first (might be a tag not fetched by shallow clone)
|
|
154
|
+
await sandbox.runCommand({
|
|
155
|
+
cmd: 'git',
|
|
156
|
+
args: ['fetch', '--depth', '1', 'origin', `refs/tags/${options.ref}:refs/tags/${options.ref}`],
|
|
157
|
+
});
|
|
158
|
+
const retryResult = await sandbox.runCommand({
|
|
159
|
+
cmd: 'git',
|
|
160
|
+
args: ['checkout', options.ref],
|
|
161
|
+
});
|
|
162
|
+
if (retryResult.exitCode !== 0) {
|
|
163
|
+
throw new Error(`Failed to checkout ${options.ref}: ${checkoutCapture.getOutput()}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
sendEvent({
|
|
124
169
|
type: 'progress',
|
|
125
170
|
stage: 'detecting',
|
|
@@ -127,57 +172,57 @@ async function runScanWithProgress(
|
|
|
127
172
|
progress: 10,
|
|
128
173
|
});
|
|
129
174
|
|
|
130
|
-
// Detect package manager
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
cmd: 'ls',
|
|
134
|
-
args: ['-1'],
|
|
135
|
-
stdout: lsCapture.stream,
|
|
136
|
-
});
|
|
137
|
-
const files = lsCapture.getOutput();
|
|
138
|
-
|
|
139
|
-
let installCmd: string;
|
|
140
|
-
let installArgs: string[];
|
|
141
|
-
let pm: 'pnpm' | 'bun' | 'npm' = 'npm';
|
|
142
|
-
let pmMessage = 'Detected npm project';
|
|
143
|
-
|
|
144
|
-
if (files.includes('pnpm-lock.yaml')) {
|
|
145
|
-
pm = 'pnpm';
|
|
146
|
-
installCmd = 'pnpm';
|
|
147
|
-
installArgs = ['install', '--frozen-lockfile'];
|
|
148
|
-
pmMessage = 'Detected pnpm monorepo';
|
|
149
|
-
} else if (files.includes('bun.lock') || files.includes('bun.lockb')) {
|
|
150
|
-
pm = 'bun';
|
|
151
|
-
installCmd = 'bun';
|
|
152
|
-
installArgs = ['install', '--frozen-lockfile'];
|
|
153
|
-
pmMessage = 'Detected bun project';
|
|
154
|
-
} else {
|
|
155
|
-
installCmd = 'npm';
|
|
156
|
-
installArgs = ['install', '--ignore-scripts', '--legacy-peer-deps'];
|
|
157
|
-
}
|
|
158
|
-
|
|
175
|
+
// Detect package manager using SDK
|
|
176
|
+
const pmInfo = await detectPackageManager(fs);
|
|
177
|
+
const pmMessage = pmInfo.lockfile ? `Detected ${pmInfo.name} project` : 'No lockfile detected';
|
|
159
178
|
sendEvent({ type: 'progress', stage: 'detecting', message: pmMessage, progress: 15 });
|
|
160
179
|
|
|
161
|
-
//
|
|
162
|
-
if (
|
|
180
|
+
// Early monorepo detection - fail fast if monorepo without package param
|
|
181
|
+
if (!options.package) {
|
|
182
|
+
const mono = await detectMonorepo(fs);
|
|
183
|
+
|
|
184
|
+
if (mono.isMonorepo) {
|
|
185
|
+
sendEvent({
|
|
186
|
+
type: 'progress',
|
|
187
|
+
stage: 'detecting',
|
|
188
|
+
message: 'Monorepo detected, listing packages...',
|
|
189
|
+
progress: 17,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const availablePackages = mono.packages
|
|
193
|
+
.filter((p) => !p.private)
|
|
194
|
+
.map((p) => p.name);
|
|
195
|
+
|
|
196
|
+
await sandbox.stop();
|
|
197
|
+
sendEvent({
|
|
198
|
+
type: 'error',
|
|
199
|
+
message: `Monorepo detected. Please specify a package to analyze using the 'package' query parameter.`,
|
|
200
|
+
availablePackages,
|
|
201
|
+
});
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Install package manager if needed (npm and pnpm are pre-installed in node22)
|
|
207
|
+
if (pmInfo.name === 'bun') {
|
|
163
208
|
sendEvent({
|
|
164
209
|
type: 'progress',
|
|
165
210
|
stage: 'installing',
|
|
166
|
-
message: 'Installing
|
|
211
|
+
message: 'Installing bun...',
|
|
167
212
|
progress: 18,
|
|
168
213
|
});
|
|
169
|
-
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', '
|
|
170
|
-
} else if (
|
|
214
|
+
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'bun'] });
|
|
215
|
+
} else if (pmInfo.name === 'yarn') {
|
|
171
216
|
sendEvent({
|
|
172
217
|
type: 'progress',
|
|
173
218
|
stage: 'installing',
|
|
174
|
-
message: 'Installing
|
|
219
|
+
message: 'Installing yarn...',
|
|
175
220
|
progress: 18,
|
|
176
221
|
});
|
|
177
|
-
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', '
|
|
222
|
+
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'yarn'] });
|
|
178
223
|
}
|
|
179
224
|
|
|
180
|
-
// Install dependencies
|
|
225
|
+
// Install dependencies with fallback chain
|
|
181
226
|
sendEvent({
|
|
182
227
|
type: 'progress',
|
|
183
228
|
stage: 'installing',
|
|
@@ -185,60 +230,99 @@ async function runScanWithProgress(
|
|
|
185
230
|
progress: 20,
|
|
186
231
|
});
|
|
187
232
|
|
|
233
|
+
let installed = false;
|
|
234
|
+
let activePm = pmInfo.name;
|
|
188
235
|
const installCapture = createCaptureStream();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
236
|
+
|
|
237
|
+
// Try primary package manager using SDK's getInstallCommand
|
|
238
|
+
const primaryCmd = getInstallCommand(pmInfo);
|
|
239
|
+
const primaryResult = await sandbox.runCommand({
|
|
240
|
+
cmd: primaryCmd[0],
|
|
241
|
+
args: primaryCmd.slice(1),
|
|
192
242
|
stdout: installCapture.stream,
|
|
193
243
|
stderr: installCapture.stream,
|
|
194
244
|
});
|
|
195
245
|
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
sendEvent({
|
|
201
|
-
type: 'progress',
|
|
202
|
-
stage: 'installing',
|
|
203
|
-
message: 'Dependencies installed',
|
|
204
|
-
progress: 40,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Check for build script
|
|
208
|
-
const pkgCapture = createCaptureStream();
|
|
209
|
-
await sandbox.runCommand({
|
|
210
|
-
cmd: 'cat',
|
|
211
|
-
args: ['package.json'],
|
|
212
|
-
stdout: pkgCapture.stream,
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
const pkgJson = JSON.parse(pkgCapture.getOutput()) as { scripts?: Record<string, string> };
|
|
217
|
-
const scripts = pkgJson.scripts ?? {};
|
|
218
|
-
const buildScript = scripts.build ? 'build' : scripts.compile ? 'compile' : null;
|
|
246
|
+
if (primaryResult.exitCode === 0) {
|
|
247
|
+
installed = true;
|
|
248
|
+
} else {
|
|
249
|
+
const errorOutput = installCapture.getOutput();
|
|
219
250
|
|
|
220
|
-
if
|
|
251
|
+
// Check if it's a workspace:* protocol error - try bun fallback
|
|
252
|
+
if (errorOutput.includes('workspace:') || errorOutput.includes('EUNSUPPORTEDPROTOCOL')) {
|
|
221
253
|
sendEvent({
|
|
222
254
|
type: 'progress',
|
|
223
|
-
stage: '
|
|
224
|
-
message: '
|
|
225
|
-
progress:
|
|
255
|
+
stage: 'installing',
|
|
256
|
+
message: 'Trying bun fallback for workspace protocol...',
|
|
257
|
+
progress: 25,
|
|
226
258
|
});
|
|
227
259
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
cmd:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
260
|
+
// Install bun if not already the primary
|
|
261
|
+
if (pmInfo.name !== 'bun') {
|
|
262
|
+
await sandbox.runCommand({ cmd: 'npm', args: ['install', '-g', 'bun'] });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const bunCapture = createCaptureStream();
|
|
266
|
+
const bunResult = await sandbox.runCommand({
|
|
267
|
+
cmd: 'bun',
|
|
268
|
+
args: ['install'],
|
|
269
|
+
stdout: bunCapture.stream,
|
|
270
|
+
stderr: bunCapture.stream,
|
|
234
271
|
});
|
|
235
272
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
273
|
+
if (bunResult.exitCode === 0) {
|
|
274
|
+
installed = true;
|
|
275
|
+
activePm = 'bun'; // Update pm for build step
|
|
276
|
+
}
|
|
239
277
|
}
|
|
240
|
-
}
|
|
241
|
-
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (installed) {
|
|
281
|
+
sendEvent({
|
|
282
|
+
type: 'progress',
|
|
283
|
+
stage: 'installing',
|
|
284
|
+
message: 'Dependencies installed',
|
|
285
|
+
progress: 40,
|
|
286
|
+
});
|
|
287
|
+
} else {
|
|
288
|
+
// Graceful degradation - continue with limited analysis
|
|
289
|
+
sendEvent({
|
|
290
|
+
type: 'progress',
|
|
291
|
+
stage: 'installing',
|
|
292
|
+
message: 'Install failed (continuing with limited analysis)',
|
|
293
|
+
progress: 40,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Check for build script using SDK
|
|
298
|
+
const buildInfo = await detectBuildInfo(fs);
|
|
299
|
+
const buildScript = getPrimaryBuildScript(buildInfo);
|
|
300
|
+
|
|
301
|
+
if (buildScript) {
|
|
302
|
+
sendEvent({
|
|
303
|
+
type: 'progress',
|
|
304
|
+
stage: 'building',
|
|
305
|
+
message: 'Running build...',
|
|
306
|
+
progress: 45,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
const buildCapture = createCaptureStream();
|
|
310
|
+
// Use activePm (may have changed to bun as fallback)
|
|
311
|
+
const buildCmd =
|
|
312
|
+
activePm === 'npm' || activePm === 'yarn'
|
|
313
|
+
? [activePm, 'run', buildScript]
|
|
314
|
+
: [activePm, buildScript];
|
|
315
|
+
|
|
316
|
+
const buildResult = await sandbox.runCommand({
|
|
317
|
+
cmd: buildCmd[0],
|
|
318
|
+
args: buildCmd.slice(1),
|
|
319
|
+
stdout: buildCapture.stream,
|
|
320
|
+
stderr: buildCapture.stream,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
const buildMessage =
|
|
324
|
+
buildResult.exitCode === 0 ? 'Build complete' : 'Build failed (continuing)';
|
|
325
|
+
sendEvent({ type: 'progress', stage: 'building', message: buildMessage, progress: 55 });
|
|
242
326
|
}
|
|
243
327
|
|
|
244
328
|
// Install doccov CLI
|
|
@@ -290,30 +374,54 @@ async function runScanWithProgress(
|
|
|
290
374
|
progress: 85,
|
|
291
375
|
});
|
|
292
376
|
|
|
293
|
-
//
|
|
377
|
+
// Check if spec file was created
|
|
378
|
+
const checkFileCapture = createCaptureStream();
|
|
379
|
+
await sandbox.runCommand({
|
|
380
|
+
cmd: 'cat',
|
|
381
|
+
args: [specFile],
|
|
382
|
+
stdout: checkFileCapture.stream,
|
|
383
|
+
stderr: checkFileCapture.stream,
|
|
384
|
+
});
|
|
385
|
+
const specContent = checkFileCapture.getOutput();
|
|
386
|
+
|
|
387
|
+
if (!specContent.trim() || specContent.includes('No such file')) {
|
|
388
|
+
throw new Error(`Spec file not found or empty. Generate output: ${genOutput.slice(-500)}`);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Extract summary with error handling
|
|
294
392
|
const extractScript = `
|
|
295
393
|
const fs = require('fs');
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const docs = exp.docs;
|
|
301
|
-
if (!docs) continue;
|
|
302
|
-
if ((docs.missing?.length || 0) > 0 || (docs.coverageScore || 0) < 100) {
|
|
303
|
-
undocumented.push(exp.name);
|
|
394
|
+
try {
|
|
395
|
+
if (!fs.existsSync('${specFile}')) {
|
|
396
|
+
console.error('Spec file not found: ${specFile}');
|
|
397
|
+
process.exit(1);
|
|
304
398
|
}
|
|
305
|
-
|
|
306
|
-
|
|
399
|
+
const content = fs.readFileSync('${specFile}', 'utf-8');
|
|
400
|
+
const spec = JSON.parse(content);
|
|
401
|
+
const undocumented = [];
|
|
402
|
+
const drift = [];
|
|
403
|
+
for (const exp of spec.exports || []) {
|
|
404
|
+
const docs = exp.docs;
|
|
405
|
+
if (!docs) continue;
|
|
406
|
+
if ((docs.missing?.length || 0) > 0 || (docs.coverageScore || 0) < 100) {
|
|
407
|
+
undocumented.push(exp.name);
|
|
408
|
+
}
|
|
409
|
+
for (const d of docs.drift || []) {
|
|
410
|
+
drift.push({ export: exp.name, type: d.type, issue: d.issue });
|
|
411
|
+
}
|
|
307
412
|
}
|
|
413
|
+
console.log(JSON.stringify({
|
|
414
|
+
coverage: spec.docs?.coverageScore || 0,
|
|
415
|
+
exportCount: spec.exports?.length || 0,
|
|
416
|
+
typeCount: spec.types?.length || 0,
|
|
417
|
+
undocumented: undocumented.slice(0, 50),
|
|
418
|
+
drift: drift.slice(0, 20),
|
|
419
|
+
driftCount: drift.length,
|
|
420
|
+
}));
|
|
421
|
+
} catch (e) {
|
|
422
|
+
console.error('Extract error:', e.message);
|
|
423
|
+
process.exit(1);
|
|
308
424
|
}
|
|
309
|
-
console.log(JSON.stringify({
|
|
310
|
-
coverage: spec.docs?.coverageScore || 0,
|
|
311
|
-
exportCount: spec.exports?.length || 0,
|
|
312
|
-
typeCount: spec.types?.length || 0,
|
|
313
|
-
undocumented: undocumented.slice(0, 50),
|
|
314
|
-
drift: drift.slice(0, 20),
|
|
315
|
-
driftCount: drift.length,
|
|
316
|
-
}));
|
|
317
425
|
`.replace(/\n/g, ' ');
|
|
318
426
|
|
|
319
427
|
const nodeCapture = createCaptureStream();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doccov/api",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "DocCov API - Badge endpoint and coverage services",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"doccov",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"format": "biome format --write src/"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@openpkg-ts/spec": "^0.
|
|
30
|
+
"@openpkg-ts/spec": "^0.4.0",
|
|
31
31
|
"@vercel/sandbox": "^1.0.3",
|
|
32
32
|
"hono": "^4.0.0",
|
|
33
33
|
"ms": "^2.1.3"
|