1import { cli } from "@effect/api-diff"
2import { ApiDiff } from "@effect/api-diff/ApiDiff"
3import type { ApiEntity, ApiSnapshot } from "@effect/api-diff/Model"
4import { Worktrees } from "@effect/api-diff/Worktrees"
5import * as NodeServices from "@effect/platform-node/NodeServices"
6import { assert, it } from "@effect/vitest"
7import * as Command from "effect/cli/Command"
8import * as Effect from "effect/Effect"
9import * as Exit from "effect/Exit"
10import * as FileSystem from "effect/FileSystem"
11import * as Layer from "effect/Layer"
12import * as Path from "effect/Path"
13
14const snapshot = (ref: string, sha: string, entities: ReadonlyArray<ApiEntity> = []): ApiSnapshot => ({
15 version: 1,
16 compiler: { name: "typescript", version: "fixture" },
17 ref,
18 sha,
19 packages: [],
20 entrypoints: [],
21 entities,
22 diagnostics: []
23})
24
25const WorktreesTest = Layer.succeed(
26 Worktrees,
27 Worktrees.of({
28 resolveRef: (_repoRoot, ref) => Effect.succeed(ref.repeat(40).slice(0, 40)),
29 prepareSnapshot: (options) => Effect.succeed(snapshot(options.ref, options.sha))
30 })
31)
32
33const MainLayer = ApiDiff.layerNoDependencies.pipe(
34 Layer.provide(WorktreesTest),
35 Layer.provideMerge(NodeServices.layer)
36)
37
38const removed: ApiEntity = {
39 id: "effect/Effect#removed#value",
40 packageName: "effect",
41 module: "effect/Effect",
42 path: ["removed"],
43 bucket: "value",
44 declarationKind: "variable",
45 importRoutes: [{ module: "effect/Effect", path: ["removed"] }],
46 declarations: [{ kind: "variable", name: "removed", type: { kind: "primitive", name: "string" } }],
47 displaySignature: "declare const removed: string",
48 fingerprint: "removed",
49 documentation: { stability: "stable" },
50 source: { file: "Effect.d.ts", line: 1, column: 1 }
51}
52
53const CheckLayer = ApiDiff.layerNoDependencies.pipe(
54 Layer.provide(Layer.succeed(
55 Worktrees,
56 Worktrees.of({
57 resolveRef: (_repoRoot, ref) => Effect.succeed(ref.repeat(40).slice(0, 40)),
58 prepareSnapshot: (options) =>
59 Effect.succeed(snapshot(options.ref, options.sha, options.name === "base" ? [removed] : []))
60 })
61 )),
62 Layer.provideMerge(NodeServices.layer)
63)
64
65it.effect("compares refs without a migration map", () =>
66 Effect.gen(function*() {
67 const fs = yield* FileSystem.FileSystem
68 const path = yield* Path.Path
69 const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-cli-" })
70 const output = path.join(root, "output")
71
72 yield* Command.runWith(cli, { version: "0.0.0" })([
73 "--base-ref",
74 "a",
75 "--head-ref",
76 "b",
77 "--output",
78 output
79 ])
80
81 const diff = JSON.parse(yield* fs.readFileString(path.join(output, "diff.json")))
82 assert.deepStrictEqual(diff.base, { ref: "a", sha: "a".repeat(40) })
83 assert.deepStrictEqual(diff.head, { ref: "b", sha: "b".repeat(40) })
84 }).pipe(Effect.provide(MainLayer)))
85
86it.effect("writes the migration document with default refs", () =>
87 Effect.gen(function*() {
88 const fs = yield* FileSystem.FileSystem
89 const path = yield* Path.Path
90 const root = yield* fs.makeTempDirectoryScoped({ prefix: "api-diff-cli-" })
91 const document = path.join(root, "v3-to-v4.md")
92 yield* fs.writeFileString(document, "# Existing\n\n## Import Map\n\nKeep this map.\n\n## API Renames\n")
93
94 yield* Command.runWith(cli, { version: "0.0.0" })([
95 "--write-doc",
96 document
97 ])
98
99 const source = yield* fs.readFileString(document)
100 assert(source.includes(`Base: \`v3\` (\`${"v3".repeat(40).slice(0, 40)}\`)`))
101 assert(source.includes(`Head: \`main\` (\`${"origin/main".repeat(40).slice(0, 40)}\`)`))
102 assert(source.includes("## Import Map\n\nKeep this map."))
103 assert(source.includes("## API Reference"))
104 }).pipe(Effect.provide(MainLayer)))
105
106it.effect("fails check mode when guidance is missing", () =>
107 Effect.gen(function*() {
108 const result = yield* Effect.exit(Command.runWith(cli, { version: "0.0.0" })(["--check"]))
109
110 assert(Exit.isFailure(result))
111 }).pipe(Effect.provide(CheckLayer)))