@pyreon/cli 0.15.0 → 0.16.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/lib/analysis/index.js.html +1 -1
- package/lib/index.js +33 -3
- package/lib/types/index.d.ts +24 -0
- package/package.json +2 -2
- package/src/doctor.ts +62 -0
- package/src/index.ts +9 -1
- package/src/tests/doctor.test.ts +125 -0
|
@@ -5386,7 +5386,7 @@ var drawChart = (function (exports) {
|
|
|
5386
5386
|
</script>
|
|
5387
5387
|
<script>
|
|
5388
5388
|
/*<!--*/
|
|
5389
|
-
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"
|
|
5389
|
+
const data = {"version":2,"tree":{"name":"root","children":[{"name":"index.js","children":[{"name":"src","children":[{"uid":"599dd4cb-1","name":"context.ts"},{"uid":"599dd4cb-3","name":"doctor.ts"},{"uid":"599dd4cb-5","name":"index.ts"}]}]}],"isRoot":true},"nodeParts":{"599dd4cb-1":{"renderedLength":1323,"gzipLength":627,"brotliLength":0,"metaUid":"599dd4cb-0"},"599dd4cb-3":{"renderedLength":6054,"gzipLength":2002,"brotliLength":0,"metaUid":"599dd4cb-2"},"599dd4cb-5":{"renderedLength":2831,"gzipLength":1057,"brotliLength":0,"metaUid":"599dd4cb-4"}},"nodeMetas":{"599dd4cb-0":{"id":"/src/context.ts","moduleParts":{"index.js":"599dd4cb-1"},"imported":[{"uid":"599dd4cb-6"},{"uid":"599dd4cb-7"},{"uid":"599dd4cb-8"}],"importedBy":[{"uid":"599dd4cb-4"}]},"599dd4cb-2":{"id":"/src/doctor.ts","moduleParts":{"index.js":"599dd4cb-3"},"imported":[{"uid":"599dd4cb-6"},{"uid":"599dd4cb-7"},{"uid":"599dd4cb-8"}],"importedBy":[{"uid":"599dd4cb-4"}]},"599dd4cb-4":{"id":"/src/index.ts","moduleParts":{"index.js":"599dd4cb-5"},"imported":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}],"importedBy":[],"isEntry":true},"599dd4cb-6":{"id":"node:fs","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}]},"599dd4cb-7":{"id":"node:path","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}]},"599dd4cb-8":{"id":"@pyreon/compiler","moduleParts":{},"imported":[],"importedBy":[{"uid":"599dd4cb-0"},{"uid":"599dd4cb-2"}]}},"env":{"rollup":"4.23.0"},"options":{"gzip":true,"brotli":false,"sourcemap":false}};
|
|
5390
5390
|
|
|
5391
5391
|
const run = () => {
|
|
5392
5392
|
const width = window.innerWidth;
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { auditTestEnvironment, detectReactPatterns, formatTestAudit, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
|
|
4
|
+
import { auditIslands, auditSsg, auditTestEnvironment, detectReactPatterns, formatIslandAudit, formatSsgAudit, formatTestAudit, generateContext as generateContext$1, hasReactPatterns, migrateReactCode } from "@pyreon/compiler";
|
|
5
5
|
|
|
6
6
|
//#region src/context.ts
|
|
7
7
|
/**
|
|
@@ -66,6 +66,28 @@ async function doctor(options) {
|
|
|
66
66
|
console.log("");
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
|
+
if (options.checkIslands) {
|
|
70
|
+
const islandsResult = auditIslands(options.cwd);
|
|
71
|
+
if (options.json) {
|
|
72
|
+
console.log("");
|
|
73
|
+
console.log(JSON.stringify({ islandAudit: islandsResult }, null, 2));
|
|
74
|
+
} else {
|
|
75
|
+
console.log("");
|
|
76
|
+
console.log(formatIslandAudit(islandsResult));
|
|
77
|
+
console.log("");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (options.checkSsg) {
|
|
81
|
+
const ssgResult = auditSsg(options.cwd);
|
|
82
|
+
if (options.json) {
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log(JSON.stringify({ ssgAudit: ssgResult }, null, 2));
|
|
85
|
+
} else {
|
|
86
|
+
console.log("");
|
|
87
|
+
console.log(formatSsgAudit(ssgResult));
|
|
88
|
+
console.log("");
|
|
89
|
+
}
|
|
90
|
+
}
|
|
69
91
|
return result.summary.totalErrors;
|
|
70
92
|
}
|
|
71
93
|
const sourceExtensions = new Set([
|
|
@@ -233,10 +255,16 @@ function printUsage() {
|
|
|
233
255
|
pyreon <command> [options]
|
|
234
256
|
|
|
235
257
|
Commands:
|
|
236
|
-
doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
|
|
258
|
+
doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>] [--check-islands] [--check-ssg]
|
|
237
259
|
Scan for React patterns, bad imports, common mistakes.
|
|
238
260
|
--audit-tests appends mock-vnode test-audit (PR #197 class).
|
|
239
261
|
--audit-min-risk is high|medium|low (default medium).
|
|
262
|
+
--check-islands appends project-wide islands audit
|
|
263
|
+
(duplicate names, dead islands, registry drift, nested,
|
|
264
|
+
never-with-registry).
|
|
265
|
+
--check-ssg appends project-wide SSG / ISR audit
|
|
266
|
+
(_404.tsx placement, dynamic routes missing
|
|
267
|
+
getStaticPaths, non-literal revalidate exports).
|
|
240
268
|
context [--out <path>] Generate .pyreon/context.json for AI tools
|
|
241
269
|
|
|
242
270
|
Options:
|
|
@@ -266,7 +294,9 @@ async function main() {
|
|
|
266
294
|
ci: args.includes("--ci"),
|
|
267
295
|
cwd: process.cwd(),
|
|
268
296
|
auditTests: args.includes("--audit-tests"),
|
|
269
|
-
auditMinRisk: rawRisk
|
|
297
|
+
auditMinRisk: rawRisk,
|
|
298
|
+
checkIslands: args.includes("--check-islands"),
|
|
299
|
+
checkSsg: args.includes("--check-ssg")
|
|
270
300
|
};
|
|
271
301
|
const exitCode = await doctor(options);
|
|
272
302
|
if (options.ci && exitCode > 0) process.exit(1);
|
package/lib/types/index.d.ts
CHANGED
|
@@ -23,6 +23,30 @@ interface DoctorOptions {
|
|
|
23
23
|
auditTests?: boolean | undefined;
|
|
24
24
|
/** Minimum risk level to include in the test-audit report. Default 'medium'. */
|
|
25
25
|
auditMinRisk?: AuditRisk | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* When true, run the project-wide islands audit and append the result
|
|
28
|
+
* to the doctor output. Catches cross-file foot-guns (duplicate names,
|
|
29
|
+
* dead islands, registry drift, nested islands, never-with-registry)
|
|
30
|
+
* that PR G's per-file detector and PR B's auto-registry can't reach
|
|
31
|
+
* (manual `hydrateIslands({...})` for non-Vite consumers, library
|
|
32
|
+
* authors, multi-package projects). Default false.
|
|
33
|
+
*/
|
|
34
|
+
checkIslands?: boolean | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* When true, run the project-wide SSG / ISR audit (M3.4) and append
|
|
37
|
+
* the result to the doctor output. Catches:
|
|
38
|
+
* - `_404.tsx` not co-located with `_layout.tsx` (PR L5 carve-out)
|
|
39
|
+
* - dynamic routes (`[id].tsx`) without `getStaticPaths` (PR A
|
|
40
|
+
* silently skips them under `mode: 'ssg'`)
|
|
41
|
+
* - `export const revalidate = X` where X isn't a numeric literal
|
|
42
|
+
* (PR I's extractor silently drops non-literal forms)
|
|
43
|
+
*
|
|
44
|
+
* Like the islands audit, this is a "should review" signal — the exit
|
|
45
|
+
* code is unaffected (the build doesn't break) but CI can pipe
|
|
46
|
+
* `--check-ssg --json` and grep for findings.length > 0 to gate on
|
|
47
|
+
* it. Default false.
|
|
48
|
+
*/
|
|
49
|
+
checkSsg?: boolean | undefined;
|
|
26
50
|
}
|
|
27
51
|
declare function doctor(options: DoctorOptions): Promise<number>;
|
|
28
52
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "CLI tools for Pyreon — doctor, generate, context",
|
|
5
5
|
"homepage": "https://github.com/pyreon/pyreon/tree/main/packages/cli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"prepublishOnly": "bun run build"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@pyreon/compiler": "^0.
|
|
49
|
+
"@pyreon/compiler": "^0.16.0"
|
|
50
50
|
},
|
|
51
51
|
"peerDependencies": {
|
|
52
52
|
"typescript": ">=5.0.0"
|
package/src/doctor.ts
CHANGED
|
@@ -17,9 +17,13 @@
|
|
|
17
17
|
import * as fs from 'node:fs'
|
|
18
18
|
import * as path from 'node:path'
|
|
19
19
|
import {
|
|
20
|
+
auditIslands,
|
|
21
|
+
auditSsg,
|
|
20
22
|
auditTestEnvironment,
|
|
21
23
|
type AuditRisk,
|
|
22
24
|
detectReactPatterns,
|
|
25
|
+
formatIslandAudit,
|
|
26
|
+
formatSsgAudit,
|
|
23
27
|
formatTestAudit,
|
|
24
28
|
hasReactPatterns,
|
|
25
29
|
migrateReactCode,
|
|
@@ -41,6 +45,30 @@ export interface DoctorOptions {
|
|
|
41
45
|
auditTests?: boolean | undefined
|
|
42
46
|
/** Minimum risk level to include in the test-audit report. Default 'medium'. */
|
|
43
47
|
auditMinRisk?: AuditRisk | undefined
|
|
48
|
+
/**
|
|
49
|
+
* When true, run the project-wide islands audit and append the result
|
|
50
|
+
* to the doctor output. Catches cross-file foot-guns (duplicate names,
|
|
51
|
+
* dead islands, registry drift, nested islands, never-with-registry)
|
|
52
|
+
* that PR G's per-file detector and PR B's auto-registry can't reach
|
|
53
|
+
* (manual `hydrateIslands({...})` for non-Vite consumers, library
|
|
54
|
+
* authors, multi-package projects). Default false.
|
|
55
|
+
*/
|
|
56
|
+
checkIslands?: boolean | undefined
|
|
57
|
+
/**
|
|
58
|
+
* When true, run the project-wide SSG / ISR audit (M3.4) and append
|
|
59
|
+
* the result to the doctor output. Catches:
|
|
60
|
+
* - `_404.tsx` not co-located with `_layout.tsx` (PR L5 carve-out)
|
|
61
|
+
* - dynamic routes (`[id].tsx`) without `getStaticPaths` (PR A
|
|
62
|
+
* silently skips them under `mode: 'ssg'`)
|
|
63
|
+
* - `export const revalidate = X` where X isn't a numeric literal
|
|
64
|
+
* (PR I's extractor silently drops non-literal forms)
|
|
65
|
+
*
|
|
66
|
+
* Like the islands audit, this is a "should review" signal — the exit
|
|
67
|
+
* code is unaffected (the build doesn't break) but CI can pipe
|
|
68
|
+
* `--check-ssg --json` and grep for findings.length > 0 to gate on
|
|
69
|
+
* it. Default false.
|
|
70
|
+
*/
|
|
71
|
+
checkSsg?: boolean | undefined
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
interface FileResult {
|
|
@@ -90,6 +118,40 @@ export async function doctor(options: DoctorOptions): Promise<number> {
|
|
|
90
118
|
}
|
|
91
119
|
}
|
|
92
120
|
|
|
121
|
+
// Islands audit — optional follow-on pass (PR C of the islands DX
|
|
122
|
+
// roadmap). Runs the project-wide cross-file scan. Like the
|
|
123
|
+
// test-audit, this is a "should review" signal — exit code unaffected
|
|
124
|
+
// (the build doesn't break) but in CI you can pipe `--check-islands
|
|
125
|
+
// --json` and grep for findings.length > 0 to gate on it.
|
|
126
|
+
if (options.checkIslands) {
|
|
127
|
+
const islandsResult = auditIslands(options.cwd)
|
|
128
|
+
if (options.json) {
|
|
129
|
+
console.log('')
|
|
130
|
+
console.log(JSON.stringify({ islandAudit: islandsResult }, null, 2))
|
|
131
|
+
} else {
|
|
132
|
+
console.log('')
|
|
133
|
+
console.log(formatIslandAudit(islandsResult))
|
|
134
|
+
console.log('')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// M3.4 — SSG audit. Catches `_404.tsx` placement (PR L5 carve-out),
|
|
139
|
+
// dynamic-route enumerators (PR A silent skip), and non-literal
|
|
140
|
+
// revalidate exports (PR I's extractor limitation). Exit code
|
|
141
|
+
// unaffected — same "should review" treatment as islands / test
|
|
142
|
+
// audits; CI gates via `--json | jq '.ssgAudit.findings | length'`.
|
|
143
|
+
if (options.checkSsg) {
|
|
144
|
+
const ssgResult = auditSsg(options.cwd)
|
|
145
|
+
if (options.json) {
|
|
146
|
+
console.log('')
|
|
147
|
+
console.log(JSON.stringify({ ssgAudit: ssgResult }, null, 2))
|
|
148
|
+
} else {
|
|
149
|
+
console.log('')
|
|
150
|
+
console.log(formatSsgAudit(ssgResult))
|
|
151
|
+
console.log('')
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
93
155
|
return result.summary.totalErrors
|
|
94
156
|
}
|
|
95
157
|
|
package/src/index.ts
CHANGED
|
@@ -19,10 +19,16 @@ function printUsage(): void {
|
|
|
19
19
|
pyreon <command> [options]
|
|
20
20
|
|
|
21
21
|
Commands:
|
|
22
|
-
doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>]
|
|
22
|
+
doctor [--fix] [--json] [--ci] [--audit-tests] [--audit-min-risk <level>] [--check-islands] [--check-ssg]
|
|
23
23
|
Scan for React patterns, bad imports, common mistakes.
|
|
24
24
|
--audit-tests appends mock-vnode test-audit (PR #197 class).
|
|
25
25
|
--audit-min-risk is high|medium|low (default medium).
|
|
26
|
+
--check-islands appends project-wide islands audit
|
|
27
|
+
(duplicate names, dead islands, registry drift, nested,
|
|
28
|
+
never-with-registry).
|
|
29
|
+
--check-ssg appends project-wide SSG / ISR audit
|
|
30
|
+
(_404.tsx placement, dynamic routes missing
|
|
31
|
+
getStaticPaths, non-literal revalidate exports).
|
|
26
32
|
context [--out <path>] Generate .pyreon/context.json for AI tools
|
|
27
33
|
|
|
28
34
|
Options:
|
|
@@ -56,6 +62,8 @@ async function main(): Promise<void> {
|
|
|
56
62
|
cwd: process.cwd(),
|
|
57
63
|
auditTests: args.includes('--audit-tests'),
|
|
58
64
|
auditMinRisk: rawRisk as DoctorOptions['auditMinRisk'],
|
|
65
|
+
checkIslands: args.includes('--check-islands'),
|
|
66
|
+
checkSsg: args.includes('--check-ssg'),
|
|
59
67
|
}
|
|
60
68
|
const exitCode = await doctor(options)
|
|
61
69
|
if (options.ci && exitCode > 0) {
|
package/src/tests/doctor.test.ts
CHANGED
|
@@ -354,3 +354,128 @@ describe('doctor — --audit-tests integration', () => {
|
|
|
354
354
|
expect(output).not.toMatch(/^## MEDIUM/m)
|
|
355
355
|
})
|
|
356
356
|
})
|
|
357
|
+
|
|
358
|
+
// ─── --check-islands integration (PR C) ─────────────────────────────────────
|
|
359
|
+
|
|
360
|
+
describe('doctor — --check-islands integration', () => {
|
|
361
|
+
let logSpy: ReturnType<typeof vi.spyOn>
|
|
362
|
+
let tmpDir: string
|
|
363
|
+
|
|
364
|
+
beforeEach(() => {
|
|
365
|
+
logSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined)
|
|
366
|
+
tmpDir = makeTmpDir()
|
|
367
|
+
fs.mkdirSync(path.join(tmpDir, 'packages'), { recursive: true })
|
|
368
|
+
})
|
|
369
|
+
afterEach(() => {
|
|
370
|
+
logSpy.mockRestore()
|
|
371
|
+
fs.rmSync(tmpDir, { recursive: true, force: true })
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
it('does NOT print islands audit output when --check-islands is absent (default)', async () => {
|
|
375
|
+
writeFile(
|
|
376
|
+
tmpDir,
|
|
377
|
+
'packages/x/src/Counter.tsx',
|
|
378
|
+
`import { island } from '@pyreon/server'
|
|
379
|
+
export const Counter = island(() => import('./Inner'), { name: 'Counter', hydrate: 'load' })`,
|
|
380
|
+
)
|
|
381
|
+
const opts: DoctorOptions = {
|
|
382
|
+
fix: false,
|
|
383
|
+
json: false,
|
|
384
|
+
ci: false,
|
|
385
|
+
cwd: tmpDir,
|
|
386
|
+
checkIslands: false,
|
|
387
|
+
}
|
|
388
|
+
await doctor(opts)
|
|
389
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
|
|
390
|
+
expect(output).not.toContain('Islands audit')
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('prints the islands audit when --check-islands is passed', async () => {
|
|
394
|
+
writeFile(
|
|
395
|
+
tmpDir,
|
|
396
|
+
'packages/x/src/A.tsx',
|
|
397
|
+
`import { island } from '@pyreon/server'
|
|
398
|
+
export const A = island(() => import('./AInner'), { name: 'Dup', hydrate: 'load' })`,
|
|
399
|
+
)
|
|
400
|
+
writeFile(
|
|
401
|
+
tmpDir,
|
|
402
|
+
'packages/y/src/B.tsx',
|
|
403
|
+
`import { island } from '@pyreon/server'
|
|
404
|
+
export const B = island(() => import('./BInner'), { name: 'Dup', hydrate: 'load' })`,
|
|
405
|
+
)
|
|
406
|
+
writeFile(tmpDir, 'packages/x/src/AInner.tsx', `export default () => null`)
|
|
407
|
+
writeFile(tmpDir, 'packages/y/src/BInner.tsx', `export default () => null`)
|
|
408
|
+
const opts: DoctorOptions = {
|
|
409
|
+
fix: false,
|
|
410
|
+
json: false,
|
|
411
|
+
ci: false,
|
|
412
|
+
cwd: tmpDir,
|
|
413
|
+
checkIslands: true,
|
|
414
|
+
}
|
|
415
|
+
await doctor(opts)
|
|
416
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
|
|
417
|
+
expect(output).toContain('Islands audit')
|
|
418
|
+
expect(output).toContain('## duplicate-name')
|
|
419
|
+
// Both file locations appear in the human-readable section
|
|
420
|
+
expect(output).toContain('A.tsx')
|
|
421
|
+
expect(output).toContain('B.tsx')
|
|
422
|
+
})
|
|
423
|
+
|
|
424
|
+
it('emits machine-readable JSON when --json + --check-islands both set', async () => {
|
|
425
|
+
writeFile(
|
|
426
|
+
tmpDir,
|
|
427
|
+
'packages/x/src/Orphan.tsx',
|
|
428
|
+
`import { island } from '@pyreon/server'
|
|
429
|
+
export const Orphan = island(() => import('./Inner'), { name: 'Orphan', hydrate: 'load' })`,
|
|
430
|
+
)
|
|
431
|
+
writeFile(tmpDir, 'packages/x/src/Inner.tsx', `export default () => null`)
|
|
432
|
+
const opts: DoctorOptions = {
|
|
433
|
+
fix: false,
|
|
434
|
+
json: true,
|
|
435
|
+
ci: false,
|
|
436
|
+
cwd: tmpDir,
|
|
437
|
+
checkIslands: true,
|
|
438
|
+
}
|
|
439
|
+
await doctor(opts)
|
|
440
|
+
// Two JSON blobs logged separately — doctor result then the audit.
|
|
441
|
+
const blobs = logSpy.mock.calls
|
|
442
|
+
.map((c: unknown[]) => String(c[0] ?? ''))
|
|
443
|
+
.filter((s: string) => s.trim().startsWith('{'))
|
|
444
|
+
expect(blobs.length).toBeGreaterThanOrEqual(2)
|
|
445
|
+
const auditBlob = blobs.find((s: string) => s.includes('islandAudit'))
|
|
446
|
+
expect(auditBlob).toBeDefined()
|
|
447
|
+
const parsed = JSON.parse(auditBlob!) as {
|
|
448
|
+
islandAudit: { findings: Array<{ code: string }>; summary: { islandsDeclared: number } }
|
|
449
|
+
}
|
|
450
|
+
expect(parsed.islandAudit.summary.islandsDeclared).toBe(1)
|
|
451
|
+
// The orphan declaration should produce exactly one dead-island finding.
|
|
452
|
+
const codes = parsed.islandAudit.findings.map((f) => f.code)
|
|
453
|
+
expect(codes).toContain('dead-island')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
it('emits a clean green-light section when no findings are present', async () => {
|
|
457
|
+
writeFile(
|
|
458
|
+
tmpDir,
|
|
459
|
+
'packages/x/src/Counter.tsx',
|
|
460
|
+
`import { island } from '@pyreon/server'
|
|
461
|
+
export const Counter = island(() => import('./Inner'), { name: 'Counter', hydrate: 'load' })`,
|
|
462
|
+
)
|
|
463
|
+
writeFile(tmpDir, 'packages/x/src/Inner.tsx', `export default () => null`)
|
|
464
|
+
writeFile(
|
|
465
|
+
tmpDir,
|
|
466
|
+
'packages/x/src/index.ts',
|
|
467
|
+
`export { Counter } from './Counter'`,
|
|
468
|
+
)
|
|
469
|
+
const opts: DoctorOptions = {
|
|
470
|
+
fix: false,
|
|
471
|
+
json: false,
|
|
472
|
+
ci: false,
|
|
473
|
+
cwd: tmpDir,
|
|
474
|
+
checkIslands: true,
|
|
475
|
+
}
|
|
476
|
+
await doctor(opts)
|
|
477
|
+
const output = logSpy.mock.calls.map((c: unknown[]) => c.join(' ')).join('\n')
|
|
478
|
+
expect(output).toContain('Islands audit')
|
|
479
|
+
expect(output).toContain('No island findings')
|
|
480
|
+
})
|
|
481
|
+
})
|