// @effect-diagnostics nodeBuiltinImport:off - Knip and the TypeScript compiler host use synchronous Node paths.import * as NodePath from "node:path";import type { Preprocessor } from "knip";// Knip needs the legacy compiler API, which TypeScript 7 no longer exports.import ts from "typescript-legacy"; // Effect 4 schemas carry this marker, including aliases and Schema.Class constructors.// Checking the type avoids evaluating application modules or exempting schema factories/decoders.const schemaTypeId = "~effect/Schema/Schema"; const preprocess: Preprocessor = (options) => { const categories = ["exports", "nsExports", "duplicates"] as const; const projects = new Map<string | undefined, Set<string>>(); for (const category of categories) { for (const [filePath, issues] of Object.entries(options.issues[category])) { if (Object.keys(issues).length === 0) continue; const configPath = ts.findConfigFile( NodePath.dirname(NodePath.resolve(options.cwd, filePath)), ts.sys.fileExists, ); const files = projects.get(configPath) ?? new Set<string>(); files.add(filePath); projects.set(configPath, files); } } for (const [configPath, files] of projects) { const config = configPath ? ts.getParsedCommandLineOfConfigFile( configPath, {}, { ...ts.sys, onUnRecoverableConfigFileDiagnostic: (diagnostic) => { throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n")); }, }, ) : undefined; if (config?.errors.length) { throw new Error( config.errors .map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n")) .join("\n"), ); } const program = ts.createProgram( [...files].map((file) => NodePath.resolve(options.cwd, file)), { module: ts.ModuleKind.NodeNext, allowJs: true, ...config?.options, noEmit: true, }, ); const checker = program.getTypeChecker(); for (const filePath of files) { const source = program.getSourceFile(NodePath.resolve(options.cwd, filePath)); const module = source && checker.getSymbolAtLocation(source); if (!source || !module) continue; const schemas = new Set( checker.getExportsOfModule(module).flatMap((symbol) => { const exported = symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol; // Duplicate exports can name a private value with a public type of the same name. const target = exported.flags & ts.SymbolFlags.Value ? exported : checker.resolveName(symbol.name, source, ts.SymbolFlags.Value, false); if (!target) return []; const type = checker.getTypeOfSymbolAtLocation(target, target.valueDeclaration ?? source); const marker = type.getProperty(schemaTypeId); if (!marker) return []; const markerType = checker.getTypeOfSymbolAtLocation(marker, source); return markerType.isStringLiteral() && markerType.value === schemaTypeId ? [symbol.name] : []; }), ); for (const category of categories) { const issues = options.issues[category][filePath]; if (!issues) continue; for (const [key, issue] of Object.entries(issues)) { const symbols = issue.symbols ?? [{ symbol: issue.symbol }]; if (symbols.length > 0 && symbols.every(({ symbol }) => schemas.has(symbol))) { delete issues[key]; options.counters[category]--; } } if (Object.keys(issues).length === 0) delete options.issues[category][filePath]; } } } return options;}; export default preprocess;
// @effect-diagnostics nodeBuiltinImport:off - Knip and the TypeScript compiler host use synchronous Node paths.
import * as NodePath from "node:path";
import type { Preprocessor } from "knip";
// Knip needs the legacy compiler API, which TypeScript 7 no longer exports.
import ts from "typescript-legacy";
// Effect 4 schemas carry this marker, including aliases and Schema.Class constructors.
// Checking the type avoids evaluating application modules or exempting schema factories/decoders.
const schemaTypeId = "~effect/Schema/Schema";
const preprocess: Preprocessor = (options) => {
const categories = ["exports", "nsExports", "duplicates"] as const;
const projects = new Map<string | undefined, Set<string>>();
for (const category of categories) {
for (const [filePath, issues] of Object.entries(options.issues[category])) {
if (Object.keys(issues).length === 0) continue;
const configPath = ts.findConfigFile(
NodePath.dirname(NodePath.resolve(options.cwd, filePath)),
ts.sys.fileExists,
);
const files = projects.get(configPath) ?? new Set<string>();
files.add(filePath);
projects.set(configPath, files);
}
}
for (const [configPath, files] of projects) {
const config = configPath
? ts.getParsedCommandLineOfConfigFile(
configPath,
{},
{
...ts.sys,
onUnRecoverableConfigFileDiagnostic: (diagnostic) => {
throw new Error(ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"));
},
},
)
: undefined;
if (config?.errors.length) {
throw new Error(
config.errors
.map((error) => ts.flattenDiagnosticMessageText(error.messageText, "\n"))
.join("\n"),
);
}
const program = ts.createProgram(
[...files].map((file) => NodePath.resolve(options.cwd, file)),
{
module: ts.ModuleKind.NodeNext,
allowJs: true,
...config?.options,
noEmit: true,
},
);
const checker = program.getTypeChecker();
for (const filePath of files) {
const source = program.getSourceFile(NodePath.resolve(options.cwd, filePath));
const module = source && checker.getSymbolAtLocation(source);
if (!source || !module) continue;
const schemas = new Set(
checker.getExportsOfModule(module).flatMap((symbol) => {
const exported =
symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
// Duplicate exports can name a private value with a public type of the same name.
const target =
exported.flags & ts.SymbolFlags.Value
? exported
: checker.resolveName(symbol.name, source, ts.SymbolFlags.Value, false);
if (!target) return [];
const type = checker.getTypeOfSymbolAtLocation(target, target.valueDeclaration ?? source);
const marker = type.getProperty(schemaTypeId);
if (!marker) return [];
const markerType = checker.getTypeOfSymbolAtLocation(marker, source);
return markerType.isStringLiteral() && markerType.value === schemaTypeId
? [symbol.name]
: [];
}),
);
for (const category of categories) {
const issues = options.issues[category][filePath];
if (!issues) continue;
for (const [key, issue] of Object.entries(issues)) {
const symbols = issue.symbols ?? [{ symbol: issue.symbol }];
if (symbols.length > 0 && symbols.every(({ symbol }) => schemas.has(symbol))) {
delete issues[key];
options.counters[category]--;
}
}
if (Object.keys(issues).length === 0) delete options.issues[category][filePath];
}
}
}
return options;
};
export default preprocess;