1// @effect-diagnostics nodeBuiltinImport:off - Runs the real Knip CLI against a disposable on-disk project.
2import * as NodeChildProcess from "node:child_process";
3import * as NodeFS from "node:fs";
4import * as NodeModule from "node:module";
5import * as NodePath from "node:path";
6import * as NodeProcess from "node:process";
7import { expect, it } from "vite-plus/test";
8
9const require = NodeModule.createRequire(import.meta.url);
10const cli = NodePath.join(NodePath.dirname(require.resolve("knip")), "cli.js");
11const preprocessor = NodePath.join(import.meta.dirname, "knip-schemas.ts");
12
13it("allows types and schemas through the real Knip CLI without hiding runtime or file findings", () => {
14 // Keeping the disposable project here gives it the same Effect installation as the scripts.
15 const cwd = NodeFS.mkdtempSync(NodePath.join(import.meta.dirname, ".knip-test-"));
16 const write = (file: string, content: string) =>
17 NodeFS.writeFileSync(NodePath.join(cwd, file), content);
18 const run = (filtered: boolean) =>
19 NodeChildProcess.spawnSync(
20 NodeProcess.execPath,
21 [
22 cli,
23 "--directory",
24 cwd,
25 "--config",
26 NodePath.join(cwd, "knip.json"),
27 "--no-config-hints",
28 "--include",
29 "files,dependencies,exports,nsExports,types,nsTypes,duplicates",
30 "--reporter",
31 "json",
32 ...(filtered ? ["--preprocessor", preprocessor] : []),
33 ],
34 { encoding: "utf8" },
35 );
36 try {
37 write(
38 "package.json",
39 JSON.stringify({ private: true, type: "module", dependencies: { effect: "*" } }),
40 );
41 write(
42 "tsconfig.json",
43 JSON.stringify({ compilerOptions: { module: "NodeNext", strict: true } }),
44 );
45 write(
46 "knip.json",
47 JSON.stringify({
48 entry: ["entry.ts"],
49 project: ["*.ts"],
50 includeEntryExports: true,
51 include: [
52 "exports",
53 "nsExports",
54 "types",
55 "nsTypes",
56 "duplicates",
57 "files",
58 "dependencies",
59 ],
60 rules: { types: "off", nsTypes: "off" },
61 }),
62 );
63 write(
64 "entry.ts",
65 `
66 import * as S from "effect/Schema";
67 import * as ns from "./namespace.ts";
68 console.log(ns);
69 export { Remote as Reexported } from "./remote.ts";
70 export const Text = S.String;
71 export const Alias = Text;
72 const Internal = S.Boolean;
73 export type Internal = typeof Internal.Type;
74 export const PublicAlias = Internal;
75 export default Text;
76 export const Record = S.Struct({ name: Text }).annotate({ title: "record" });
77 export const Branded = S.String.pipe(S.brand("Name"));
78 export class Failure extends S.TaggedError<Failure>()("Failure", { reason: Text }) {}
79 export class Person extends S.Class<Person>("Person")({ name: Text }) {}
80 export type UnusedType = { name: string };
81 export interface UnusedInterface { name: string }
82 `,
83 );
84 write(
85 "remote.ts",
86 `
87 import { Schema as S } from "effect";
88 export const Remote = S.Struct({ id: S.Number });
89 throw new Error("The preprocessor must never evaluate application modules");
90 `,
91 );
92 write(
93 "namespace.ts",
94 `
95 import * as S from "effect/Schema";
96 export const Unused = S.Number;
97 export type UnusedType = string;
98 `,
99 );
100 const baseline = run(false);
101 expect(baseline.status, baseline.stderr).toBe(1);
102 expect(baseline.stdout).toContain('"Text"');
103 const allowed = run(true);
104 expect(allowed.status, allowed.stderr + allowed.stdout).toBe(0);
105 expect(JSON.parse(allowed.stdout).issues).toEqual([]);
106
107 NodeFS.appendFileSync(
108 NodePath.join(cwd, "entry.ts"),
109 `
110 export const decode = S.decodeUnknownSync(Text);
111 export const makeSchema = () => S.String;
112 export const LooksLikeSchema = { ast: "not a schema" };
113 export const ordinary = 123;
114 export const duplicate = ordinary;
115 `,
116 );
117 NodeFS.appendFileSync(NodePath.join(cwd, "namespace.ts"), `export const helper = () => 1;`);
118 write("unused.ts", `export const orphan = 1;`);
119 write(
120 "package.json",
121 JSON.stringify({
122 private: true,
123 type: "module",
124 dependencies: { effect: "*", "unused-knip-fixture-dependency": "*" },
125 }),
126 );
127 const rejected = run(true);
128 expect(rejected.status, rejected.stderr).toBe(1);
129 const issues = JSON.parse(rejected.stdout).issues;
130 const entry = issues.find((issue: { file: string }) => issue.file === "entry.ts");
131 expect(entry.exports.map((issue: { name: string }) => issue.name).sort()).toEqual([
132 "LooksLikeSchema",
133 "decode",
134 "duplicate",
135 "makeSchema",
136 "ordinary",
137 ]);
138 expect(entry.duplicates).toHaveLength(1);
139 expect(
140 issues.find((issue: { file: string }) => issue.file === "namespace.ts").nsExports,
141 ).toEqual([expect.objectContaining({ name: "helper" })]);
142 expect(issues.find((issue: { file: string }) => issue.file === "unused.ts").files).toHaveLength(
143 1,
144 );
145 expect(
146 issues.find((issue: { file: string }) => issue.file === "package.json").dependencies,
147 ).toEqual([expect.objectContaining({ name: "unused-knip-fixture-dependency" })]);
148 } finally {
149 NodeFS.rmSync(cwd, { recursive: true, force: true });
150 }
151});