Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support ".ts" modules #32

Merged
merged 3 commits into from
Jun 7, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 154 additions & 37 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,117 @@
import * as ts from "typescript";
import { PluginImpl } from "rollup";
import * as path from "path";
import { PluginImpl, SourceDescription } 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(),
};

function 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 = (main: string) => {
main = path.resolve(main);
const compilerOptions: ts.CompilerOptions = {
...getCompilerOptions(main),
// 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([main], 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<{}> = () => {
// There exists one Program object per entry point,
// except when all entry points are ".d.ts" modules.
const programs = new Map<string, ts.Program>();
const getModule = (fileName: string) => {
let source: ts.SourceFile | null = null;
let program: ts.Program | null = null;
if (programs.size) {
// Rollup doesn't tell you the entry point of each module in the bundle,
// so we need to ask every TypeScript program for the given filename.
for (program of programs.values()) {
source = program.getSourceFile(fileName) || null;
if (source) break;
}
}
// Create any `ts.SourceFile` objects on-demand for ".d.ts" modules,
// but only when there are zero ".ts" entry points.
else if (fileName.endsWith(dts)) {
const code = ts.sys.readFile(fileName, "utf8");
if (code)
source = ts.createSourceFile(
fileName,
code,
ts.ScriptTarget.Latest,
true, // setParentNodes
);
}
return { source, program };
};

return {
name: "dts",

options(options) {
let { input } = options;
if (!Array.isArray(input)) {
input = !input ? [] : typeof input === "string" ? [input] : Object.values(input);
}
if (!input.every(main => main.endsWith(dts))) {
input.forEach(main => programs.set(main, createProgram(main)));
}
return {
...options,
treeshake: {
Expand All @@ -20,8 +124,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 +136,50 @@ const plugin: PluginImpl<{}> = () => {
};
},

load(id) {
if (!tsx.test(id)) {
return null;
}
if (id.endsWith(dts)) {
const { source } = getModule(id);
return source ? transformFile(source) : null;
}
// Always try ".d.ts" before ".tsx?"
const declarationId = id.replace(tsx, dts);
let module = getModule(declarationId);
if (module.source) {
return transformFile(module.source);
}
// Generate in-memory ".d.ts" modules from ".tsx?" modules!
module = getModule(id);
if (!module.source || !module.program) {
return null;
}
let generated!: SourceDescription;
const { emitSkipped, diagnostics } = module.program.emit(
module.source,
(_, declarationText) =>
(generated = transformFile(
ts.createSourceFile(
declarationId,
declarationText,
ts.ScriptTarget.Latest,
true, // setParentNodes
),
)),
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 +193,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