Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More โ†’
Socket
Sign inDemoInstall
Socket

@line/ts-remove-unused

Package Overview
Dependencies
Maintainers
0
Versions
17
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@line/ts-remove-unused - npm Package Compare versions

Comparing version 0.6.2 to 0.7.0

dist/util/createDependencyGraph.d.ts

1208

dist/cli.js

@@ -7,3 +7,3 @@ #!/usr/bin/env node

// lib/remove.ts
import ts5 from "typescript";
import ts7 from "typescript";

@@ -40,6 +40,13 @@ // lib/util/MemoryFileService.ts

}
eject() {
const res = /* @__PURE__ */ new Map();
for (const [name, { content }] of this.#files) {
res.set(name, content);
}
return res;
}
};
// lib/util/removeUnusedExport.ts
import ts4 from "typescript";
import ts6 from "typescript";

@@ -81,22 +88,59 @@ // lib/util/applyTextChanges.ts

import ts from "typescript";
var fixIdDelete = "unusedIdentifier_delete";
var fixIdDeleteImports = "unusedIdentifier_deleteImports";
var filterChanges = ({
sourceFile,
textChanges
}) => {
const result = [];
const visit = (node) => {
if ((ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) && node.parameters.length > 0) {
const start = node.parameters[0]?.getStart();
const end = node.parameters[node.parameters.length - 1]?.getEnd();
if (typeof start === "number" && typeof end === "number") {
result.push({ start, end });
}
}
node.forEachChild(visit);
};
sourceFile.forEachChild(visit);
return textChanges.filter((change) => {
const start = change.span.start;
const end = change.span.start + change.span.length;
return !result.some((r) => r.start <= start && end <= r.end);
});
};
var applyCodeFix = ({
fixId,
languageService,
fileName
}) => {
const program = languageService.getProgram();
if (!program) {
throw new Error("program not found");
}
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error(`source file not found: ${fileName}`);
}
let content = sourceFile.getFullText();
const actions = languageService.getCombinedCodeFix(
{
type: "file",
fileName
},
fixId,
{},
{}
);
for (const change of actions.changes) {
const textChanges = fixId === fixIdDelete ? filterChanges({ sourceFile, textChanges: change.textChanges }) : change.textChanges;
content = applyTextChanges(content, textChanges);
}
return content;
};
// lib/util/getFileFromModuleSpecifierText.ts
// lib/util/createDependencyGraph.ts
import ts2 from "typescript";
var getFileFromModuleSpecifierText = ({
specifier,
fileName,
program,
fileService
}) => ts2.resolveModuleName(specifier, fileName, program.getCompilerOptions(), {
fileExists(f) {
return fileService.exists(f);
},
readFile(f) {
return fileService.get(f);
}
}).resolvedModule?.resolvedFileName;
// lib/util/collectImports.ts
import ts3 from "typescript";
// lib/util/Graph.ts

