Skip to content

Commit

Permalink
fix: handle deeply nested whole exports
Browse files Browse the repository at this point in the history
  • Loading branch information
FredericEspiau committed Dec 19, 2024
1 parent 7c1120b commit 1e5eda1
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 159 deletions.
20 changes: 20 additions & 0 deletions lib/util/edit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,26 @@ export const b = 'b';`,
assert.equal(fileService.exists('/app/a_reexport.ts'), false);
assert.equal(fileService.exists('/app/a.ts'), false);
});

it('should look for deeply nested whole re-export without removing files', () => {
const fileService = new MemoryFileService();
fileService.set('/app/main.ts', `import { c } from './a';`);
fileService.set('/app/a.ts', `export * from './b';`);
fileService.set('/app/b.ts', `export * from './c';`);
fileService.set('/app/c.ts', `export const c = 'c';`);

edit({
fileService,
recursive,
deleteUnusedFile: true,
entrypoints: ['/app/main.ts'],
});

assert.equal(fileService.get('/app/main.ts'), `import { c } from './a';`);
assert.equal(fileService.get('/app/a.ts'), `export * from './b';`);
assert.equal(fileService.get('/app/b.ts'), `export * from './c';`);
assert.equal(fileService.get('/app/c.ts'), `export const c = 'c';`);
});
});

describe('namespace export declaration', () => {
Expand Down
83 changes: 63 additions & 20 deletions lib/util/edit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { MemoryFileService } from './MemoryFileService.js';
import { findFileUsage } from './findFileUsage.js';
import { parseFile } from './parseFile.js';
import { Output } from './Output.js';
import * as Export from './export.js';

const transform = (
source: string,
Expand Down Expand Up @@ -92,7 +93,7 @@ const createLanguageService = ({
projectRoot: string;
fileService: FileService;
}) => {
const languageService = ts.createLanguageService({
return ts.createLanguageService({
getCompilationSettings() {
return options;
},
Expand All @@ -118,8 +119,6 @@ const createLanguageService = ({
return fileService.get(name);
},
});

return languageService;
};

const updateExportDeclaration = (code: string, unused: string[]) => {
Expand Down Expand Up @@ -148,7 +147,7 @@ const updateExportDeclaration = (code: string, unused: string[]) => {

const printer = ts.createPrinter();
const printed = result ? printer.printFile(result).replace(/\n$/, '') : '';
const leading = code.match(/^([\s]+)/)?.[0] || '';
const leading = code.match(/^(\s+)/)?.[0] || '';

return `${leading}${printed}`;
};
Expand Down Expand Up @@ -181,6 +180,58 @@ const getSpecifierPosition = (exportDeclaration: string) => {
return result;
};

/**
* Retrieves the names of the exports from a whole export declaration.
* For each whole export declaration, it will recursively get the names of the exports from the file it points to.
*
* No need to memoize this function because `parseFile` already memoizes the file parsing.
*/
const deeplyGetExportNames = ({
item,
files,
fileNames,
options,
filesAlreadyVisited = new Set<string>(),
}: {
item: Export.WholeExportDeclaration.FileFound;
files: Map<string, string>;
fileNames: Set<string>;
options: ts.CompilerOptions;
filesAlreadyVisited?: Set<string>;
}): string[] => {
if (filesAlreadyVisited.has(item.file)) {
return [];
}

const parsed = parseFile({
file: item.file,
content: files.get(item.file) || '',
options,
destFiles: fileNames,
});

const deepExportNames = parsed.exports
.filter(
(v) =>
Export.isWholeExportDeclaration(v) &&
Export.WholeExportDeclaration.isFileFound(v),
)
.flatMap((v) =>
deeplyGetExportNames({
item: v,
files,
fileNames,
options,
filesAlreadyVisited: filesAlreadyVisited.add(item.file),
}),
);

return parsed.exports
.filter(Export.isNamedExport)
.flatMap((v) => v.name)
.concat(deepExportNames);
};

const processFile = ({
targetFile,
files,
Expand Down Expand Up @@ -424,23 +475,19 @@ const processFile = ({
break;
}
case 'whole': {
if (!item.file) {
if (!Export.WholeExportDeclaration.isFileFound(item)) {
// whole export is directed towards a file that is not in the project
break;
}

const parsed = parseFile({
file: item.file,
content: files.get(item.file) || '',
const exportNames = deeplyGetExportNames({
item,
files,
fileNames,
options,
destFiles: fileNames,
});

const exported = parsed.exports.flatMap((v) =>
'name' in v ? v.name : [],
);

if (exported.some((v) => usage.has(v))) {
if (exportNames.some((v) => usage.has(v))) {
break;
}

Expand Down Expand Up @@ -536,13 +583,11 @@ export {};\n`,
}

