Skip to content

Commit

Permalink
Transform type exports to "export type" in order to comply with isola…
Browse files Browse the repository at this point in the history
…tedModules (#278)

When the TypeScript option isolatedModules is used, re-exported types must be exported as `export type { ... }` or `export { type ... }`. This fixup determines the kind of export (type or anything else) and adapts the export statement accordingly.

---------

Co-authored-by: Lucas Treffenstädt <[email protected]>
  • Loading branch information
mithodin and ltreffenstaedtc24 authored Aug 4, 2023
1 parent 3fa31b3 commit 1b228d0
Show file tree
Hide file tree
Showing 78 changed files with 267 additions and 140 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ jobs:
strategy:
fail-fast: false
matrix:
node: [14, 18]
node: [18, 20]
os: [ubuntu-latest, windows-latest]

name: Node ${{ matrix.node }} on ${{ matrix.os }}
Expand All @@ -24,11 +24,11 @@ jobs:
# frequently hangs for whatever reason,
# and we have no platform-specific code anyway…
- name: upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.node == 18
if: matrix.os == 'ubuntu-latest' && matrix.node == 20
timeout-minutes: 1
continue-on-error: true
run: bash <(curl -s https://codecov.io/bash) -t ${{secrets.CODECOV_TOKEN}} -B ${{ github.ref }} -f coverage/coverage-final.json
# test the minimum supported peer dependency version
- run: npm install typescript@4.1 [email protected]
- run: npm install typescript@4.5 [email protected]
# aka `npm test` without the `pretest/build`
- run: node .build/tests/index.js
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Changelog

## 6.0.0

**Compatibility Notice**:

This release raises the minimum required TypeScript version to **4.5** and the minimum required node.js version to **18**.

**Fixes**:

- Export types with `export { type T }` syntax.

## 5.3.1

**Fixes**:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"homepage": "https://github.com/Swatinem/rollup-plugin-dts#readme",
"engines": {
"node": ">=v14.21.3"
"node": ">=v18.16.0"
},
"type": "module",
"main": "./dist/rollup-plugin-dts.cjs",
Expand Down Expand Up @@ -60,7 +60,7 @@
},
"peerDependencies": {
"rollup": "^3.0",
"typescript": "^4.1 || ^5.0"
"typescript": "^4.5 || ^5.0"
},
"optionalDependencies": {
"@babel/code-frame": "^7.22.5"
Expand Down
149 changes: 149 additions & 0 deletions src/transform/ExportsFixer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import ts from "typescript";

type NamedExport = {
localName: string;
exportedName: string;
kind: 'type' | 'value';
};
type ExportDeclaration = {
location: {
start: number;
end: number;
};
exports: Array<NamedExport>
}

export class ExportsFixer {
private readonly DEBUG = !!(process.env.DTS_EXPORTS_FIXER_DEBUG);
constructor(private readonly source: ts.SourceFile) {}

public fix(): string {
const exports = this.findExports();
exports.sort((a, b) => a.location.start - b.location.start);
return this.getCodeParts(exports).join('');
}

private findExports(): Array<ExportDeclaration> {
const { rawExports, values, types} = this.getExportsAndLocals();

return rawExports.map((rawExport) => {
const elements = rawExport.elements.map((e) => {
const exportedName = e.name.text;
const localName = e.propertyName?.text ?? e.name.text;
const kind = types.some(node => node.getText() === localName) && !values.some(node => node.getText() === localName) ? 'type' as const : 'value' as const;
return {
exportedName,
localName,
kind
}
})
return {
location: {
start: rawExport.getStart(),
end: rawExport.getEnd(),
},
exports: elements
};
});
}

private getExportsAndLocals(statements: Iterable<ts.Node> = this.source.statements) {
const rawExports: Array<ts.NamedExports> = [];
const values: Array<ts.Identifier> = [];
const types: Array<ts.Identifier> = [];

const recurseInto = (subStatements: Iterable<ts.Node>) => {
const { rawExports: subExports, values: subValues, types: subTypes} = this.getExportsAndLocals(subStatements);
rawExports.push(...subExports);
values.push(...subValues);
types.push(...subTypes);
};

for (const statement of statements) {
this.DEBUG && console.log(statement.getText(), statement.kind);
if (ts.isImportDeclaration(statement)) {
continue;
}
if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) {
this.DEBUG && console.log(`${statement.name.getFullText()} is a type`);
types.push(statement.name);
continue;
}
if (
ts.isEnumDeclaration(statement) ||
ts.isFunctionDeclaration(statement) ||
ts.isClassDeclaration(statement) ||
ts.isVariableStatement(statement)
) {
if (ts.isVariableStatement(statement)) {
for (const declaration of statement.declarationList.declarations) {
if (ts.isIdentifier(declaration.name)) {
this.DEBUG && console.log(`${declaration.name.getFullText()} is a value (from var statement)`);
values.push(declaration.name);
}
}
} else {
if (statement.name) {
this.DEBUG && console.log(`${statement.name.getFullText()} is a value (from declaration)`);
values.push(statement.name);
}
}
continue;
}
if (ts.isModuleBlock(statement)) {
const subStatements = statement.statements;
recurseInto(subStatements);
continue;
}
if (ts.isModuleDeclaration(statement)) {
recurseInto(statement.getChildren());
continue;
}
if (ts.isExportDeclaration(statement)) {
if (statement.moduleSpecifier) {
continue;
}
if (statement.isTypeOnly) {
// no fixup neccessary
continue;
}
const exportClause = statement.exportClause;
if (!exportClause || !ts.isNamedExports(exportClause)) {
continue;
}
rawExports.push(exportClause);
continue;
}
this.DEBUG && console.log('unhandled statement', statement.getFullText(), statement.kind);
}
return { rawExports, values, types };
}

private createNamedExport(exportSpec: NamedExport, elideType = false) {
return `${!elideType && exportSpec.kind === 'type' ? 'type ' : ''}${exportSpec.localName}${exportSpec.localName === exportSpec.exportedName ? '' : ` as ${exportSpec.exportedName}`}`;
}

private getCodeParts(exports: Array<ExportDeclaration>) {
let cursor = 0;
const code = this.source.getFullText();
const parts: Array<string> = [];
for (const exportDeclaration of exports) {
const head = code.slice(cursor, exportDeclaration.location.start);
if (head.length > 0) {
parts.push(head);
}
parts.push(this.getExportStatement(exportDeclaration));

cursor = exportDeclaration.location.end;
}
if (cursor < code.length) {
parts.push(code.slice(cursor));
}
return parts;
}

private getExportStatement(exportDeclaration: ExportDeclaration) {
const isTypeOnly = exportDeclaration.exports.every((e) => e.kind === 'type') && exportDeclaration.exports.length > 0;
return `${isTypeOnly ? 'type ' : ''}{ ${exportDeclaration.exports.map((exp) => this.createNamedExport(exp, isTypeOnly)).join(', ')} }`
}
}
11 changes: 7 additions & 4 deletions src/transform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ts from "typescript";
import { NamespaceFixer } from "./NamespaceFixer.js";
import { preProcess } from "./preprocess.js";
import { convert } from "./Transformer.js";
import {ExportsFixer} from "./ExportsFixer.js";

function parse(fileName: string, code: string): ts.SourceFile {
return ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true);
Expand Down Expand Up @@ -91,8 +92,8 @@ export const transform = () => {
return { code, ast: converted.ast as any, map: preprocessed.code.generateMap() as any };
},

renderChunk(code, chunk, options) {
const source = parse(chunk.fileName, code);
renderChunk(inputCode, chunk, options) {
const source = parse(chunk.fileName, inputCode);
const fixer = new NamespaceFixer(source);

const typeReferences = new Set<string>();
Expand Down Expand Up @@ -120,15 +121,17 @@ export const transform = () => {
}
}

code = writeBlock(Array.from(fileReferences, (ref) => `/// <reference path="${ref}" />`));
let code = writeBlock(Array.from(fileReferences, (ref) => `/// <reference path="${ref}" />`));
code += writeBlock(Array.from(typeReferences, (ref) => `/// <reference types="${ref}" />`));
code += fixer.fix();

if (!code) {
code += "\nexport { }";
}

return { code, map: { mappings: "" } };
const exportsFixer = new ExportsFixer(parse(chunk.fileName, code));

return { code: exportsFixer.fix(), map: { mappings: "" } };
},
} satisfies Plugin;
};
Expand Down
2 changes: 1 addition & 1 deletion tests/testcases/call-signature/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ declare const fn: {
(arg: string): string;
staticProp: string;
};
export { I, fn };
export { type I, fn };
2 changes: 1 addition & 1 deletion tests/testcases/computed-property/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ type Klass = {
[0]: C;
[Dprop]: D;
};
export { Klass };
export type { Klass };
2 changes: 1 addition & 1 deletion tests/testcases/construct-signature/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
interface Foo {
new (): any;
}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/custom-tsconfig/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/export-as-namespace/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/export-default-interface/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo as default };
export type { Foo as default };
2 changes: 1 addition & 1 deletion tests/testcases/export-empty-object/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { }
export { }
2 changes: 1 addition & 1 deletion tests/testcases/export-multiple-vars/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,4 @@ declare const options: {
declare const params: {
normalize: (inVar: In) => Out;
};
export { In, Out, config, options, params };
export { type In, type Out, config, options, params };
2 changes: 1 addition & 1 deletion tests/testcases/export-simple-alias/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
interface Foo {}
declare type Bar = Foo;
export { Bar };
export type { Bar };
2 changes: 1 addition & 1 deletion tests/testcases/export-simple-interface/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo };
export type { Foo };
4 changes: 1 addition & 3 deletions tests/testcases/export-star-as/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
interface A {}
type foo_d_A = A;
declare namespace foo_d {
export {
foo_d_A as A,
};
export type { foo_d_A as A };
}
export { foo_d as foo };
2 changes: 1 addition & 1 deletion tests/testcases/export-star/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
interface B {}
declare class A {}
export { A, B };
export { A, type B };
2 changes: 1 addition & 1 deletion tests/testcases/externals-link/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
* Container element type usable for mouse/touch functions
*/
type DragContainerElement = HTMLElement | SVGSVGElement | SVGGElement;
export { DragContainerElement };
export type { DragContainerElement };
2 changes: 1 addition & 1 deletion tests/testcases/externals/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ export { ReactFragment } from 'react';
* Container element type usable for mouse/touch functions
*/
type DragContainerElement = HTMLElement | SVGSVGElement | SVGGElement;
export { DragContainerElement };
export type { DragContainerElement };
2 changes: 1 addition & 1 deletion tests/testcases/generic-extends/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ type AnimatedProps<T> = T;
type AnimatedComponent<T extends ElementType> = ForwardRefExoticComponent<
AnimatedProps<ComponentPropsWithRef<T>>
>;
export { AnimatedComponent, AnimatedProps };
export type { AnimatedComponent, AnimatedProps };
2 changes: 1 addition & 1 deletion tests/testcases/generics/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ declare function fn<T = G>(g: T, h: Gen<H>): void;
declare type TyFn = <T = J>(j: T, k: Gen<K>) => L;
declare type TyCtor = new <T = M>(m: T, n: Gen<N>) => O;
interface I2 extends Gen<P> {}
export { Cl, I1, I2, Ty, TyCtor, TyFn, fn };
export { Cl, type I1, type I2, type Ty, type TyCtor, type TyFn, fn };
2 changes: 1 addition & 1 deletion tests/testcases/implements-expression/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ interface MyComponentProps extends ns.Props<G> {
bar: string;
}
declare class MyComponent extends ns.Component<MyComponentProps> {}
export { MyComponent, MyComponentProps };
export { MyComponent, type MyComponentProps };
2 changes: 1 addition & 1 deletion tests/testcases/import-default-interface/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
interface Bar {}
interface Foo extends Bar {}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/import-no-import-clause/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/import-referenced-interface/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ interface Bar {}
interface Foo {
bar: Bar;
}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/import-renamed/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ interface Bar {}
interface Foo {
bar: Bar;
}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/import-unused-interface/expected.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
interface Foo {}
export { Foo };
export type { Foo };
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ interface Foo {
ns1: foo;
ns2: typeof foo;
}
export { Foo };
export type { Foo };
2 changes: 1 addition & 1 deletion tests/testcases/inline-import-generic/expected.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ interface Bar<T> {
interface Foo {
bar: Bar<number>;
}
export { Foo };
export type { Foo };
Loading

0 comments on commit 1b228d0

Please sign in to comment.