@@ -161,30 +205,37 @@ var Graph = class {

super(() => ({
depth: 0,
hasReexport: false,
fromDynamic: /* @__PURE__ */ new Set(),
// will not be updated when we delete a file,
// but it's fine since we only use it to find the used specifier from a given file path.
wholeReexportSpecifier: /* @__PURE__ */ new Map()
depth: 0
}));
}
deleteVertex(vertex) {
const selected = this.vertexes.get(vertex);
if (!selected) {
return;
eject() {
const map = /* @__PURE__ */ new Map();
for (const [k, v] of this.vertexes.entries()) {
map.set(k, {
from: new Set(v.from),
to: new Set(v.to),
data: {
depth: v.data.depth
}
});
}
for (const v of selected.to) {
const target = this.vertexes.get(v);
if (!target) {
continue;
}
target.data.fromDynamic.delete(vertex);
}
super.deleteVertex(vertex);
return map;
}
};
// lib/util/collectImports.ts
// lib/util/createDependencyGraph.ts
var getFileFromModuleSpecifierText = ({
specifier,
fileName,
program,
fileService
}) => ts2.resolveModuleName(specifier, fileName, program.getCompilerOptions(), {
fileExists(f) {
return fileService.exists(f);
},
readFile(f) {
return fileService.get(f);
}
}).resolvedModule?.resolvedFileName;
var getMatchingNode = (node) => {
if (ts3.isImportDeclaration(node)) {
if (ts3.isStringLiteral(node.moduleSpecifier)) {
if (ts2.isImportDeclaration(node)) {
if (ts2.isStringLiteral(node.moduleSpecifier)) {
return {

@@ -200,4 +251,4 @@ type: "import",

}
if (ts3.isExportDeclaration(node)) {
if (node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
if (ts2.isExportDeclaration(node)) {
if (node.moduleSpecifier && ts2.isStringLiteral(node.moduleSpecifier)) {
const result = {

@@ -215,4 +266,4 @@ type: "reexport",

}
if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword) {
if (node.arguments[0] && ts3.isStringLiteral(node.arguments[0])) {
if (ts2.isCallExpression(node) && node.expression.kind === ts2.SyntaxKind.ImportKeyword) {
if (node.arguments[0] && ts2.isStringLiteral(node.arguments[0])) {
return {

@@ -230,3 +281,3 @@ type: "dynamicImport",

};
var collectImports = ({
var createDependencyGraph = ({
fileService,

@@ -239,3 +290,3 @@ program,

const stack = [];
const visited = /* @__PURE__ */ new Set();
const untouched = new Set(fileService.getFileNames());
for (const entrypoint of entrypoints) {

@@ -250,6 +301,6 @@ stack.push({ file: entrypoint, depth: 0 });

const { file, depth } = item;
if (visited.has(file)) {
if (!untouched.has(file)) {
continue;
}
visited.add(file);
untouched.delete(file);
const sourceFile = program.getSourceFile(file);

@@ -259,3 +310,2 @@ if (!sourceFile) {

}
let hasReexport = false;
const visit = (node) => {

@@ -268,5 +318,2 @@ const match = getMatchingNode(node);

if (match.specifier) {
if (match.type === "reexport") {
hasReexport = true;
}
const dest = getFileFromModuleSpecifierText({

@@ -278,10 +325,4 @@ specifier: match.specifier,

});
if (match.type === "reexport" && dest && match.whole) {
graph.vertexes.get(file)?.data.wholeReexportSpecifier.set(dest, match.specifier);
}
if (dest && files.has(dest)) {
graph.addEdge(sourceFile.fileName, dest);
if (match.type === "dynamicImport") {
graph.vertexes.get(dest)?.data.fromDynamic.add(file);
}
stack.push({ file: dest, depth: depth + 1 });

@@ -295,6 +336,35 @@ }

if (vertex) {
vertex.data.hasReexport = hasReexport;
vertex.data.depth = depth;
}
}
for (const file of untouched.values()) {
const sourceFile = program.getSourceFile(file);
if (!sourceFile) {
continue;
}
const visit = (node) => {
const match = getMatchingNode(node);
if (!match) {
node.forEachChild(visit);
return;
}
if (match.specifier) {
const dest = getFileFromModuleSpecifierText({
specifier: match.specifier,
program,
fileName: sourceFile.fileName,
fileService
});
if (dest && files.has(dest)) {
graph.addEdge(sourceFile.fileName, dest);
}
return;
}
};
sourceFile.forEachChild(visit);
const vertex = graph.vertexes.get(file);
if (vertex) {
vertex.data.depth = Infinity;
}
}
return graph;

@@ -347,48 +417,416 @@ };

// lib/util/removeUnusedExport.ts
// lib/util/findFileUsage.ts
import ts4 from "typescript";
// lib/util/parseFile.ts
import ts3 from "typescript";
// lib/util/memoize.ts
var memoize = (fn2, { key }) => {
const cache = /* @__PURE__ */ new Map();
return (...args) => {
const k = key(...args);
if (cache.has(k)) {
return cache.get(k);
}
const result = fn2(...args);
cache.set(k, result);
return result;
};
};
// lib/util/parseFile.ts
var IGNORE_COMMENT = "ts-remove-unused-skip";
var disabledEditTracker = {
start: () => {
var getLeadingComment = (node) => {
const fullText = node.getSourceFile().getFullText();
const ranges = ts3.getLeadingCommentRanges(fullText, node.getFullStart());
if (!ranges) {
return "";
}
return ranges.map((range) => fullText.slice(range.pos, range.end)).join("");
};
var resolve = ({
specifier,
file,
destFiles,
options
}) => ts3.resolveModuleName(specifier, file, options, {
fileExists(f) {
return destFiles.has(f);
},
end: () => {
},
delete: () => {
},
removeExport: () => {
readFile(f) {
throw new Error(`Unexpected readFile call: ${f}`);
}
}).resolvedModule?.resolvedFileName;
var getChange = (node) => {
if (ts3.isExportDeclaration(node) || ts3.isExportAssignment(node)) {
return {
code: node.getFullText(),
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
if ((ts3.isFunctionDeclaration(node) || ts3.isClassDeclaration(node)) && !node.name) {
return {
code: node.getFullText(),
isUnnamedDefaultExport: true,
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
const syntaxListIndex = node.getChildren().findIndex((n) => n.kind === ts3.SyntaxKind.SyntaxList);
const syntaxList = node.getChildren()[syntaxListIndex];
const nextSibling = node.getChildren()[syntaxListIndex + 1];
if (!syntaxList || !nextSibling) {
throw new Error("Unexpected syntax");
}
return {
code: node.getSourceFile().getFullText().slice(syntaxList.getStart(), nextSibling.getStart()),
span: {
start: syntaxList.getStart(),
length: nextSibling.getStart() - syntaxList.getStart()
}
};
};
var getNecessaryFiles = ({
var fn = ({
file,
content,
destFiles,
options = {}
}) => {
const imports = {};
const exports = [];
const sourceFile = ts3.createSourceFile(
file,
content,
ts3.ScriptTarget.ESNext,
true
);
const visit = (node) => {
if (ts3.isVariableStatement(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
const name = node.declarationList.declarations.map(
(d) => d.name.getText()
);
exports.push({
kind: ts3.SyntaxKind.VariableStatement,
name,
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isFunctionDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isClassDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
if (node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.DefaultKeyword)) {
exports.push({
kind: node.kind,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
} else {
exports.push({
kind: node.kind,
name: node.name?.getText() || "",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isTypeAliasDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
exports.push({
kind: node.kind,
name: node.name.getText(),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isExportAssignment(node) && !node.isExportEquals) {
exports.push({
kind: ts3.SyntaxKind.ExportAssignment,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
ts3.forEachChild(node, visit);
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && !node.moduleSpecifier) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: false,
start: node.getStart()
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
node.exportClause.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamespaceExport && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "namespace",
name: node.exportClause.name.text,
start: node.getStart(),
change: getChange(node)
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
}
return;
}
if (ts3.isExportDeclaration(node) && !node.exportClause && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "whole",
file: resolved || null,
specifier: node.moduleSpecifier.text,
start: node.getStart(),
change: getChange(node)
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push({ type: "wholeReexport", file });
}
return;
}
if (ts3.isImportDeclaration(node) && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamespaceImport) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamedImports) {
const namedImports = node.importClause?.namedBindings;
namedImports.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
if (node.importClause?.name) {
imports[resolved] ||= [];
imports[resolved]?.push("default");
}
return;
}
if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword && node.arguments[0] && ts3.isStringLiteral(node.arguments[0])) {
const resolved = resolve({
specifier: node.arguments[0].text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
node.forEachChild(visit);
};
sourceFile.forEachChild(visit);
return { imports, exports };
};
var parseFile = memoize(fn, {
key: (arg) => JSON.stringify({
file: arg.file,
content: arg.content,
destFiles: Array.from(arg.destFiles).sort(),
options: arg.options
})
});
// lib/util/findFileUsage.ts
var fallback = () => ({
from: /* @__PURE__ */ new Set(),
to: /* @__PURE__ */ new Set(),
data: {
depth: Infinity,
wholeReexportSpecifier: /* @__PURE__ */ new Map()
}
});
var ALL_EXPORTS_OF_UNKNOWN_FILE = "__all_exports_of_unknown_file__";
var getExportsOfFile = ({
targetFile,
dependencyGraph,
files
vertexes,
files,
options
}) => {
if (!dependencyGraph.vertexes.has(targetFile)) {
return new Set(files.filter((file) => !dependencyGraph.vertexes.has(file)));
}
const result = /* @__PURE__ */ new Set();
const result = [];
const stack = [targetFile];
while (stack.length > 0) {
const file = stack.pop();
if (!file) {
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
result.add(file);
const vertex = dependencyGraph.vertexes.get(file);
if (!vertex) {
continue;
const vertex = vertexes.get(item) || fallback();
const { exports } = parseFile({
file: item,
content: files.get(item) || "",
destFiles: new Set(vertex.to),
options
});
exports.forEach((it) => {
if (it.kind === ts4.SyntaxKind.ExportDeclaration && it.type === "whole") {
if (it.file) {
stack.push(it.file);
} else {
result.push(ALL_EXPORTS_OF_UNKNOWN_FILE);
}
return;
}
result.push(...Array.isArray(it.name) ? it.name : [it.name]);
});
}
return new Set(result);
};
var findFileUsage = ({
targetFile,
options,
vertexes,
files
}) => {
const result = [];
const exportsOfTargetFile = getExportsOfFile({
targetFile,
vertexes,
files,
options
});
const stack = [];
vertexes.get(targetFile)?.from.forEach(
(file) => stack.push({
file,
to: targetFile
})
);
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
for (const from of vertex.from) {
result.add(from);
const fromVertex = dependencyGraph.vertexes.get(from);
if (fromVertex && fromVertex.data.hasReexport) {
stack.push(from);
continue;
const { file, to } = item;
const vertex = vertexes.get(file) || fallback();
const { imports } = parseFile({
file,
content: files.get(file) || "",
destFiles: new Set(vertex.to),
options
});
const list = imports[to] || [];
list.forEach((it) => {
if (typeof it === "object" && it.type === "wholeReexport") {
const n = vertexes.get(it.file);
if (!n) {
return;
}
if (n.data.depth === 0) {
result.push("*");
return;
}
vertexes.get(it.file)?.from.forEach((f) => {
stack.push({
file: f,
to: it.file
});
});
return;
}
if (vertex.data.fromDynamic.has(from)) {
stack.push(from);
if (typeof it === "string") {
result.push(it);
return;
}
}
});
}
return result;
if (exportsOfTargetFile.has(ALL_EXPORTS_OF_UNKNOWN_FILE)) {
return new Set(result);
}
return new Set(
result.filter((it) => exportsOfTargetFile.has(it) || it === "*")
);
};
// lib/util/createProgram.ts
import ts5 from "typescript";
var createProgram = ({

@@ -404,3 +842,3 @@ fileService,

}
return ts4.createSourceFile(
return ts5.createSourceFile(
fileName,

@@ -411,3 +849,3 @@ fileService.get(fileName),

},
getDefaultLibFileName: (o) => ts4.getDefaultLibFilePath(o),
getDefaultLibFileName: (o) => ts5.getDefaultLibFilePath(o),
writeFile: (fileName, content) => {

@@ -419,7 +857,7 @@ fileService.set(fileName, content);

readFile: (fileName) => fileService.get(fileName),
getCanonicalFileName: (fileName) => ts4.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getCanonicalFileName: (fileName) => ts5.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
useCaseSensitiveFileNames: () => true,
getNewLine: () => "\n"
};
const program = ts4.createProgram(
const program = ts5.createProgram(
fileService.getFileNames(),

@@ -431,78 +869,365 @@ options,

};
var collectSkipFiles = ({
entrypoints,
program,
// lib/util/removeUnusedExport.ts
var stripExportKeyword = (syntaxList) => {
const file = ts6.createSourceFile(
"tmp.ts",
`${syntaxList} function f() {}`,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts6.isFunctionDeclaration(node)) {
return ts6.factory.createFunctionDeclaration(
node.modifiers?.filter(
(v) => v.kind !== ts6.SyntaxKind.ExportKeyword && v.kind !== ts6.SyntaxKind.DefaultKeyword
),
node.asteriskToken,
node.name,
node.typeParameters,
node.parameters,
node.type,
node.body
);
}
return ts6.visitEachChild(node, visitor, context);
};
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts6.transform(file, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const code = result ? printer.printFile(result).trim() : "";
const pos = code.indexOf("function");
return code.slice(0, pos);
};
var disabledEditTracker = {
start: () => {
},
end: () => {
},
delete: () => {
},
removeExport: () => {
}
};
var createLanguageService = ({
options,
projectRoot,
fileService
}) => {
const stack = [...entrypoints];
const result = [];
while (stack.length > 0) {
const file = stack.pop();
if (!file) {
break;
const languageService = ts6.createLanguageService({
getCompilationSettings() {
return options;
},
getScriptFileNames() {
return fileService.getFileNames();
},
getScriptVersion(fileName) {
return fileService.getVersion(fileName);
},
getScriptSnapshot(fileName) {
return ts6.ScriptSnapshot.fromString(fileService.get(fileName));
},
getCurrentDirectory() {
return projectRoot;
},
getDefaultLibFileName(o) {
return ts6.getDefaultLibFileName(o);
},
fileExists(name) {
return fileService.exists(name);
},
readFile(name) {
return fileService.get(name);
}
const sourceFile = program.getSourceFile(file);
if (!sourceFile) {
continue;
}
const visit = (node) => {
if (!ts4.isExportDeclaration(node)) {
return;
});
return languageService;
};
var updateExportDeclaration = (code, unused) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
code,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts6.isExportSpecifier(node) && unused.includes(node.getText(sourceFile))) {
return void 0;
}
if (!node.moduleSpecifier || !ts4.isStringLiteral(node.moduleSpecifier)) {
return;
}
if (node.exportClause) {
return;
}
const dest = getFileFromModuleSpecifierText({
specifier: node.moduleSpecifier.text,
program,
fileName: sourceFile.fileName,
fileService
});
if (dest) {
stack.push(dest);
result.push(dest);
}
return ts6.visitEachChild(node, visitor, context);
};
sourceFile.forEachChild(visit);
}
return result;
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts6.transform(sourceFile, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const printed = result ? printer.printFile(result).replace(/\n$/, "") : "";
const leading = code.match(/^([\s]+)/)?.[0] || "";
return `${leading}${printed}`;
};
var removeWholeExportSpecifier = (content, specifier, target) => {
const sourceFile = ts4.createSourceFile(
var getSpecifierPosition = (exportDeclaration) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
content,
target ?? ts4.ScriptTarget.Latest
exportDeclaration,
ts6.ScriptTarget.Latest
);
const result = [];
const result = /* @__PURE__ */ new Map();
const visit = (node) => {
if (result.length > 0) {
return;
if (ts6.isExportDeclaration(node) && node.exportClause?.kind === ts6.SyntaxKind.NamedExports) {
node.exportClause.elements.forEach((element) => {
result.set(
element.name.text,
element.getStart(sourceFile) - sourceFile.getStart()
);
});
}
if (ts4.isExportDeclaration(node) && node.moduleSpecifier && ts4.isStringLiteral(node.moduleSpecifier) && !node.exportClause && node.moduleSpecifier.text === specifier) {
result.push({
textChange: {
};
sourceFile.forEachChild(visit);
return result;
};
var processFile = ({
targetFile,
files,
vertexes,
deleteUnusedFile,
enableCodeFix,
options,
projectRoot
}) => {
const usage = findFileUsage({
targetFile,
vertexes,
files,
options
});
if (usage.has("*")) {
return {
operation: "edit",
content: files.get(targetFile) || "",
removedExports: []
};
}
const { exports } = parseFile({
file: targetFile,
content: files.get(targetFile) || "",
options,
destFiles: vertexes.get(targetFile)?.to || /* @__PURE__ */ new Set([])
});
if (usage.size === 0 && deleteUnusedFile && !exports.some((v) => "skip" in v && v.skip)) {
return {
operation: "delete"
};
}
const changes = [];
const logs = [];
exports.forEach((item) => {
switch (item.kind) {
case ts6.SyntaxKind.VariableStatement: {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
changes.push({
newText: "",
span: {
start: node.getFullStart(),
length: node.getFullWidth()
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
// todo: handle variable statement with multiple declarations properly
code: item.name.join(", ")
});
break;
}
case ts6.SyntaxKind.FunctionDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: item.change.isUnnamedDefaultExport ? "" : stripExportKeyword(item.change.code),
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.InterfaceDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.TypeAliasDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.ExportAssignment: {
if (item.skip || usage.has("default")) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: "default"
});
break;
}
case ts6.SyntaxKind.ExportDeclaration: {
switch (item.type) {
case "named": {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
const unused = item.name.filter((it) => !usage.has(it));
const count = item.name.length - unused.length;
changes.push({
newText: count > 0 ? updateExportDeclaration(item.change.code, unused) : "",
span: item.change.span
});
const position = getSpecifierPosition(item.change.code);
logs.push(
...unused.map((it) => ({
fileName: targetFile,
position: item.start + (position.get(it) || 0),
code: it
}))
);
break;
}
},
info: {
position: node.getStart(sourceFile),
code: node.getText(sourceFile)
case "namespace": {
if (usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case "whole": {
if (!item.file) {
break;
}
const parsed = parseFile({
file: item.file,
content: files.get(item.file) || "",
options,
destFiles: vertexes.get(item.file)?.to || /* @__PURE__ */ new Set([])
});
const exported = parsed.exports.flatMap(
(v) => "name" in v ? v.name : []
);
if (exported.some((v) => usage.has(v))) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: `export * from '${item.specifier}';`
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
break;
}
case ts6.SyntaxKind.ClassDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
});
if (changes.length === 0) {
const result2 = {
operation: "edit",
content: files.get(targetFile) || "",
removedExports: logs
};
return result2;
}
let content = applyTextChanges(files.get(targetFile) || "", changes);
const fileService = new MemoryFileService();
fileService.set(targetFile, content);
if (enableCodeFix && changes.length > 0) {
const languageService = createLanguageService({
options,
projectRoot,
fileService
});
while (true) {
fileService.set(targetFile, content);
const result2 = applyCodeFix({
fixId: fixIdDelete,
fileName: targetFile,
languageService
});
if (result2 === content) {
break;
}
content = result2;
}
};
sourceFile.forEachChild(visit);
if (!result[0]) {
return null;
fileService.set(targetFile, content);
content = applyCodeFix({
fixId: fixIdDeleteImports,
fileName: targetFile,
languageService
});
}
return {
info: result[0].info,
content: applyTextChanges(content, [result[0].textChange])
fileService.set(targetFile, content);
const result = {
operation: "edit",
content: fileService.get(targetFile),
removedExports: logs
};
return result;
};

@@ -521,8 +1246,3 @@ var removeUnusedExport = async ({

const program = createProgram({ fileService, options, projectRoot });
const skipFiles = collectSkipFiles({
entrypoints,
program,
fileService
});
const dependencyGraph = collectImports({
const dependencyGraph = createDependencyGraph({
fileService,

@@ -532,26 +1252,9 @@ program,

});
let filesOutsideOfGraphHasSkipComment = false;
const initialFiles = [];
for (const file of fileService.getFileNames()) {
if (entrypoints.includes(file)) {
continue;
}
const vertex = dependencyGraph.vertexes.get(file);
if (vertex) {
initialFiles.push({ file, depth: vertex.data.depth });
continue;
}
if (fileService.get(file).includes(IGNORE_COMMENT)) {
filesOutsideOfGraphHasSkipComment = true;
}
if (deleteUnusedFile && !filesOutsideOfGraphHasSkipComment && !skipFiles.includes(file)) {
editTracker.start(file, fileService.get(file));
editTracker.delete(file);
fileService.delete(file);
continue;
}
initialFiles.push({ file, depth: -1 });
}
const initialFiles = Array.from(
fileService.getFileNames().filter((file) => !entrypoints.includes(file))
).map((file) => ({
file,
depth: dependencyGraph.vertexes.get(file)?.data.depth || Infinity
}));
initialFiles.sort((a, b) => a.depth - b.depth);
const wholeReexportsToBeDeleted = [];
const taskManager = new TaskManager(async (c) => {

@@ -562,23 +1265,2 @@ if (!fileService.exists(c.file)) {

const vertex = dependencyGraph.vertexes.get(c.file);
if (vertex && vertex.data.fromDynamic.size > 0) {
await Promise.resolve();
if (c.signal.aborted) {
return;
}
editTracker.start(c.file, fileService.get(c.file));
editTracker.end(c.file);
return;
}
const necessaryFiles = getNecessaryFiles({
targetFile: c.file,
dependencyGraph,
files: fileService.getFileNames()
});
const files = Array.from(necessaryFiles).reduce(
(acc, cur) => ({
...acc,
[cur]: fileService.get(cur)
}),
{}
);
await Promise.resolve();

@@ -588,5 +1270,7 @@ if (c.signal.aborted) {

}
const result = await pool.run({
file: c.file,
files,
const fn2 = pool ? pool.run.bind(pool) : processFile;
const result = await fn2({
targetFile: c.file,
vertexes: dependencyGraph.eject(),
files: fileService.eject(),
deleteUnusedFile,

@@ -603,3 +1287,3 @@ enableCodeFix,

editTracker.start(c.file, fileService.get(c.file));
if (skipFiles.includes(c.file)) {
if (entrypoints.includes(c.file)) {
editTracker.end(c.file);

@@ -611,12 +1295,2 @@ break;

if (vertex) {
for (const v of vertex.from) {
const target = dependencyGraph.vertexes.get(v);
if (!target) {
continue;
}
const specifier = target.data.wholeReexportSpecifier.get(c.file);
if (target.data.hasReexport && specifier) {
wholeReexportsToBeDeleted.push({ file: v, specifier });
}
}
dependencyGraph.deleteVertex(c.file);

@@ -651,16 +1325,2 @@ if (recursive) {

await taskManager.execute(initialFiles.map((v) => v.file));
for (const item of wholeReexportsToBeDeleted) {
if (!fileService.exists(item.file)) {
continue;
}
const content = fileService.get(item.file);
const result = removeWholeExportSpecifier(content, item.specifier);
if (!result) {
continue;
}
fileService.set(item.file, result.content);
editTracker.start(item.file, content);
editTracker.removeExport(item.file, result.info);
editTracker.end(item.file);
}
};

@@ -675,2 +1335,7 @@

import { relative } from "node:path";
// lib/util/formatCount.ts
var formatCount = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
// lib/util/CliEditTracker.ts
var EditTrackerError = class extends Error {

@@ -804,22 +1469,20 @@ };

const result = [
deleteCount > 0 ? `delete ${deleteCount} file(s)` : "",
editCount > 0 ? `remove ${editCount} export(s)` : ""
deleteCount > 0 ? `delete ${formatCount(deleteCount, "file")}` : "",
editCount > 0 ? `remove ${formatCount(editCount, "export")}` : ""
].filter((t) => !!t);
if (result.length > 0) {
this.#logger.write(chalk.red.bold(`
\u2716 ${result.join(", ")}
this.#logger.write(chalk.red.bold(`\u2716 ${result.join(", ")}
`));
return;
}
this.#logger.write(chalk.green.bold("\n\u2714 all good!\n"));
this.#logger.write(chalk.green.bold("\u2714 all good!\n"));
return;
} else {
const result = [
deleteCount > 0 ? `deleted ${deleteCount} file(s)` : "",
editCount > 0 ? `removed ${editCount} export(s)` : ""
deleteCount > 0 ? `deleted ${formatCount(deleteCount, "file")}` : "",
editCount > 0 ? `removed ${formatCount(editCount, "export")}` : ""
].filter((t) => !!t);
this.#logger.write(
chalk.green.bold(
`
\u2714 ${result.length > 0 ? result.join(", ") : "all good!"}
`\u2714 ${result.length > 0 ? result.join(", ") : "all good!"}
`

@@ -878,10 +1541,10 @@ )

run(arg) {
return new Promise((resolve2, reject) => {
return new Promise((resolve3, reject) => {
const worker = this.#idle.pop();
if (!worker) {
this.#queue.push({ resolve: resolve2, reject, arg });
this.#queue.push({ resolve: resolve3, reject, arg });
return;
}
worker.current = {
resolve: resolve2,
resolve: resolve3,
reject

@@ -912,5 +1575,5 @@ };

}
const { resolve: resolve2, reject, arg } = this.#queue.shift();
const { resolve: resolve3, reject, arg } = this.#queue.shift();
worker.current = {
resolve: resolve2,
resolve: resolve3,
reject

@@ -954,3 +1617,2 @@ };

if (code === 0) {
console.log("here");
return;

@@ -974,2 +1636,5 @@ }

// lib/util/regex.ts
var dts = /\.d\.ts$/;
// lib/remove.ts

@@ -991,13 +1656,8 @@ var createNodeJsLogger = () => "isTTY" in stdout && stdout.isTTY ? {

recursive = false,
system = ts5.sys,
system = ts7.sys,
logger = createNodeJsLogger()
}) => {
const pool = new WorkerPool({
name: "processFile",
url: globalThis.__INTERNAL_WORKER_URL__ || new URL("./worker.js", import.meta.url).href
});
const editTracker = new CliEditTracker(logger, mode, projectRoot);
const { config, error } = ts5.readConfigFile(configPath, system.readFile);
const { config, error } = ts7.readConfigFile(configPath, system.readFile);
const relativeToCwd = (fileName) => relative2(cwd(), fileName).replaceAll("\\", "/");
const { options, fileNames } = ts5.parseJsonConfigFileContent(
const { options, fileNames } = ts7.parseJsonConfigFileContent(
config,

@@ -1008,7 +1668,4 @@ system,

if (!error) {
logger.write(
`${chalk2.blue("tsconfig")} ${chalk2.gray("using")} ${relativeToCwd(configPath)}
`
);
logger.write(`${chalk2.blue("tsconfig")} ${relativeToCwd(configPath)}
`);
}

@@ -1022,10 +1679,31 @@ const fileService = new MemoryFileService();

);
if (skip.filter((it) => it !== dts).length === 0) {
logger.write(
chalk2.bold.red(
"At least one pattern must be specified for the skip option\n"
)
);
system.exit(1);
return;
}
if (entrypoints.length === 0) {
logger.write(chalk2.bold.red("No files matched the skip pattern\n"));
system.exit(1);
return;
}
const editTracker = new CliEditTracker(logger, mode, projectRoot);
editTracker.setTotal(fileNames.length - entrypoints.length);
logger.write(
chalk2.gray(
`Project has ${fileNames.length} file(s), skipping ${entrypoints.length} file(s)...
`Project has ${formatCount(fileNames.length, "file")}, skipping ${formatCount(
entrypoints.length,
"file"
)}
`
)
);
const pool = new WorkerPool({
name: "processFile",
url: globalThis.__INTERNAL_WORKER_URL__ || new URL("./worker.js", import.meta.url).href
});
await removeUnusedExport({

@@ -1067,3 +1745,3 @@ fileService,

import { createRequire } from "node:module";
import { resolve } from "node:path";
import { resolve as resolve2 } from "node:path";
import { cwd as cwd2 } from "node:process";

@@ -1084,6 +1762,6 @@ var cli = cac("ts-remove-unused");

if (!options["includeD-ts"]) {
skip.push(new RegExp("\\.d\\.ts"));
skip.push(dts);
}
remove({
configPath: resolve(options.project || "./tsconfig.json"),
configPath: resolve2(options.project || "./tsconfig.json"),
skip,

@@ -1090,0 +1768,0 @@ mode: options.check ? "check" : "write",

// lib/remove.ts
import ts5 from "typescript";
import ts7 from "typescript";

@@ -34,6 +34,13 @@ // lib/util/MemoryFileService.ts

}
eject() {
const res = /* @__PURE__ */ new Map();
for (const [name, { content }] of this.#files) {
res.set(name, content);
}
return res;
}
};
// lib/util/removeUnusedExport.ts
import ts4 from "typescript";
import ts6 from "typescript";

@@ -75,22 +82,59 @@ // lib/util/applyTextChanges.ts

import ts from "typescript";
var fixIdDelete = "unusedIdentifier_delete";
var fixIdDeleteImports = "unusedIdentifier_deleteImports";
var filterChanges = ({
sourceFile,
textChanges
}) => {
const result = [];
const visit = (node) => {
if ((ts.isArrowFunction(node) || ts.isMethodDeclaration(node) || ts.isFunctionDeclaration(node)) && node.parameters.length > 0) {
const start = node.parameters[0]?.getStart();
const end = node.parameters[node.parameters.length - 1]?.getEnd();
if (typeof start === "number" && typeof end === "number") {
result.push({ start, end });
}
}
node.forEachChild(visit);
};
sourceFile.forEachChild(visit);
return textChanges.filter((change) => {
const start = change.span.start;
const end = change.span.start + change.span.length;
return !result.some((r) => r.start <= start && end <= r.end);
});
};
var applyCodeFix = ({
fixId,
languageService,
fileName
}) => {
const program = languageService.getProgram();
if (!program) {
throw new Error("program not found");
}
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error(`source file not found: ${fileName}`);
}
let content = sourceFile.getFullText();
const actions = languageService.getCombinedCodeFix(
{
type: "file",
fileName
},
fixId,
{},
{}
);
for (const change of actions.changes) {
const textChanges = fixId === fixIdDelete ? filterChanges({ sourceFile, textChanges: change.textChanges }) : change.textChanges;
content = applyTextChanges(content, textChanges);
}
return content;
};
// lib/util/getFileFromModuleSpecifierText.ts
// lib/util/createDependencyGraph.ts
import ts2 from "typescript";
var getFileFromModuleSpecifierText = ({
specifier,
fileName,
program,
fileService
}) => ts2.resolveModuleName(specifier, fileName, program.getCompilerOptions(), {
fileExists(f) {
return fileService.exists(f);
},
readFile(f) {
return fileService.get(f);
}
}).resolvedModule?.resolvedFileName;
// lib/util/collectImports.ts
import ts3 from "typescript";
// lib/util/Graph.ts

@@ -155,30 +199,37 @@ var Graph = class {

super(() => ({
depth: 0,
hasReexport: false,
fromDynamic: /* @__PURE__ */ new Set(),
// will not be updated when we delete a file,
// but it's fine since we only use it to find the used specifier from a given file path.
wholeReexportSpecifier: /* @__PURE__ */ new Map()
depth: 0
}));
}
deleteVertex(vertex) {
const selected = this.vertexes.get(vertex);
if (!selected) {
return;
eject() {
const map = /* @__PURE__ */ new Map();
for (const [k, v] of this.vertexes.entries()) {
map.set(k, {
from: new Set(v.from),
to: new Set(v.to),
data: {
depth: v.data.depth
}
});
}
for (const v of selected.to) {
const target = this.vertexes.get(v);
if (!target) {
continue;
}
target.data.fromDynamic.delete(vertex);
}
super.deleteVertex(vertex);
return map;
}
};
// lib/util/collectImports.ts
// lib/util/createDependencyGraph.ts
var getFileFromModuleSpecifierText = ({
specifier,
fileName,
program,
fileService
}) => ts2.resolveModuleName(specifier, fileName, program.getCompilerOptions(), {
fileExists(f) {
return fileService.exists(f);
},
readFile(f) {
return fileService.get(f);
}
}).resolvedModule?.resolvedFileName;
var getMatchingNode = (node) => {
if (ts3.isImportDeclaration(node)) {
if (ts3.isStringLiteral(node.moduleSpecifier)) {
if (ts2.isImportDeclaration(node)) {
if (ts2.isStringLiteral(node.moduleSpecifier)) {
return {

@@ -194,4 +245,4 @@ type: "import",

}
if (ts3.isExportDeclaration(node)) {
if (node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
if (ts2.isExportDeclaration(node)) {
if (node.moduleSpecifier && ts2.isStringLiteral(node.moduleSpecifier)) {
const result = {

@@ -209,4 +260,4 @@ type: "reexport",

}
if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword) {
if (node.arguments[0] && ts3.isStringLiteral(node.arguments[0])) {
if (ts2.isCallExpression(node) && node.expression.kind === ts2.SyntaxKind.ImportKeyword) {
if (node.arguments[0] && ts2.isStringLiteral(node.arguments[0])) {
return {

@@ -224,3 +275,3 @@ type: "dynamicImport",

};
var collectImports = ({
var createDependencyGraph = ({
fileService,

@@ -233,3 +284,3 @@ program,

const stack = [];
const visited = /* @__PURE__ */ new Set();
const untouched = new Set(fileService.getFileNames());
for (const entrypoint of entrypoints) {

@@ -244,6 +295,6 @@ stack.push({ file: entrypoint, depth: 0 });

const { file, depth } = item;
if (visited.has(file)) {
if (!untouched.has(file)) {
continue;
}
visited.add(file);
untouched.delete(file);
const sourceFile = program.getSourceFile(file);

@@ -253,3 +304,2 @@ if (!sourceFile) {

}
let hasReexport = false;
const visit = (node) => {

@@ -262,5 +312,2 @@ const match = getMatchingNode(node);

if (match.specifier) {
if (match.type === "reexport") {
hasReexport = true;
}
const dest = getFileFromModuleSpecifierText({

@@ -272,10 +319,4 @@ specifier: match.specifier,

});
if (match.type === "reexport" && dest && match.whole) {
graph.vertexes.get(file)?.data.wholeReexportSpecifier.set(dest, match.specifier);
}
if (dest && files.has(dest)) {
graph.addEdge(sourceFile.fileName, dest);
if (match.type === "dynamicImport") {
graph.vertexes.get(dest)?.data.fromDynamic.add(file);
}
stack.push({ file: dest, depth: depth + 1 });

@@ -289,6 +330,35 @@ }

if (vertex) {
vertex.data.hasReexport = hasReexport;
vertex.data.depth = depth;
}
}
for (const file of untouched.values()) {
const sourceFile = program.getSourceFile(file);
if (!sourceFile) {
continue;
}
const visit = (node) => {
const match = getMatchingNode(node);
if (!match) {
node.forEachChild(visit);
return;
}
if (match.specifier) {
const dest = getFileFromModuleSpecifierText({
specifier: match.specifier,
program,
fileName: sourceFile.fileName,
fileService
});
if (dest && files.has(dest)) {
graph.addEdge(sourceFile.fileName, dest);
}
return;
}
};
sourceFile.forEachChild(visit);
const vertex = graph.vertexes.get(file);
if (vertex) {
vertex.data.depth = Infinity;
}
}
return graph;

@@ -341,48 +411,416 @@ };

// lib/util/removeUnusedExport.ts
// lib/util/findFileUsage.ts
import ts4 from "typescript";
// lib/util/parseFile.ts
import ts3 from "typescript";
// lib/util/memoize.ts
var memoize = (fn2, { key }) => {
const cache = /* @__PURE__ */ new Map();
return (...args) => {
const k = key(...args);
if (cache.has(k)) {
return cache.get(k);
}
const result = fn2(...args);
cache.set(k, result);
return result;
};
};
// lib/util/parseFile.ts
var IGNORE_COMMENT = "ts-remove-unused-skip";
var disabledEditTracker = {
start: () => {
var getLeadingComment = (node) => {
const fullText = node.getSourceFile().getFullText();
const ranges = ts3.getLeadingCommentRanges(fullText, node.getFullStart());
if (!ranges) {
return "";
}
return ranges.map((range) => fullText.slice(range.pos, range.end)).join("");
};
var resolve = ({
specifier,
file,
destFiles,
options
}) => ts3.resolveModuleName(specifier, file, options, {
fileExists(f) {
return destFiles.has(f);
},
end: () => {
},
delete: () => {
},
removeExport: () => {
readFile(f) {
throw new Error(`Unexpected readFile call: ${f}`);
}
}).resolvedModule?.resolvedFileName;
var getChange = (node) => {
if (ts3.isExportDeclaration(node) || ts3.isExportAssignment(node)) {
return {
code: node.getFullText(),
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
if ((ts3.isFunctionDeclaration(node) || ts3.isClassDeclaration(node)) && !node.name) {
return {
code: node.getFullText(),
isUnnamedDefaultExport: true,
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
const syntaxListIndex = node.getChildren().findIndex((n) => n.kind === ts3.SyntaxKind.SyntaxList);
const syntaxList = node.getChildren()[syntaxListIndex];
const nextSibling = node.getChildren()[syntaxListIndex + 1];
if (!syntaxList || !nextSibling) {
throw new Error("Unexpected syntax");
}
return {
code: node.getSourceFile().getFullText().slice(syntaxList.getStart(), nextSibling.getStart()),
span: {
start: syntaxList.getStart(),
length: nextSibling.getStart() - syntaxList.getStart()
}
};
};
var getNecessaryFiles = ({
var fn = ({
file,
content,
destFiles,
options = {}
}) => {
const imports = {};
const exports = [];
const sourceFile = ts3.createSourceFile(
file,
content,
ts3.ScriptTarget.ESNext,
true
);
const visit = (node) => {
if (ts3.isVariableStatement(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
const name = node.declarationList.declarations.map(
(d) => d.name.getText()
);
exports.push({
kind: ts3.SyntaxKind.VariableStatement,
name,
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isFunctionDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isClassDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
if (node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.DefaultKeyword)) {
exports.push({
kind: node.kind,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
} else {
exports.push({
kind: node.kind,
name: node.name?.getText() || "",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isTypeAliasDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
exports.push({
kind: node.kind,
name: node.name.getText(),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
if (ts3.isExportAssignment(node) && !node.isExportEquals) {
exports.push({
kind: ts3.SyntaxKind.ExportAssignment,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
ts3.forEachChild(node, visit);
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && !node.moduleSpecifier) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: false,
start: node.getStart()
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
node.exportClause.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamespaceExport && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "namespace",
name: node.exportClause.name.text,
start: node.getStart(),
change: getChange(node)
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
}
return;
}
if (ts3.isExportDeclaration(node) && !node.exportClause && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "whole",
file: resolved || null,
specifier: node.moduleSpecifier.text,
start: node.getStart(),
change: getChange(node)
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push({ type: "wholeReexport", file });
}
return;
}
if (ts3.isImportDeclaration(node) && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamespaceImport) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamedImports) {
const namedImports = node.importClause?.namedBindings;
namedImports.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
if (node.importClause?.name) {
imports[resolved] ||= [];
imports[resolved]?.push("default");
}
return;
}
if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword && node.arguments[0] && ts3.isStringLiteral(node.arguments[0])) {
const resolved = resolve({
specifier: node.arguments[0].text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
node.forEachChild(visit);
};
sourceFile.forEachChild(visit);
return { imports, exports };
};
var parseFile = memoize(fn, {
key: (arg) => JSON.stringify({
file: arg.file,
content: arg.content,
destFiles: Array.from(arg.destFiles).sort(),
options: arg.options
})
});
// lib/util/findFileUsage.ts
var fallback = () => ({
from: /* @__PURE__ */ new Set(),
to: /* @__PURE__ */ new Set(),
data: {
depth: Infinity,
wholeReexportSpecifier: /* @__PURE__ */ new Map()
}
});
var ALL_EXPORTS_OF_UNKNOWN_FILE = "__all_exports_of_unknown_file__";
var getExportsOfFile = ({
targetFile,
dependencyGraph,
files
vertexes,
files,
options
}) => {
if (!dependencyGraph.vertexes.has(targetFile)) {
return new Set(files.filter((file) => !dependencyGraph.vertexes.has(file)));
}
const result = /* @__PURE__ */ new Set();
const result = [];
const stack = [targetFile];
while (stack.length > 0) {
const file = stack.pop();
if (!file) {
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
result.add(file);
const vertex = dependencyGraph.vertexes.get(file);
if (!vertex) {
continue;
const vertex = vertexes.get(item) || fallback();
const { exports } = parseFile({
file: item,
content: files.get(item) || "",
destFiles: new Set(vertex.to),
options
});
exports.forEach((it) => {
if (it.kind === ts4.SyntaxKind.ExportDeclaration && it.type === "whole") {
if (it.file) {
stack.push(it.file);
} else {
result.push(ALL_EXPORTS_OF_UNKNOWN_FILE);
}
return;
}
result.push(...Array.isArray(it.name) ? it.name : [it.name]);
});
}
return new Set(result);
};
var findFileUsage = ({
targetFile,
options,
vertexes,
files
}) => {
const result = [];
const exportsOfTargetFile = getExportsOfFile({
targetFile,
vertexes,
files,
options
});
const stack = [];
vertexes.get(targetFile)?.from.forEach(
(file) => stack.push({
file,
to: targetFile
})
);
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
for (const from of vertex.from) {
result.add(from);
const fromVertex = dependencyGraph.vertexes.get(from);
if (fromVertex && fromVertex.data.hasReexport) {
stack.push(from);
continue;
const { file, to } = item;
const vertex = vertexes.get(file) || fallback();
const { imports } = parseFile({
file,
content: files.get(file) || "",
destFiles: new Set(vertex.to),
options
});
const list = imports[to] || [];
list.forEach((it) => {
if (typeof it === "object" && it.type === "wholeReexport") {
const n = vertexes.get(it.file);
if (!n) {
return;
}
if (n.data.depth === 0) {
result.push("*");
return;
}
vertexes.get(it.file)?.from.forEach((f) => {
stack.push({
file: f,
to: it.file
});
});
return;
}
if (vertex.data.fromDynamic.has(from)) {
stack.push(from);
if (typeof it === "string") {
result.push(it);
return;
}
}
});
}
return result;
if (exportsOfTargetFile.has(ALL_EXPORTS_OF_UNKNOWN_FILE)) {
return new Set(result);
}
return new Set(
result.filter((it) => exportsOfTargetFile.has(it) || it === "*")
);
};
// lib/util/createProgram.ts
import ts5 from "typescript";
var createProgram = ({

@@ -398,3 +836,3 @@ fileService,

}
return ts4.createSourceFile(
return ts5.createSourceFile(
fileName,

@@ -405,3 +843,3 @@ fileService.get(fileName),

},
getDefaultLibFileName: (o) => ts4.getDefaultLibFilePath(o),
getDefaultLibFileName: (o) => ts5.getDefaultLibFilePath(o),
writeFile: (fileName, content) => {

@@ -413,7 +851,7 @@ fileService.set(fileName, content);

readFile: (fileName) => fileService.get(fileName),
getCanonicalFileName: (fileName) => ts4.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
getCanonicalFileName: (fileName) => ts5.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
useCaseSensitiveFileNames: () => true,
getNewLine: () => "\n"
};
const program = ts4.createProgram(
const program = ts5.createProgram(
fileService.getFileNames(),

@@ -425,78 +863,365 @@ options,

};
var collectSkipFiles = ({
entrypoints,
program,
// lib/util/removeUnusedExport.ts
var stripExportKeyword = (syntaxList) => {
const file = ts6.createSourceFile(
"tmp.ts",
`${syntaxList} function f() {}`,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts6.isFunctionDeclaration(node)) {
return ts6.factory.createFunctionDeclaration(
node.modifiers?.filter(
(v) => v.kind !== ts6.SyntaxKind.ExportKeyword && v.kind !== ts6.SyntaxKind.DefaultKeyword
),
node.asteriskToken,
node.name,
node.typeParameters,
node.parameters,
node.type,
node.body
);
}
return ts6.visitEachChild(node, visitor, context);
};
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts6.transform(file, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const code = result ? printer.printFile(result).trim() : "";
const pos = code.indexOf("function");
return code.slice(0, pos);
};
var disabledEditTracker = {
start: () => {
},
end: () => {
},
delete: () => {
},
removeExport: () => {
}
};
var createLanguageService = ({
options,
projectRoot,
fileService
}) => {
const stack = [...entrypoints];
const result = [];
while (stack.length > 0) {
const file = stack.pop();
if (!file) {
break;
const languageService = ts6.createLanguageService({
getCompilationSettings() {
return options;
},
getScriptFileNames() {
return fileService.getFileNames();
},
getScriptVersion(fileName) {
return fileService.getVersion(fileName);
},
getScriptSnapshot(fileName) {
return ts6.ScriptSnapshot.fromString(fileService.get(fileName));
},
getCurrentDirectory() {
return projectRoot;
},
getDefaultLibFileName(o) {
return ts6.getDefaultLibFileName(o);
},
fileExists(name) {
return fileService.exists(name);
},
readFile(name) {
return fileService.get(name);
}
const sourceFile = program.getSourceFile(file);
if (!sourceFile) {
continue;
}
const visit = (node) => {
if (!ts4.isExportDeclaration(node)) {
return;
});
return languageService;
};
var updateExportDeclaration = (code, unused) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
code,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts6.isExportSpecifier(node) && unused.includes(node.getText(sourceFile))) {
return void 0;
}
if (!node.moduleSpecifier || !ts4.isStringLiteral(node.moduleSpecifier)) {
return;
}
if (node.exportClause) {
return;
}
const dest = getFileFromModuleSpecifierText({
specifier: node.moduleSpecifier.text,
program,
fileName: sourceFile.fileName,
fileService
});
if (dest) {
stack.push(dest);
result.push(dest);
}
return ts6.visitEachChild(node, visitor, context);
};
sourceFile.forEachChild(visit);
}
return result;
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts6.transform(sourceFile, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const printed = result ? printer.printFile(result).replace(/\n$/, "") : "";
const leading = code.match(/^([\s]+)/)?.[0] || "";
return `${leading}${printed}`;
};
var removeWholeExportSpecifier = (content, specifier, target) => {
const sourceFile = ts4.createSourceFile(
var getSpecifierPosition = (exportDeclaration) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
content,
target ?? ts4.ScriptTarget.Latest
exportDeclaration,
ts6.ScriptTarget.Latest
);
const result = [];
const result = /* @__PURE__ */ new Map();
const visit = (node) => {
if (result.length > 0) {
return;
if (ts6.isExportDeclaration(node) && node.exportClause?.kind === ts6.SyntaxKind.NamedExports) {
node.exportClause.elements.forEach((element) => {
result.set(
element.name.text,
element.getStart(sourceFile) - sourceFile.getStart()
);
});
}
if (ts4.isExportDeclaration(node) && node.moduleSpecifier && ts4.isStringLiteral(node.moduleSpecifier) && !node.exportClause && node.moduleSpecifier.text === specifier) {
result.push({
textChange: {
};
sourceFile.forEachChild(visit);
return result;
};
var processFile = ({
targetFile,
files,
vertexes,
deleteUnusedFile,
enableCodeFix,
options,
projectRoot
}) => {
const usage = findFileUsage({
targetFile,
vertexes,
files,
options
});
if (usage.has("*")) {
return {
operation: "edit",
content: files.get(targetFile) || "",
removedExports: []
};
}
const { exports } = parseFile({
file: targetFile,
content: files.get(targetFile) || "",
options,
destFiles: vertexes.get(targetFile)?.to || /* @__PURE__ */ new Set([])
});
if (usage.size === 0 && deleteUnusedFile && !exports.some((v) => "skip" in v && v.skip)) {
return {
operation: "delete"
};
}
const changes = [];
const logs = [];
exports.forEach((item) => {
switch (item.kind) {
case ts6.SyntaxKind.VariableStatement: {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
changes.push({
newText: "",
span: {
start: node.getFullStart(),
length: node.getFullWidth()
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
// todo: handle variable statement with multiple declarations properly
code: item.name.join(", ")
});
break;
}
case ts6.SyntaxKind.FunctionDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: item.change.isUnnamedDefaultExport ? "" : stripExportKeyword(item.change.code),
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.InterfaceDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.TypeAliasDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.ExportAssignment: {
if (item.skip || usage.has("default")) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: "default"
});
break;
}
case ts6.SyntaxKind.ExportDeclaration: {
switch (item.type) {
case "named": {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
const unused = item.name.filter((it) => !usage.has(it));
const count = item.name.length - unused.length;
changes.push({
newText: count > 0 ? updateExportDeclaration(item.change.code, unused) : "",
span: item.change.span
});
const position = getSpecifierPosition(item.change.code);
logs.push(
...unused.map((it) => ({
fileName: targetFile,
position: item.start + (position.get(it) || 0),
code: it
}))
);
break;
}
},
info: {
position: node.getStart(sourceFile),
code: node.getText(sourceFile)
case "namespace": {
if (usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case "whole": {
if (!item.file) {
break;
}
const parsed = parseFile({
file: item.file,
content: files.get(item.file) || "",
options,
destFiles: vertexes.get(item.file)?.to || /* @__PURE__ */ new Set([])
});
const exported = parsed.exports.flatMap(
(v) => "name" in v ? v.name : []
);
if (exported.some((v) => usage.has(v))) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: `export * from '${item.specifier}';`
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
break;
}
case ts6.SyntaxKind.ClassDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
});
if (changes.length === 0) {
const result2 = {
operation: "edit",
content: files.get(targetFile) || "",
removedExports: logs
};
return result2;
}
let content = applyTextChanges(files.get(targetFile) || "", changes);
const fileService = new MemoryFileService();
fileService.set(targetFile, content);
if (enableCodeFix && changes.length > 0) {
const languageService = createLanguageService({
options,
projectRoot,
fileService
});
while (true) {
fileService.set(targetFile, content);
const result2 = applyCodeFix({
fixId: fixIdDelete,
fileName: targetFile,
languageService
});
if (result2 === content) {
break;
}
content = result2;
}
};
sourceFile.forEachChild(visit);
if (!result[0]) {
return null;
fileService.set(targetFile, content);
content = applyCodeFix({
fixId: fixIdDeleteImports,
fileName: targetFile,
languageService
});
}
return {
info: result[0].info,
content: applyTextChanges(content, [result[0].textChange])
fileService.set(targetFile, content);
const result = {
operation: "edit",
content: fileService.get(targetFile),
removedExports: logs
};
return result;
};

@@ -515,8 +1240,3 @@ var removeUnusedExport = async ({

const program = createProgram({ fileService, options, projectRoot });
const skipFiles = collectSkipFiles({
entrypoints,
program,
fileService
});
const dependencyGraph = collectImports({
const dependencyGraph = createDependencyGraph({
fileService,

@@ -526,26 +1246,9 @@ program,

});
let filesOutsideOfGraphHasSkipComment = false;
const initialFiles = [];
for (const file of fileService.getFileNames()) {
if (entrypoints.includes(file)) {
continue;
}
const vertex = dependencyGraph.vertexes.get(file);
if (vertex) {
initialFiles.push({ file, depth: vertex.data.depth });
continue;
}
if (fileService.get(file).includes(IGNORE_COMMENT)) {
filesOutsideOfGraphHasSkipComment = true;
}
if (deleteUnusedFile && !filesOutsideOfGraphHasSkipComment && !skipFiles.includes(file)) {
editTracker.start(file, fileService.get(file));
editTracker.delete(file);
fileService.delete(file);
continue;
}
initialFiles.push({ file, depth: -1 });
}
const initialFiles = Array.from(
fileService.getFileNames().filter((file) => !entrypoints.includes(file))
).map((file) => ({
file,
depth: dependencyGraph.vertexes.get(file)?.data.depth || Infinity
}));
initialFiles.sort((a, b) => a.depth - b.depth);
const wholeReexportsToBeDeleted = [];
const taskManager = new TaskManager(async (c) => {

@@ -556,23 +1259,2 @@ if (!fileService.exists(c.file)) {

const vertex = dependencyGraph.vertexes.get(c.file);
if (vertex && vertex.data.fromDynamic.size > 0) {
await Promise.resolve();
if (c.signal.aborted) {
return;
}
editTracker.start(c.file, fileService.get(c.file));
editTracker.end(c.file);
return;
}
const necessaryFiles = getNecessaryFiles({
targetFile: c.file,
dependencyGraph,
files: fileService.getFileNames()
});
const files = Array.from(necessaryFiles).reduce(
(acc, cur) => ({
...acc,
[cur]: fileService.get(cur)
}),
{}
);
await Promise.resolve();

@@ -582,5 +1264,7 @@ if (c.signal.aborted) {

}
const result = await pool.run({
file: c.file,
files,
const fn2 = pool ? pool.run.bind(pool) : processFile;
const result = await fn2({
targetFile: c.file,
vertexes: dependencyGraph.eject(),
files: fileService.eject(),
deleteUnusedFile,

@@ -597,3 +1281,3 @@ enableCodeFix,

editTracker.start(c.file, fileService.get(c.file));
if (skipFiles.includes(c.file)) {
if (entrypoints.includes(c.file)) {
editTracker.end(c.file);

@@ -605,12 +1289,2 @@ break;

if (vertex) {
for (const v of vertex.from) {
const target = dependencyGraph.vertexes.get(v);
if (!target) {
continue;
}
const specifier = target.data.wholeReexportSpecifier.get(c.file);
if (target.data.hasReexport && specifier) {
wholeReexportsToBeDeleted.push({ file: v, specifier });
}
}
dependencyGraph.deleteVertex(c.file);

@@ -645,16 +1319,2 @@ if (recursive) {

await taskManager.execute(initialFiles.map((v) => v.file));
for (const item of wholeReexportsToBeDeleted) {
if (!fileService.exists(item.file)) {
continue;
}
const content = fileService.get(item.file);
const result = removeWholeExportSpecifier(content, item.specifier);
if (!result) {
continue;
}
fileService.set(item.file, result.content);
editTracker.start(item.file, content);
editTracker.removeExport(item.file, result.info);
editTracker.end(item.file);
}
};

@@ -669,2 +1329,7 @@

import { relative } from "node:path";
// lib/util/formatCount.ts
var formatCount = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
// lib/util/CliEditTracker.ts
var EditTrackerError = class extends Error {

@@ -798,22 +1463,20 @@ };

const result = [
deleteCount > 0 ? `delete ${deleteCount} file(s)` : "",
editCount > 0 ? `remove ${editCount} export(s)` : ""
deleteCount > 0 ? `delete ${formatCount(deleteCount, "file")}` : "",
editCount > 0 ? `remove ${formatCount(editCount, "export")}` : ""
].filter((t) => !!t);
if (result.length > 0) {
this.#logger.write(chalk.red.bold(`
\u2716 ${result.join(", ")}
this.#logger.write(chalk.red.bold(`\u2716 ${result.join(", ")}
`));
return;
}
this.#logger.write(chalk.green.bold("\n\u2714 all good!\n"));
this.#logger.write(chalk.green.bold("\u2714 all good!\n"));
return;
} else {
const result = [
deleteCount > 0 ? `deleted ${deleteCount} file(s)` : "",
editCount > 0 ? `removed ${editCount} export(s)` : ""
deleteCount > 0 ? `deleted ${formatCount(deleteCount, "file")}` : "",
editCount > 0 ? `removed ${formatCount(editCount, "export")}` : ""
].filter((t) => !!t);
this.#logger.write(
chalk.green.bold(
`
\u2714 ${result.length > 0 ? result.join(", ") : "all good!"}
`\u2714 ${result.length > 0 ? result.join(", ") : "all good!"}
`

@@ -872,10 +1535,10 @@ )

run(arg) {
return new Promise((resolve, reject) => {
return new Promise((resolve2, reject) => {
const worker = this.#idle.pop();
if (!worker) {
this.#queue.push({ resolve, reject, arg });
this.#queue.push({ resolve: resolve2, reject, arg });
return;
}
worker.current = {
resolve,
resolve: resolve2,
reject

@@ -906,5 +1569,5 @@ };

}
const { resolve, reject, arg } = this.#queue.shift();
const { resolve: resolve2, reject, arg } = this.#queue.shift();
worker.current = {
resolve,
resolve: resolve2,
reject

@@ -948,3 +1611,2 @@ };

if (code === 0) {
console.log("here");
return;

@@ -968,2 +1630,5 @@ }

// lib/util/regex.ts
var dts = /\.d\.ts$/;
// lib/remove.ts

@@ -985,13 +1650,8 @@ var createNodeJsLogger = () => "isTTY" in stdout && stdout.isTTY ? {

recursive = false,
system = ts5.sys,
system = ts7.sys,
logger = createNodeJsLogger()
}) => {
const pool = new WorkerPool({
name: "processFile",
url: globalThis.__INTERNAL_WORKER_URL__ || new URL("./worker.js", import.meta.url).href
});
const editTracker = new CliEditTracker(logger, mode, projectRoot);
const { config, error } = ts5.readConfigFile(configPath, system.readFile);
const { config, error } = ts7.readConfigFile(configPath, system.readFile);
const relativeToCwd = (fileName) => relative2(cwd(), fileName).replaceAll("\\", "/");
const { options, fileNames } = ts5.parseJsonConfigFileContent(
const { options, fileNames } = ts7.parseJsonConfigFileContent(
config,

@@ -1002,7 +1662,4 @@ system,

if (!error) {
logger.write(
`${chalk2.blue("tsconfig")} ${chalk2.gray("using")} ${relativeToCwd(configPath)}
`
);
logger.write(`${chalk2.blue("tsconfig")} ${relativeToCwd(configPath)}
`);
}

@@ -1016,10 +1673,31 @@ const fileService = new MemoryFileService();

);
if (skip.filter((it) => it !== dts).length === 0) {
logger.write(
chalk2.bold.red(
"At least one pattern must be specified for the skip option\n"
)
);
system.exit(1);
return;
}
if (entrypoints.length === 0) {
logger.write(chalk2.bold.red("No files matched the skip pattern\n"));
system.exit(1);
return;
}
const editTracker = new CliEditTracker(logger, mode, projectRoot);
editTracker.setTotal(fileNames.length - entrypoints.length);
logger.write(
chalk2.gray(
`Project has ${fileNames.length} file(s), skipping ${entrypoints.length} file(s)...
`Project has ${formatCount(fileNames.length, "file")}, skipping ${formatCount(
entrypoints.length,
"file"
)}
`
)
);
const pool = new WorkerPool({
name: "processFile",
url: globalThis.__INTERNAL_WORKER_URL__ || new URL("./worker.js", import.meta.url).href
});
await removeUnusedExport({

@@ -1026,0 +1704,0 @@ fileService,

import { Graph } from './Graph.js';
export declare class DependencyGraph extends Graph<{
depth: number;
hasReexport: boolean;
fromDynamic: Set<string>;
wholeReexportSpecifier: Map<string, string>;
}> {
constructor();
deleteVertex(vertex: string): void;
eject(): Map<string, {
to: Set<string>;
from: Set<string>;
data: {
depth: number;
};
}>;
}
export type Vertexes = ReturnType<DependencyGraph['eject']>;

@@ -8,2 +8,3 @@ export interface FileService {

exists(name: string): boolean;
eject(): Map<string, string>;
}

@@ -11,2 +11,3 @@ import { FileService } from './FileService.js';

exists(name: string): boolean;
eject(): Map<string, string>;
}
import ts from 'typescript';
import { FileService } from './FileService.js';
import { EditTracker } from './EditTracker.js';
import { Vertexes } from './DependencyGraph.js';
import { WorkerPool } from './WorkerPool.js';
type RemovedExport = {
fileName: string;
position: number;
code: string;
};
export declare const processFile: ({ file, files, deleteUnusedFile, enableCodeFix, options, projectRoot, }: {
file: string;
files: {
[fileName: string]: string;
};
export declare const processFile: ({ targetFile, files, vertexes, deleteUnusedFile, enableCodeFix, options, projectRoot, }: {
targetFile: string;
vertexes: Map<string, {
to: Set<string>;
from: Set<string>;
data: {
depth: number;
};
}>;
files: Map<string, string>;
deleteUnusedFile: boolean;

@@ -20,7 +21,11 @@ enableCodeFix: boolean;

}) => {
operation: "delete";
} | {
operation: "edit";
content: string;
removedExports: RemovedExport[];
removedExports: {
fileName: string;
position: number;
code: string;
}[];
} | {
operation: "delete";
};

@@ -36,4 +41,27 @@ export declare const removeUnusedExport: ({ entrypoints, fileService, deleteUnusedFile, enableCodeFix, editTracker, options, projectRoot, pool, recursive, }: {

recursive: boolean;
pool: WorkerPool<typeof processFile>;
pool?: WorkerPool<({ targetFile, files, vertexes, deleteUnusedFile, enableCodeFix, options, projectRoot, }: {
targetFile: string;
vertexes: Map<string, {
to: Set<string>;
from: Set<string>;
data: {
depth: number;
};
}>;
files: Map<string, string>;
deleteUnusedFile: boolean;
enableCodeFix: boolean;
options: ts.CompilerOptions;
projectRoot: string;
}) => {
operation: "edit";
content: string;
removedExports: {
fileName: string;
position: number;
code: string;
}[];
} | {
operation: "delete";
}> | undefined;
}) => Promise<void>;
export {};
// lib/util/removeUnusedExport.ts
import ts4 from "typescript";
import ts6 from "typescript";

@@ -93,21 +93,5 @@ // lib/util/applyTextChanges.ts

// lib/util/getFileFromModuleSpecifierText.ts
// lib/util/createDependencyGraph.ts
import ts2 from "typescript";
var getFileFromModuleSpecifierText = ({
specifier,
fileName,
program,
fileService
}) => ts2.resolveModuleName(specifier, fileName, program.getCompilerOptions(), {
fileExists(f) {
return fileService.exists(f);
},
readFile(f) {
return fileService.get(f);
}
}).resolvedModule?.resolvedFileName;
// lib/util/collectImports.ts
import ts3 from "typescript";
// lib/util/MemoryFileService.ts

@@ -143,25 +127,36 @@ var MemoryFileService = class {

}
eject() {
const res = /* @__PURE__ */ new Map();
for (const [name, { content }] of this.#files) {
res.set(name, content);
}
return res;
}
};
// lib/util/removeUnusedExport.ts
var findFirstNodeOfKind = (root, kind) => {
let result;
const visitor = (node) => {
if (result) {
return;
// lib/util/findFileUsage.ts
import ts4 from "typescript";
// lib/util/parseFile.ts
import ts3 from "typescript";
// lib/util/memoize.ts
var memoize = (fn2, { key }) => {
const cache = /* @__PURE__ */ new Map();
return (...args) => {
const k = key(...args);
if (cache.has(k)) {
return cache.get(k);
}
if (node.kind === kind) {
result = node;
return;
}
ts4.forEachChild(node, visitor);
const result = fn2(...args);
cache.set(k, result);
return result;
};
ts4.forEachChild(root, visitor);
return result;
};
// lib/util/parseFile.ts
var IGNORE_COMMENT = "ts-remove-unused-skip";
var getLeadingComment = (node) => {
const sourceFile = node.getSourceFile();
const fullText = sourceFile.getFullText();
const ranges = ts4.getLeadingCommentRanges(fullText, node.getFullStart());
const fullText = node.getSourceFile().getFullText();
const ranges = ts3.getLeadingCommentRanges(fullText, node.getFullStart());
if (!ranges) {

@@ -172,200 +167,400 @@ return "";

};
var isTarget = (node) => {
if (ts4.isExportAssignment(node) || ts4.isExportSpecifier(node)) {
return true;
var resolve = ({
specifier,
file,
destFiles,
options
}) => ts3.resolveModuleName(specifier, file, options, {
fileExists(f) {
return destFiles.has(f);
},
readFile(f) {
throw new Error(`Unexpected readFile call: ${f}`);
}
if (ts4.isVariableStatement(node) || ts4.isFunctionDeclaration(node) || ts4.isInterfaceDeclaration(node) || ts4.isTypeAliasDeclaration(node) || ts4.isClassDeclaration(node)) {
const hasExportKeyword = !!findFirstNodeOfKind(
node,
ts4.SyntaxKind.ExportKeyword
);
if (!hasExportKeyword) {
return false;
}
return true;
}).resolvedModule?.resolvedFileName;
var getChange = (node) => {
if (ts3.isExportDeclaration(node) || ts3.isExportAssignment(node)) {
return {
code: node.getFullText(),
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
return false;
};
var findReferences = (node, service) => {
if (ts4.isVariableStatement(node)) {
const variableDeclaration = findFirstNodeOfKind(
node,
ts4.SyntaxKind.VariableDeclaration
);
if (!variableDeclaration) {
return void 0;
}
const references = service.findReferences(
node.getSourceFile().fileName,
variableDeclaration.getStart()
);
return references;
if ((ts3.isFunctionDeclaration(node) || ts3.isClassDeclaration(node)) && !node.name) {
return {
code: node.getFullText(),
isUnnamedDefaultExport: true,
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
};
}
if (ts4.isFunctionDeclaration(node) || ts4.isInterfaceDeclaration(node) || ts4.isTypeAliasDeclaration(node) || ts4.isExportSpecifier(node) || ts4.isClassDeclaration(node)) {
return service.findReferences(
node.getSourceFile().fileName,
node.getStart()
);
const syntaxListIndex = node.getChildren().findIndex((n) => n.kind === ts3.SyntaxKind.SyntaxList);
const syntaxList = node.getChildren()[syntaxListIndex];
const nextSibling = node.getChildren()[syntaxListIndex + 1];
if (!syntaxList || !nextSibling) {
throw new Error("Unexpected syntax");
}
if (ts4.isExportAssignment(node)) {
const defaultKeyword = node.getChildren().find((n) => n.kind === ts4.SyntaxKind.DefaultKeyword);
if (!defaultKeyword) {
return void 0;
return {
code: node.getSourceFile().getFullText().slice(syntaxList.getStart(), nextSibling.getStart()),
span: {
start: syntaxList.getStart(),
length: nextSibling.getStart() - syntaxList.getStart()
}
return service.findReferences(
node.getSourceFile().fileName,
defaultKeyword.getStart()
);
}
throw new Error(`unexpected node type: ${node}`);
};
};
var getReexportInFile = (file) => {
const result = [];
var fn = ({
file,
content,
destFiles,
options = {}
}) => {
const imports = {};
const exports = [];
const sourceFile = ts3.createSourceFile(
file,
content,
ts3.ScriptTarget.ESNext,
true
);
const visit = (node) => {
if (ts4.isExportSpecifier(node)) {
if (node.parent.parent.moduleSpecifier && ts4.isStringLiteral(node.parent.parent.moduleSpecifier)) {
result.push(node);
if (ts3.isVariableStatement(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
const name = node.declarationList.declarations.map(
(d) => d.name.getText()
);
exports.push({
kind: ts3.SyntaxKind.VariableStatement,
name,
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
node.forEachChild(visit);
};
file.forEachChild(visit);
return result;
};
var getAncestorFiles = (node, references, fileService, program, fileName) => {
const result = /* @__PURE__ */ new Set();
const referencesKeyValue = Object.fromEntries(
references.map((v) => [v.definition.fileName, v])
);
if (!node.parent.parent.moduleSpecifier || !ts4.isStringLiteral(node.parent.parent.moduleSpecifier)) {
return result;
}
let specifier = node.parent.parent.moduleSpecifier.text;
let currentFile = fileName;
while (specifier) {
const origin = getFileFromModuleSpecifierText({
specifier,
fileName: currentFile,
program,
fileService
});
if (!origin) {
break;
if (ts3.isFunctionDeclaration(node) || ts3.isInterfaceDeclaration(node) || ts3.isClassDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
if (node.modifiers?.some((m) => m.kind === ts3.SyntaxKind.DefaultKeyword)) {
exports.push({
kind: node.kind,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
} else {
exports.push({
kind: node.kind,
name: node.name?.getText() || "",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
}
ts3.forEachChild(node, visit);
return;
}
result.add(origin);
const referencedSymbol = referencesKeyValue[origin];
if (!referencedSymbol) {
break;
if (ts3.isTypeAliasDeclaration(node)) {
const isExported = node.modifiers?.some(
(m) => m.kind === ts3.SyntaxKind.ExportKeyword
);
if (isExported) {
exports.push({
kind: node.kind,
name: node.name.getText(),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
}
ts3.forEachChild(node, visit);
return;
}
const sourceFile = program.getSourceFile(origin);
if (!sourceFile) {
break;
if (ts3.isExportAssignment(node) && !node.isExportEquals) {
exports.push({
kind: ts3.SyntaxKind.ExportAssignment,
name: "default",
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
ts3.forEachChild(node, visit);
return;
}
const reexportSpecifiers = getReexportInFile(sourceFile);
const firstReferencedSymbol = referencedSymbol.references[0];
const reexportNode = firstReferencedSymbol ? reexportSpecifiers.find((r) => {
const start = firstReferencedSymbol.textSpan.start;
const end = start + firstReferencedSymbol.textSpan.length;
return r.getStart() === start && r.getEnd() === end;
}) : void 0;
if (reexportNode) {
if (!reexportNode.parent.parent.moduleSpecifier || !ts4.isStringLiteral(reexportNode.parent.parent.moduleSpecifier)) {
throw new Error("unexpected reexportNode");
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && !node.moduleSpecifier) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: !!getLeadingComment(node).includes(IGNORE_COMMENT),
start: node.getStart()
});
return;
}
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamedExports && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "named",
// we always collect the name not the propertyName because its for exports
name: node.exportClause.elements.map((element) => element.name.text),
change: getChange(node),
skip: false,
start: node.getStart()
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
node.exportClause.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
specifier = reexportNode.parent.parent.moduleSpecifier.text;
currentFile = origin;
} else {
specifier = null;
return;
}
}
return result;
};
var getUnusedExports = (languageService, fileName, fileService) => {
const nodes = [];
let isUsed = false;
const program = languageService.getProgram();
if (!program) {
throw new Error("program not found");
}
const sourceFile = program.getSourceFile(fileName);
if (!sourceFile) {
throw new Error("source file not found");
}
const visit = (node) => {
if (ts4.isExportDeclaration(node) && !node.exportClause) {
isUsed = true;
if (ts3.isExportDeclaration(node) && node.exportClause?.kind === ts3.SyntaxKind.NamespaceExport && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "namespace",
name: node.exportClause.name.text,
start: node.getStart(),
change: getChange(node)
});
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
}
return;
}
if (isTarget(node)) {
if (getLeadingComment(node).includes(IGNORE_COMMENT)) {
isUsed = true;
return;
if (ts3.isExportDeclaration(node) && !node.exportClause && node.moduleSpecifier && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
exports.push({
kind: ts3.SyntaxKind.ExportDeclaration,
type: "whole",
file: resolved || null,
specifier: node.moduleSpecifier.text,
start: node.getStart(),
change: getChange(node)
});
if (resolved) {
imports[resolved] ||= [];
imports[resolved]?.push({ type: "wholeReexport", file });
}
const references = findReferences(node, languageService);
if (!references) {
return;
}
if (ts3.isImportDeclaration(node) && ts3.isStringLiteral(node.moduleSpecifier)) {
const resolved = resolve({
specifier: node.moduleSpecifier.text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
if (ts4.isExportSpecifier(node) && node.parent.parent.moduleSpecifier) {
const ancestors = getAncestorFiles(
node,
references,
fileService,
program,
fileName
);
const count2 = references.flatMap((v) => v.references).filter(
(v) => v.fileName !== fileName && !ancestors.has(v.fileName)
).length;
if (count2 > 0) {
isUsed = true;
} else {
nodes.push(node);
}
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamespaceImport) {
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
const count = references.flatMap((v) => v.references).filter((v) => v.fileName !== fileName).length;
if (count > 0) {
isUsed = true;
} else {
nodes.push(node);
if (node.importClause?.namedBindings?.kind === ts3.SyntaxKind.NamedImports) {
const namedImports = node.importClause?.namedBindings;
namedImports.elements.forEach((element) => {
imports[resolved] ||= [];
imports[resolved]?.push(
element.propertyName?.text || element.name.text
);
});
}
if (node.importClause?.name) {
imports[resolved] ||= [];
imports[resolved]?.push("default");
}
return;
}
if (ts3.isCallExpression(node) && node.expression.kind === ts3.SyntaxKind.ImportKeyword && node.arguments[0] && ts3.isStringLiteral(node.arguments[0])) {
const resolved = resolve({
specifier: node.arguments[0].text,
destFiles,
file,
options
});
if (!resolved) {
return;
}
imports[resolved] ||= [];
imports[resolved]?.push("*");
return;
}
node.forEachChild(visit);
};
sourceFile.forEachChild(visit);
return { nodes, isUsed };
return { imports, exports };
};
var getUpdatedExportDeclaration = (exportDeclaration, removeTarget) => {
const tmpFile = ts4.createSourceFile(
"tmp.ts",
exportDeclaration.getText(),
exportDeclaration.getSourceFile().languageVersion
var parseFile = memoize(fn, {
key: (arg) => JSON.stringify({
file: arg.file,
content: arg.content,
destFiles: Array.from(arg.destFiles).sort(),
options: arg.options
})
});
// lib/util/findFileUsage.ts
var fallback = () => ({
from: /* @__PURE__ */ new Set(),
to: /* @__PURE__ */ new Set(),
data: {
depth: Infinity,
wholeReexportSpecifier: /* @__PURE__ */ new Map()
}
});
var ALL_EXPORTS_OF_UNKNOWN_FILE = "__all_exports_of_unknown_file__";
var getExportsOfFile = ({
targetFile,
vertexes,
files,
options
}) => {
const result = [];
const stack = [targetFile];
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
const vertex = vertexes.get(item) || fallback();
const { exports } = parseFile({
file: item,
content: files.get(item) || "",
destFiles: new Set(vertex.to),
options
});
exports.forEach((it) => {
if (it.kind === ts4.SyntaxKind.ExportDeclaration && it.type === "whole") {
if (it.file) {
stack.push(it.file);
} else {
result.push(ALL_EXPORTS_OF_UNKNOWN_FILE);
}
return;
}
result.push(...Array.isArray(it.name) ? it.name : [it.name]);
});
}
return new Set(result);
};
var findFileUsage = ({
targetFile,
options,
vertexes,
files
}) => {
const result = [];
const exportsOfTargetFile = getExportsOfFile({
targetFile,
vertexes,
files,
options
});
const stack = [];
vertexes.get(targetFile)?.from.forEach(
(file) => stack.push({
file,
to: targetFile
})
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts4.isExportSpecifier(node) && node.getText(tmpFile) === removeTarget.getText()) {
return void 0;
while (stack.length) {
const item = stack.pop();
if (!item) {
break;
}
const { file, to } = item;
const vertex = vertexes.get(file) || fallback();
const { imports } = parseFile({
file,
content: files.get(file) || "",
destFiles: new Set(vertex.to),
options
});
const list = imports[to] || [];
list.forEach((it) => {
if (typeof it === "object" && it.type === "wholeReexport") {
const n = vertexes.get(it.file);
if (!n) {
return;
}
if (n.data.depth === 0) {
result.push("*");
return;
}
vertexes.get(it.file)?.from.forEach((f) => {
stack.push({
file: f,
to: it.file
});
});
return;
}
return ts4.visitEachChild(node, visitor, context);
};
return ts4.visitEachChild(rootNode, visitor, context);
};
const result = ts4.transform(tmpFile, [transformer]).transformed[0];
const printer = ts4.createPrinter();
return result ? printer.printFile(result).trim() : "";
if (typeof it === "string") {
result.push(it);
return;
}
});
}
if (exportsOfTargetFile.has(ALL_EXPORTS_OF_UNKNOWN_FILE)) {
return new Set(result);
}
return new Set(
result.filter((it) => exportsOfTargetFile.has(it) || it === "*")
);
};
// lib/util/createProgram.ts
import ts5 from "typescript";
// lib/util/removeUnusedExport.ts
var stripExportKeyword = (syntaxList) => {
const file = ts4.createSourceFile(
const file = ts6.createSourceFile(
"tmp.ts",
`${syntaxList.getText()} function f() {}`,
syntaxList.getSourceFile().languageVersion
`${syntaxList} function f() {}`,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts4.isFunctionDeclaration(node)) {
return ts4.factory.createFunctionDeclaration(
if (ts6.isFunctionDeclaration(node)) {
return ts6.factory.createFunctionDeclaration(
node.modifiers?.filter(
(v) => v.kind !== ts4.SyntaxKind.ExportKeyword && v.kind !== ts4.SyntaxKind.DefaultKeyword
(v) => v.kind !== ts6.SyntaxKind.ExportKeyword && v.kind !== ts6.SyntaxKind.DefaultKeyword
),

@@ -380,8 +575,8 @@ node.asteriskToken,

}
return ts4.visitEachChild(node, visitor, context);
return ts6.visitEachChild(node, visitor, context);
};
return ts4.visitEachChild(rootNode, visitor, context);
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts4.transform(file, [transformer]).transformed[0];
const printer = ts4.createPrinter();
const result = ts6.transform(file, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const code = result ? printer.printFile(result).trim() : "";

@@ -391,106 +586,2 @@ const pos = code.indexOf("function");

};
var getTextChanges = (languageService, fileName, fileService) => {
const removedExports = [];
const changes = [];
let aborted = false;
const { nodes, isUsed } = getUnusedExports(
languageService,
fileName,
fileService
);
for (const node of nodes) {
if (aborted === true) {
break;
}
if (ts4.isExportSpecifier(node)) {
const specifierCount = Array.from(node.parent.elements).length;
if (specifierCount === 1) {
changes.push({
newText: "",
span: {
start: node.parent.parent.getFullStart(),
length: node.parent.parent.getFullWidth()
}
});
removedExports.push({
fileName,
position: node.parent.parent.getStart(),
code: node.parent.parent.getText()
});
continue;
}
aborted = true;
changes.push({
newText: getUpdatedExportDeclaration(node.parent.parent, node),
span: {
start: node.parent.parent.getStart(),
length: node.parent.parent.getWidth()
}
});
const from = node.parent.parent.moduleSpecifier ? ` from ${node.parent.parent.moduleSpecifier.getText()}` : "";
removedExports.push({
fileName,
position: node.getStart(),
code: `export { ${node.getText()} }${from};`
});
continue;
}
if (ts4.isExportAssignment(node)) {
changes.push({
newText: "",
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
});
removedExports.push({
fileName,
position: node.getStart(),
code: node.getText()
});
continue;
}
if (ts4.isFunctionDeclaration(node) || ts4.isClassDeclaration(node)) {
const identifier = node.getChildren().find((n) => n.kind === ts4.SyntaxKind.Identifier);
if (!identifier) {
changes.push({
newText: "",
span: {
start: node.getFullStart(),
length: node.getFullWidth()
}
});
const code = node.getText().slice(
0,
ts4.isFunctionDeclaration(node) ? node.getText().indexOf(")") + 1 : node.getText().indexOf("{") - 1
);
removedExports.push({
fileName,
position: node.getStart(),
code
});
continue;
}
}
const syntaxListIndex = node.getChildren().findIndex((n) => n.kind === ts4.SyntaxKind.SyntaxList);
const syntaxList = node.getChildren()[syntaxListIndex];
const syntaxListNextSibling = node.getChildren()[syntaxListIndex + 1];
if (!syntaxList || !syntaxListNextSibling) {
throw new Error("syntax list not found");
}
changes.push({
newText: ts4.isFunctionDeclaration(node) ? stripExportKeyword(syntaxList) : "",
span: {
start: syntaxList.getStart(),
length: syntaxListNextSibling.getStart() - syntaxList.getStart()
}
});
removedExports.push({
fileName,
position: node.getStart(),
code: findFirstNodeOfKind(node, ts4.SyntaxKind.Identifier)?.getText() || ""
});
}
return { changes, done: !aborted, isUsed, removedExports };
};
var createLanguageService = ({

@@ -501,3 +592,3 @@ options,

}) => {
const languageService = ts4.createLanguageService({
const languageService = ts6.createLanguageService({
getCompilationSettings() {

@@ -513,3 +604,3 @@ return options;

getScriptSnapshot(fileName) {
return ts4.ScriptSnapshot.fromString(fileService.get(fileName));
return ts6.ScriptSnapshot.fromString(fileService.get(fileName));
},

@@ -520,3 +611,3 @@ getCurrentDirectory() {

getDefaultLibFileName(o) {
return ts4.getDefaultLibFileName(o);
return ts6.getDefaultLibFileName(o);
},

@@ -532,5 +623,47 @@ fileExists(name) {

};
var updateExportDeclaration = (code, unused) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
code,
ts6.ScriptTarget.Latest
);
const transformer = (context) => (rootNode) => {
const visitor = (node) => {
if (ts6.isExportSpecifier(node) && unused.includes(node.getText(sourceFile))) {
return void 0;
}
return ts6.visitEachChild(node, visitor, context);
};
return ts6.visitEachChild(rootNode, visitor, context);
};
const result = ts6.transform(sourceFile, [transformer]).transformed[0];
const printer = ts6.createPrinter();
const printed = result ? printer.printFile(result).replace(/\n$/, "") : "";
const leading = code.match(/^([\s]+)/)?.[0] || "";
return `${leading}${printed}`;
};
var getSpecifierPosition = (exportDeclaration) => {
const sourceFile = ts6.createSourceFile(
"tmp.ts",
exportDeclaration,
ts6.ScriptTarget.Latest
);
const result = /* @__PURE__ */ new Map();
const visit = (node) => {
if (ts6.isExportDeclaration(node) && node.exportClause?.kind === ts6.SyntaxKind.NamedExports) {
node.exportClause.elements.forEach((element) => {
result.set(
element.name.text,
element.getStart(sourceFile) - sourceFile.getStart()
);
});
}
};
sourceFile.forEachChild(visit);
return result;
};
var processFile = ({
file,
targetFile,
files,
vertexes,
deleteUnusedFile,

@@ -541,36 +674,218 @@ enableCodeFix,

}) => {
const removedExports = [];
const fileService = new MemoryFileService();
for (const [fileName, content2] of Object.entries(files)) {
fileService.set(fileName, content2);
const usage = findFileUsage({
targetFile,
vertexes,
files,
options
});
if (usage.has("*")) {
return {
operation: "edit",
content: files.get(targetFile) || "",
removedExports: []
};
}
const languageService = createLanguageService({
const { exports } = parseFile({
file: targetFile,
content: files.get(targetFile) || "",
options,
projectRoot,
fileService
destFiles: vertexes.get(targetFile)?.to || /* @__PURE__ */ new Set([])
});
let content = fileService.get(file);
let isUsed = false;
do {
const result2 = getTextChanges(languageService, file, fileService);
removedExports.push(...result2.removedExports);
isUsed = result2.isUsed;
content = applyTextChanges(content, result2.changes);
fileService.set(file, content);
if (result2.done) {
break;
if (usage.size === 0 && deleteUnusedFile && !exports.some((v) => "skip" in v && v.skip)) {
return {
operation: "delete"
};
}
const changes = [];
const logs = [];
exports.forEach((item) => {
switch (item.kind) {
case ts6.SyntaxKind.VariableStatement: {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
// todo: handle variable statement with multiple declarations properly
code: item.name.join(", ")
});
break;
}
case ts6.SyntaxKind.FunctionDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: item.change.isUnnamedDefaultExport ? "" : stripExportKeyword(item.change.code),
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.InterfaceDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.TypeAliasDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case ts6.SyntaxKind.ExportAssignment: {
if (item.skip || usage.has("default")) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: "default"
});
break;
}
case ts6.SyntaxKind.ExportDeclaration: {
switch (item.type) {
case "named": {
if (item.skip || item.name.every((it) => usage.has(it))) {
break;
}
const unused = item.name.filter((it) => !usage.has(it));
const count = item.name.length - unused.length;
changes.push({
newText: count > 0 ? updateExportDeclaration(item.change.code, unused) : "",
span: item.change.span
});
const position = getSpecifierPosition(item.change.code);
logs.push(
...unused.map((it) => ({
fileName: targetFile,
position: item.start + (position.get(it) || 0),
code: it
}))
);
break;
}
case "namespace": {
if (usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
case "whole": {
if (!item.file) {
break;
}
const parsed = parseFile({
file: item.file,
content: files.get(item.file) || "",
options,
destFiles: vertexes.get(item.file)?.to || /* @__PURE__ */ new Set([])
});
const exported = parsed.exports.flatMap(
(v) => "name" in v ? v.name : []
);
if (exported.some((v) => usage.has(v))) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: `export * from '${item.specifier}';`
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
break;
}
case ts6.SyntaxKind.ClassDeclaration: {
if (item.skip || usage.has(item.name)) {
break;
}
changes.push({
newText: "",
span: item.change.span
});
logs.push({
fileName: targetFile,
position: item.start,
code: item.name
});
break;
}
default: {
throw new Error(`unexpected: ${item}`);
}
}
} while (true);
if (!isUsed && deleteUnusedFile) {
});
if (changes.length === 0) {
const result2 = {
operation: "delete"
operation: "edit",
content: files.get(targetFile) || "",
removedExports: logs
};
return result2;
}
if (enableCodeFix) {
let content = applyTextChanges(files.get(targetFile) || "", changes);
const fileService = new MemoryFileService();
fileService.set(targetFile, content);
if (enableCodeFix && changes.length > 0) {
const languageService = createLanguageService({
options,
projectRoot,
fileService
});
while (true) {
fileService.set(file, content);
fileService.set(targetFile, content);
const result2 = applyCodeFix({
fixId: fixIdDelete,
fileName: file,
fileName: targetFile,
languageService

@@ -583,14 +898,14 @@ });

}
fileService.set(file, content);
fileService.set(targetFile, content);
content = applyCodeFix({
fixId: fixIdDeleteImports,
fileName: file,
fileName: targetFile,
languageService
});
}
fileService.set(file, content);
fileService.set(targetFile, content);
const result = {
operation: "edit",
content: fileService.get(file),
removedExports
content: fileService.get(targetFile),
removedExports: logs
};

@@ -597,0 +912,0 @@ return result;

@@ -44,3 +44,3 @@ {

},
"version": "0.6.2"
"version": "0.7.0"
}

@@ -16,22 +16,29 @@ <h1 align="center">ts-remove-unused</h1>

## Introduction
## Install
When TypeScript's `compilerOptions.noUnusedLocals` is enabled, it's possible to detect declarations that are not referenced in your file.
```typescript
// TypeScript will throw error: 'a' is declared but its value is never read.
const a = 'a';
```bash
npm install @line/ts-remove-unused
```
However when this declaration is exported and is not referenced by any file in the project, it's difficult to recognize this.
TypeScript is a peer dependency.
```typescript
// no errors will be reported even if the declaration is not used across the entire project.
export const a = 'a';
## Quick Start
1. ๐Ÿ” Check your `tsconfig.json` โ€“ย Make sure `include` and `exclude` is configured thoroughly so that we can correctly detect what's "unused" in your project.
2. ๐Ÿ” Check your entrypoint files โ€“ What's the file that is the starting point for your code? Without this information, all files will be recognized as unnecessary. Usually it is some file like `src/main.ts` or maybe a group of files like `src/pages/*`.
3. ๐Ÿš€ Execute โ€“ Set a regex pattern that matches your entry files for `--skip`. You can optionally set `--project` to a path to your custom `tsconfig.json` if necessary.
```bash
npx @line/ts-remove-unused --skip 'src/main\.ts$'
```
This is when ts-remove-unused comes in handy. ts-remove-unused is a CLI tool built on top of TypeScript that finds unused exports and auto-fixes unused code.
> [!WARNING]
> THIS COMMAND WILL DELETE CODE FROM YOUR PROJECT. Using it in a git controlled environment is highly recommended. If you're just playing around use `--check`.
Here are some examples of how ts-remove-unused auto-fixes unused code.
## Examples
Here are some examples of how this tool will auto-fixe unused code.
<!-- prettier-ignore-start -->

@@ -100,19 +107,13 @@

ts-remove-unused supports various types of exports including variable declarations (`export const`, `export let`), function declarations, class declarations, interface declarations, type alias declarations, default exports and more...
ts-remove-unused works with all kinds of code: variables, functions, interfaces, classes, type aliases and more!
Now you don't have to worry about removing unused code by yourself!
## Usage
## Install
### Options
```bash
npm install @line/ts-remove-unused
```
<!-- prettier-ignore-start -->
TypeScript is a peer dependency so make sure that it's also installed.
## Usage
```
Usage:
$ ts-remove-unused
$ ts-remove-unused

@@ -126,23 +127,33 @@ Commands:

Options:
--project <file> Path to your tsconfig.json
--skip <regexp_pattern> Specify the regexp pattern to match files that should be skipped from transforming
--include-d-ts Include .d.ts files in target for transformation
--check Check if there are any unused exports without removing them
-h, --help Display this message
-v, --version Display version number
--project <file> Path to your tsconfig.json
--skip <regexp_pattern> Specify the regexp pattern to match files that should be skipped from transforming
--include-d-ts Include .d.ts files in target for transformation
--check Check if there are any unused exports without removing them
--experimental-recursive Recursively process files until there are no issue
-h, --help Display this message
-v, --version Display version number
```
<!-- prettier-ignore-end -->
ts-remove-unused's behavior heavily depends on your `tsconfig.json`. TypeScript's compiler internally holds the list of project files by parsing relevant rules such as `include` and `exclude`. ts-remove-unused scans through this list and searches for references to determine if an export/file is "unused". You may need to maintain/update your `tsconfig` (or you can create another file for `--project`) so that the set of covered files are right.
#### `--project`
Here's an example of using the CLI. Your entry point file must be skipped or else every file will be removed.
Specifies the `tsconfig.json` that is used to analyze your codebase. Defaults to `tsconfig.json` in your project root.
```bash
npx @line/ts-remove-unused --skip 'src/main\.ts'
npx @line/ts-remove-unused --skip tsconfig.client.json
```
> [!WARNING]
> THIS COMMAND WILL DELETE CODE FROM YOUR PROJECT. Using it in a git controlled environment is highly recommended. If you're just playing around use `--check`.
#### `--skip`
### Options
Skip files that match a given regex pattern. Note that you can pass multiple patterns.
```bash
npx @line/ts-remove-unused --skip 'src/main\.ts' --skip '/pages/'
```
#### `--include-d-ts`
By default, `.d.ts` files are skipped. If you want to include `.d.ts` files, use the `--include-d-ts` option.
#### `--check`

@@ -156,6 +167,2 @@

#### `--include-d-ts`
By default, `.d.ts` files are skipped. If you want to include `.d.ts` files, use the `--include-d-ts` option.
#### `--experimental-recursive`

@@ -191,14 +198,39 @@

The `--skip` option is also available to skip files that match a given regex pattern. Note that you can pass multiple patterns.
## Handling test files
```bash
npx @line/ts-remove-unused --skip 'src/main\.ts' --skip '/pages/'
If you have a separate tsconfig for tests using [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html), that would be great! ts-remove-unused will remove exports/files that exist for the sake of testing.
If you pass a `tsconfig.json` to the CLI that includes both the implementation and the test files, ts-remove-unused will remove your test files since they are not referenced by your entry point file (which is specified in `--skip`). You can avoid tests being deleted by passing a pattern that matches your test files to `--skip` in the meantime, but the recommended way is to use project references to ensure your TypeScript config is more robust and strict (not just for using this tool).
## Comparison
### TypeScript
If you enable `compilerOptions.noUnusedLocals`, declarations that are never read will be reported.
```typescript
// 'a' is declared but its value is never read.
const a = 'a';
```
## How does ts-remove-unused handle test files?
However, when you `export` it, no errors will be reported regardless of its usage within the project. ts-remove-unused's aim is to report/fix unused code while taking project wide usage into account.
If you have a separate tsconfig for tests using [Project References](https://www.typescriptlang.org/docs/handbook/project-references.html), that would be great! ts-remove-unused will remove exports/files that exist for the sake of testing.
### ESLint
If you pass a `tsconfig.json` to the CLI that includes both the implementation and the test files, ts-remove-unused will remove your test files since they are not referenced by your entry point file (which is specified in `--skip`). You can avoid tests being deleted by passing a pattern that matches your test files to `--skip` in the meantime, but the recommended way is to use project references to ensure your TypeScript config is more robust and strict (not just for using this tool).
ESLint will detect unused imports. Plugins such as `eslint-plugin-unused-imports` can also auto-fix this issue.
```typescript
// 'foo' is defined but never used.
import { foo } from './foo';
```
However, we can't detect unused exports. ESLint's architecture works in a file by file basis and was never intended to provide linting based on project-wide usage stats.
```typescript
// a lint rule that detects if this export is used within the project is unlikely to be introduced
export const a = 'a';
```
ts-remove-unused's main goal is to remove unused exports and delete unused modules, but it will also delete unused imports that are a result of removing an export declaration.
## Author

@@ -208,2 +240,6 @@

## Contributing
Contributions are welcomed!
## License

@@ -210,0 +246,0 @@

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with โšก๏ธ by Socket Inc