Skip to content

Commit

Permalink
feat: support ".ts" modules
Browse files Browse the repository at this point in the history
This compiles ".ts" modules into ".d.ts" modules in-memory and forwards them to Rollup.

This removes the need for intermediate files.

Fixes Swatinem#31
  • Loading branch information
aleclarson committed Jun 6, 2019
1 parent edf2eff commit afbfb54
Showing 1 changed file with 116 additions and 37 deletions.
153 changes: 116 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,86 @@
import * as ts from "typescript";
import { PluginImpl } from "rollup";
import * as path from "path";
import { PluginImpl, SourceDescription, InputOptions } from "rollup";
import { Transformer } from "./Transformer";
import { NamespaceFixer } from "./NamespaceFixer";

const dts = ".d.ts";
const tsx = /\.tsx?$/;

const formatHost: ts.FormatDiagnosticsHost = {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
getCanonicalFileName: ts.sys.useCaseSensitiveFileNames ? f => f : f => f.toLowerCase(),
};

const getCompilerOptions = (input: string): ts.CompilerOptions => {
const configPath = ts.findConfigFile(path.dirname(input), ts.sys.fileExists);
if (!configPath) {
return {};
}
const { config, error } = ts.readConfigFile(configPath, ts.sys.readFile);
if (error) {
console.error(ts.formatDiagnostic(error, formatHost));
return {};
}
const { options, errors } = ts.parseJsonConfigFileContent(config, ts.sys, path.dirname(configPath));
if (errors.length) {
console.error(ts.formatDiagnostics(errors, formatHost));
return {};
}
return options;
};

const createProgram = ({ input }: InputOptions) => {
if (typeof input !== "string") {
throw new TypeError('"input" option must be a string');
}
input = path.resolve(input);
const compilerOptions: ts.CompilerOptions = {
...getCompilerOptions(input),
// Ensure ".d.ts" modules are generated
declaration: true,
// Skip ".js" generation
emitDeclarationOnly: true,
// Skip code generation when error occurs
noEmitOnError: true,
// Avoid extra work
checkJs: false,
sourceMap: false,
skipLibCheck: true,
// Ensure TS2742 errors are visible
preserveSymlinks: true,
};
const host = ts.createCompilerHost(compilerOptions, true);
return ts.createProgram([input], compilerOptions, host);
};

// Parse a TypeScript module into an ESTree program.
const transformFile = (input: ts.SourceFile): SourceDescription => {
const transformer = new Transformer(input);
const { ast, fixups } = transformer.transform();

// NOTE(swatinem):
// hm, typescript generates `export default` without a declare,
// but rollup moves the `export default` to a different place, which leaves
// the function declaration without a `declare`.
// Well luckily both words have the same length, haha :-D
let code = input.getText();
code = code.replace(/(export\s+)default(\s+(function|class))/m, "$1declare$2");
for (const fixup of fixups) {
code = code.slice(0, fixup.range.start) + fixup.identifier + code.slice(fixup.range.end);
}

return { code, ast };
};

const plugin: PluginImpl<{}> = () => {
let program: ts.Program;
return {
name: "dts",

options(options) {
program = createProgram(options);
return {
...options,
treeshake: {
Expand All @@ -20,8 +93,8 @@ const plugin: PluginImpl<{}> = () => {
outputOptions(options) {
return {
...options,
chunkFileNames: options.chunkFileNames || "[name]-[hash].d.ts",
entryFileNames: options.entryFileNames || "[name].d.ts",
chunkFileNames: options.chunkFileNames || "[name]-[hash]" + dts,
entryFileNames: options.entryFileNames || "[name]" + dts,
format: "es",
exports: "named",
compact: false,
Expand All @@ -32,6 +105,43 @@ const plugin: PluginImpl<{}> = () => {
};
},

load(id) {
if (!tsx.test(id)) {
return null;
}
const source = program.getSourceFile(id);
if (!source) {
return null;
}
if (id.endsWith(dts)) {
return transformFile(source);
}
const ambientId = id.replace(tsx, dts);
const ambientDefs = program.getSourceFile(ambientId);
if (ambientDefs) {
return transformFile(ambientDefs);
}
// Transform ".ts" modules into ".d.ts" in-memory!
let generated!: SourceDescription;
const { emitSkipped, diagnostics } = program.emit(
source,
(_, code) => {
const ambientDefs = ts.createSourceFile(ambientId, code, ts.ScriptTarget.Latest, true);
generated = transformFile(ambientDefs);
},
undefined, // cancellationToken
true, // emitOnlyDtsFiles
);
if (emitSkipped) {
const errors = diagnostics.filter(diag => diag.category === ts.DiagnosticCategory.Error);
if (errors.length) {
console.error(ts.formatDiagnostics(errors, formatHost));
this.error("Failed to compile. Check the logs above.");
}
}
return generated;
},

resolveId(source, importer) {
if (!importer) {
return;
Expand All @@ -45,40 +155,9 @@ const plugin: PluginImpl<{}> = () => {

// here, we define everything that comes from `node_modules` as `external`.
// maybe its a good idea to introduce an option for this?
if (resolvedModule.isExternalLibraryImport) {
return { id: source, external: true };
}
let id = resolvedModule.resolvedFileName;
const { extension } = resolvedModule;
if (extension !== ".d.ts") {
// ts resolves `.ts`/`.tsx` files before `.d.ts`
id = id.slice(0, id.length - extension.length) + ".d.ts";
}

return { id };
},

transform(code, id) {
if (!id.endsWith(".d.ts")) {
this.error("`rollup-plugin-dts` can only deal with `.d.ts` files.");
return;
}

const dtsSource = ts.createSourceFile(id, code, ts.ScriptTarget.Latest, true);
const converter = new Transformer(dtsSource);
const { ast, fixups } = converter.transform();

// NOTE(swatinem):
// hm, typescript generates `export default` without a declare,
// but rollup moves the `export default` to a different place, which leaves
// the function declaration without a `declare`.
// Well luckily both words have the same length, haha :-D
code = code.replace(/(export\s+)default(\s+(function|class))/m, "$1declare$2");
for (const fixup of fixups) {
code = code.slice(0, fixup.range.start) + fixup.identifier + code.slice(fixup.range.end);
}

return { code, ast };
return resolvedModule.isExternalLibraryImport
? { id: source, external: true }
: { id: resolvedModule.resolvedFileName };
},

renderChunk(code, chunk) {
Expand Down

0 comments on commit afbfb54

Please sign in to comment.