if (changes.length === 0) {
const result = {
return {
operation: 'edit' as const,
content: files.get(targetFile) || '',
removedExports: logs,
};

return result;
}

let content = applyTextChanges(files.get(targetFile) || '', changes);
Expand Down Expand Up @@ -582,13 +627,11 @@ export {};\n`,

fileService.set(targetFile, content);

const result = {
return {
operation: 'edit' as const,
content: fileService.get(targetFile),
removedExports: logs,
};

return result;
};

export const edit = ({
Expand Down
16 changes: 16 additions & 0 deletions lib/util/export.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import ts from 'typescript';

import { NamedExport } from './export/namedExport.js';
import { WholeExportDeclaration } from './export/wholeExportDeclaration.js';

export * as NamedExport from './export/namedExport.js';
export * as WholeExportDeclaration from './export/wholeExportDeclaration.js';

export type Export = NamedExport | WholeExportDeclaration;

export const isNamedExport = (v: Export): v is NamedExport => 'name' in v;

export const isWholeExportDeclaration = (
v: Export,
): v is WholeExportDeclaration =>
v.kind === ts.SyntaxKind.ExportDeclaration && v.type === 'whole';
141 changes: 141 additions & 0 deletions lib/util/export/namedExport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import ts from 'typescript';

export type ClassDeclaration = {
kind: ts.SyntaxKind.ClassDeclaration;
name: string;
change: {
code: string;
isUnnamedDefaultExport?: boolean;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type EnumDeclaration = {
kind: ts.SyntaxKind.EnumDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type ExportAssignment = {
kind: ts.SyntaxKind.ExportAssignment;
name: 'default';
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type FunctionDeclaration = {
kind: ts.SyntaxKind.FunctionDeclaration;
name: string;
change: {
code: string;
isUnnamedDefaultExport?: boolean;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type InterfaceDeclaration = {
kind: ts.SyntaxKind.InterfaceDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type NameExportDeclaration = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'named';
name: string[];
skip: boolean;
change: {
code: string;
span: {
start: number;
length: number;
};
};
start: number;
};

export type NamespaceExportDeclaration = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'namespace';
name: string;
start: number;
change: {
code: string;
span: {
start: number;
length: number;
};
};
};

export type TypeAliasDeclaration = {
kind: ts.SyntaxKind.TypeAliasDeclaration;
name: string;
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type VariableStatement = {
kind: ts.SyntaxKind.VariableStatement;
name: string[];
change: {
code: string;
span: {
start: number;
length: number;
};
};
skip: boolean;
start: number;
};

export type NamedExport =
| ClassDeclaration
| EnumDeclaration
| ExportAssignment
| FunctionDeclaration
| InterfaceDeclaration
| NameExportDeclaration
| NamespaceExportDeclaration
| TypeAliasDeclaration
| VariableStatement;
43 changes: 43 additions & 0 deletions lib/util/export/wholeExportDeclaration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import ts from 'typescript';

/**
* Whole export when the file is found within the destFiles
*/
export type FileFound = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'whole';
file: string;
specifier: string;
start: number;
change: {
code: string;
span: {
start: number;
length: number;
};
};
};

/**
* Whole export when the file is not found within the destFiles, i.e. the file is not part of the project
*/
export type FileNotFound = {
kind: ts.SyntaxKind.ExportDeclaration;
type: 'whole';
file: null;
specifier: string;
start: number;
change: {
code: string;
span: {
start: number;
length: number;
};
};
};

export type WholeExportDeclaration = FileFound | FileNotFound;

export const isFileFound = (
exportDeclaration: WholeExportDeclaration,
): exportDeclaration is FileFound => exportDeclaration.file !== null;
Loading

0 comments on commit 1e5eda1

Please sign in to comment.