diff --git a/packages/typescript/README.md b/packages/typescript/README.md
index cd8e8e09c..011a7474b 100644
--- a/packages/typescript/README.md
+++ b/packages/typescript/README.md
@@ -112,6 +112,33 @@ typescript({
});
```
+### Typescript compiler options
+
+Some of Typescript's [CompilerOptions](https://www.typescriptlang.org/docs/handbook/compiler-options.html) affect how Rollup builds files.
+
+#### `noEmitOnError`
+
+Type: `Boolean`
+Default: `true`
+
+If a type error is detected, the Rollup build is aborted when this option is set to true.
+
+#### `files`, `include`, `exclude`
+
+Type: `Array[...String]`
+Default: `[]`
+
+Declaration files are automatically included if they are listed in the `files` field in your `tsconfig.json` file. Source files in these fields are ignored as Rollup's configuration is used instead.
+
+#### Ignored options
+
+These compiler options are ignored by Rollup:
+- `declaration`, `declarationMap`: This plugin currently cannot emit declaration files.
+- `incremental`, `tsBuildInfoFile`: This plugin currently does not support incremental compilation using Typescript.
+- `noEmitHelpers`, `importHelpers`: The `tslib` helper module always must be used.
+- `noEmit`, `emitDeclarationOnly`: Typescript needs to emit code for the plugin to work with.
+- `noResolve`: Preventing Typescript from resolving code may break compilation
+
### Importing CommonJS
Though it is not recommended, it is possible to configure this plugin to handle imports of CommonJS files from TypeScript. For this, you need to specify `CommonJS` as the module format and add `rollup-plugin-commonjs` to transpile the CommonJS output generated by TypeScript to ES Modules so that rollup can process it.
@@ -158,12 +185,6 @@ export default {
};
```
-## Issues
-
-This plugin will currently **not warn for any type violations**. This plugin relies on TypeScript's [transpileModule](https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API#a-simple-transform-function) function which basically transpiles TypeScript to JavaScript by stripping any type information on a per-file basis. While this is faster than using the language service, no cross-file type checks are possible with this approach.
-
-This also causes issues with emit-less types, see [rollup/rollup-plugin-typescript#28](https://github.com/rollup/rollup-plugin-typescript/issues/28).
-
## Meta
[CONTRIBUTING](/.github/CONTRIBUTING.md)
diff --git a/packages/typescript/src/diagnostics.ts b/packages/typescript/src/diagnostics.ts
index 8250c9974..7d997ab12 100644
--- a/packages/typescript/src/diagnostics.ts
+++ b/packages/typescript/src/diagnostics.ts
@@ -9,18 +9,21 @@ const CANNOT_COMPILE_ESM = 1204;
export function emitDiagnostics(
ts: typeof import('typescript'),
context: PluginContext,
+ host: import('typescript').FormatDiagnosticsHost &
+ Pick,
diagnostics: readonly import('typescript').Diagnostic[] | undefined
) {
if (!diagnostics) return;
+ const { noEmitOnError } = host.getCompilationSettings();
diagnostics
.filter((diagnostic) => diagnostic.code !== CANNOT_COMPILE_ESM)
.forEach((diagnostic) => {
// Build a Rollup warning object from the diagnostics object.
- const warning = diagnosticToWarning(ts, diagnostic);
+ const warning = diagnosticToWarning(ts, host, diagnostic);
// Errors are fatal. Otherwise emit warnings.
- if (diagnostic.category === ts.DiagnosticCategory.Error) {
+ if (noEmitOnError && diagnostic.category === ts.DiagnosticCategory.Error) {
context.error(warning);
} else {
context.warn(warning);
@@ -33,6 +36,7 @@ export function emitDiagnostics(
*/
export function diagnosticToWarning(
ts: typeof import('typescript'),
+ host: import('typescript').FormatDiagnosticsHost | null,
diagnostic: import('typescript').Diagnostic
) {
const pluginCode = `TS${diagnostic.code}`;
@@ -44,8 +48,8 @@ export function diagnosticToWarning(
message: `@rollup/plugin-typescript ${pluginCode}: ${message}`
};
- // Add information about the file location
if (diagnostic.file) {
+ // Add information about the file location
const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
warning.loc = {
@@ -53,6 +57,19 @@ export function diagnosticToWarning(
line: line + 1,
file: diagnostic.file.fileName
};
+
+ if (host) {
+ // Extract a code frame from Typescript
+ const formatted = ts.formatDiagnosticsWithColorAndContext([diagnostic], host);
+ // Typescript only exposes this formatter as a string prefixed with the flattened message.
+ // We need to remove it here since Rollup treats the properties as separate parts.
+ let frame = formatted.slice(formatted.indexOf(message) + message.length);
+ const newLine = host.getNewLine();
+ if (frame.startsWith(newLine)) {
+ frame = frame.slice(frame.indexOf(newLine) + newLine.length);
+ }
+ warning.frame = frame;
+ }
}
return warning;
diff --git a/packages/typescript/src/documentRegistry.ts b/packages/typescript/src/documentRegistry.ts
new file mode 100644
index 000000000..a6dfa2a47
--- /dev/null
+++ b/packages/typescript/src/documentRegistry.ts
@@ -0,0 +1,26 @@
+/**
+ * Map of Typescript instances to paths to DocumentRegistries.
+ */
+const globalRegistryCache = new Map<
+ typeof import('typescript'),
+ Map
+>();
+
+/**
+ * Return a `DocumentRegistry` instance that matches the given Typescript instance
+ * and working directory. If there is no a pre-existing instance, one will be
+ * created and set in the map.
+ */
+export default function getDocumentRegistry(ts: typeof import('typescript'), cwd: string) {
+ if (!globalRegistryCache.has(ts)) {
+ globalRegistryCache.set(ts, new Map());
+ }
+ const instanceRegistryCache = globalRegistryCache.get(ts);
+ if (!instanceRegistryCache.has(cwd)) {
+ instanceRegistryCache.set(
+ cwd,
+ ts.createDocumentRegistry(ts.sys.useCaseSensitiveFileNames, cwd)
+ );
+ }
+ return instanceRegistryCache.get(cwd)!;
+}
diff --git a/packages/typescript/src/host.ts b/packages/typescript/src/host.ts
new file mode 100644
index 000000000..e145f1698
--- /dev/null
+++ b/packages/typescript/src/host.ts
@@ -0,0 +1,128 @@
+import createModuleResolver, { Resolver } from './resolver';
+
+type BaseHost = import('typescript').LanguageServiceHost &
+ import('typescript').ModuleResolutionHost &
+ import('typescript').FormatDiagnosticsHost;
+
+export interface TypescriptHost extends BaseHost {
+ /**
+ * Lets the host know about a file by adding it to its memory.
+ * @param id Filename
+ * @param code Body of the file
+ * @see https://blog.scottlogic.com/2015/01/20/typescript-compiler-api.html
+ */
+ addFile(id: string, code: string): void;
+ /**
+ * Reads the given file.
+ * Used for both `LanguageServiceHost` (2 params) and `ModuleResolutionHost` (1 param).
+ */
+ readFile(path: string, encoding?: string): string | undefined;
+ /**
+ * Uses Typescript to resolve a module path.
+ * The `compilerOptions` parameter from `LanguageServiceHost.resolveModuleNames`
+ * is ignored and omitted in this signature.
+ */
+ resolveModuleNames(
+ moduleNames: string[],
+ containingFile: string
+ ): Array;
+}
+
+interface File {
+ file: import('typescript').IScriptSnapshot;
+ version: number;
+}
+
+/**
+ * Create a language service host to use with the Typescript compiler & type checking APIs.
+ * @param parsedOptions Parsed options for Typescript.
+ * @param parsedOptions.options Typescript compiler options. Affects functions such as `getNewLine`.
+ * @param parsedOptions.fileNames Declaration files to include for typechecking.
+ * @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
+ */
+export default function createHost(
+ ts: typeof import('typescript'),
+ parsedOptions: import('typescript').ParsedCommandLine
+): TypescriptHost {
+ const files = new Map();
+
+ /** Get the code stored in a File snapshot. */
+ function getCode({ file }: File) {
+ return file.getText(0, file.getLength());
+ }
+
+ /** @see TypescriptHost.addFile */
+ function addFile(id: string, code: string) {
+ const existing = files.get(id);
+ // Don't need to update if nothing changed
+ if (existing && getCode(existing) === code) return;
+
+ files.set(id, {
+ file: ts.ScriptSnapshot.fromString(code),
+ version: existing ? existing.version + 1 : 0
+ });
+ }
+
+ /** Helper that tries to read the file if it hasn't been stored yet */
+ function getFile(id: string) {
+ if (!files.has(id)) {
+ const code = ts.sys.readFile(id);
+ if (code == null) {
+ throw new Error(`@rollup/plugin-typescript: Could not find ${id}`);
+ }
+ addFile(id, code);
+ }
+ return files.get(id);
+ }
+
+ parsedOptions.fileNames.forEach((id) => getFile(id));
+
+ let resolver: Resolver;
+ const host: TypescriptHost = {
+ getCompilationSettings: () => parsedOptions.options,
+ getCurrentDirectory: () => process.cwd(),
+ getNewLine: () => getNewLine(ts, parsedOptions.options.newLine),
+ getCanonicalFileName: (fileName) =>
+ ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase(),
+ useCaseSensitiveFileNames: () => ts.sys.useCaseSensitiveFileNames,
+ getDefaultLibFileName: ts.getDefaultLibFilePath,
+ getDirectories: ts.sys.getDirectories,
+ directoryExists: ts.sys.directoryExists,
+ realpath: ts.sys.realpath,
+ readDirectory: ts.sys.readDirectory,
+ readFile(fileName, encoding) {
+ const file = files.get(fileName);
+ if (file != null) return getCode(file);
+ return ts.sys.readFile(fileName, encoding);
+ },
+ fileExists: (fileName) => files.has(fileName) || ts.sys.fileExists(fileName),
+ getScriptFileNames: () => Array.from(files.keys()),
+ getScriptSnapshot: (fileName) => getFile(fileName).file,
+ getScriptVersion: (fileName) => getFile(fileName).version.toString(),
+ resolveModuleNames(moduleNames, containingFile) {
+ return moduleNames.map((moduleName) => resolver(moduleName, containingFile));
+ },
+ addFile
+ };
+ // Declared here because this has a circular reference
+ resolver = createModuleResolver(ts, host);
+
+ return host;
+}
+
+/**
+ * Returns the string that corresponds with the selected `NewLineKind`.
+ */
+function getNewLine(
+ ts: typeof import('typescript'),
+ kind: import('typescript').NewLineKind | undefined
+) {
+ switch (kind) {
+ case ts.NewLineKind.CarriageReturnLineFeed:
+ return '\r\n';
+ case ts.NewLineKind.LineFeed:
+ return '\n';
+ default:
+ return ts.sys.newLine;
+ }
+}
diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts
index 390b8af6c..582fa18cc 100644
--- a/packages/typescript/src/index.ts
+++ b/packages/typescript/src/index.ts
@@ -5,19 +5,25 @@ import { Plugin } from 'rollup';
import { RollupTypescriptOptions } from '../types';
import { diagnosticToWarning, emitDiagnostics } from './diagnostics';
+import getDocumentRegistry from './documentRegistry';
+import createHost from './host';
import { getPluginOptions, parseTypescriptConfig } from './options';
+import typescriptOutputToRollupTransformation from './outputToRollupTransformation';
import { TSLIB_ID } from './tslib';
export default function typescript(options: RollupTypescriptOptions = {}): Plugin {
const { filter, tsconfig, compilerOptions, tslib, typescript: ts } = getPluginOptions(options);
- const parsedConfig = parseTypescriptConfig(ts, tsconfig, compilerOptions);
+
+ const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions);
+ const host = createHost(ts, parsedOptions);
+ const services = ts.createLanguageService(host, getDocumentRegistry(ts, process.cwd()));
return {
name: 'typescript',
buildStart() {
- if (parsedConfig.errors.length > 0) {
- parsedConfig.errors.forEach((error) => this.warn(diagnosticToWarning(ts, error)));
+ if (parsedOptions.errors.length > 0) {
+ parsedOptions.errors.forEach((error) => this.warn(diagnosticToWarning(ts, host, error)));
this.error(`@rollup/plugin-typescript: Couldn't process compiler options`);
}
@@ -29,21 +35,16 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
}
if (!importer) return null;
- const containingFile = importer.split(path.win32.sep).join(path.posix.sep);
- const result = ts.nodeModuleNameResolver(
- importee,
- containingFile,
- parsedConfig.options,
- ts.sys
- );
+ // Convert path from windows separators to posix separators
+ const containingFile = importer.split(path.win32.sep).join(path.posix.sep);
- if (result.resolvedModule && result.resolvedModule.resolvedFileName) {
- if (result.resolvedModule.resolvedFileName.endsWith('.d.ts')) {
- return null;
- }
+ const resolved = host.resolveModuleNames([importee], containingFile);
+ const resolvedFile = resolved[0]?.resolvedFileName;
- return result.resolvedModule.resolvedFileName;
+ if (resolvedFile) {
+ if (resolvedFile.endsWith('.d.ts')) return null;
+ return resolvedFile;
}
return null;
@@ -59,20 +60,26 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
transform(code, id) {
if (!filter(id)) return null;
- const transformed = ts.transpileModule(code, {
- fileName: id,
- reportDiagnostics: true,
- compilerOptions: parsedConfig.options
- });
+ host.addFile(id, code);
+ const output = services.getEmitOutput(id);
- emitDiagnostics(ts, this, transformed.diagnostics);
+ if (output.emitSkipped) {
+ // Emit failed, print all diagnostics for this file
+ const allDiagnostics: import('typescript').Diagnostic[] = []
+ .concat(services.getSyntacticDiagnostics(id))
+ .concat(services.getSemanticDiagnostics(id));
+ emitDiagnostics(ts, this, host, allDiagnostics);
- return {
- code: transformed.outputText,
+ throw new Error(`Couldn't compile ${id}`);
+ }
+
+ return typescriptOutputToRollupTransformation(output.outputFiles);
+ },
- // Rollup expects `map` to be an object so we must parse the string
- map: transformed.sourceMapText ? JSON.parse(transformed.sourceMapText) : null
- };
+ generateBundle() {
+ const program = services.getProgram();
+ if (program == null) return;
+ emitDiagnostics(ts, this, host, ts.getPreEmitDiagnostics(program));
}
};
}
diff --git a/packages/typescript/src/options.ts b/packages/typescript/src/options.ts
index f544b5839..91d8942e2 100644
--- a/packages/typescript/src/options.ts
+++ b/packages/typescript/src/options.ts
@@ -111,7 +111,7 @@ function getTsConfigPath(ts: typeof import('typescript'), relativePath: string |
function readTsConfigFile(ts: typeof import('typescript'), tsConfigPath: string) {
const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
if (error) {
- throw Object.assign(Error(), diagnosticToWarning(ts, error));
+ throw Object.assign(Error(), diagnosticToWarning(ts, null, error));
}
const extendedTsConfig: string = config?.extends;
diff --git a/packages/typescript/src/outputToRollupTransformation.ts b/packages/typescript/src/outputToRollupTransformation.ts
new file mode 100644
index 000000000..e5d59f1af
--- /dev/null
+++ b/packages/typescript/src/outputToRollupTransformation.ts
@@ -0,0 +1,31 @@
+import { SourceDescription } from 'rollup';
+
+/**
+ * Checks if the given OutputFile represents some code
+ */
+function isCodeOutputFile(file: import('typescript').OutputFile): boolean {
+ return !isMapOutputFile(file) && !file.name.endsWith('.d.ts');
+}
+
+/**
+ * Checks if the given OutputFile represents some source map
+ */
+function isMapOutputFile({ name }: import('typescript').OutputFile): boolean {
+ return name.endsWith('.map');
+}
+
+/**
+ * Transforms a Typescript EmitOutput into a Rollup SourceDescription.
+ */
+export default function typescriptOutputToRollupTransformation(
+ outputFiles: readonly import('typescript').OutputFile[]
+): SourceDescription | null {
+ const code = outputFiles.find(isCodeOutputFile);
+ if (code == null) return null;
+ const map = outputFiles.find(isMapOutputFile);
+
+ return {
+ code: code.text,
+ map: map?.text
+ };
+}
diff --git a/packages/typescript/src/resolver.ts b/packages/typescript/src/resolver.ts
new file mode 100644
index 000000000..d57740334
--- /dev/null
+++ b/packages/typescript/src/resolver.ts
@@ -0,0 +1,34 @@
+type ModuleResolverHost = import('typescript').ModuleResolutionHost &
+ Pick &
+ Pick;
+
+export type Resolver = (
+ moduleName: string,
+ containingFile: string
+) => import('typescript').ResolvedModuleFull | undefined;
+
+/**
+ * Create a helper for resolving modules using Typescript.
+ */
+export default function createModuleResolver(
+ ts: typeof import('typescript'),
+ host: ModuleResolverHost
+): Resolver {
+ const compilerOptions = host.getCompilationSettings();
+ const cache = ts.createModuleResolutionCache(
+ process.cwd(),
+ host.getCanonicalFileName,
+ compilerOptions
+ );
+
+ return (moduleName, containingFile) => {
+ const resolved = ts.nodeModuleNameResolver(
+ moduleName,
+ containingFile,
+ compilerOptions,
+ host,
+ cache
+ );
+ return resolved.resolvedModule;
+ };
+}
diff --git a/packages/typescript/src/tslib.ts b/packages/typescript/src/tslib.ts
index 8f9d5eb59..d2769e06f 100644
--- a/packages/typescript/src/tslib.ts
+++ b/packages/typescript/src/tslib.ts
@@ -16,9 +16,9 @@ const resolveIdAsync = (file: string, opts?: AsyncOpts) =>
/**
* Returns code asynchronously for the tslib helper library.
- * @param customCode Overrides the injected helpers with a custom version.
+ * @param customHelperCode Overrides the injected helpers with a custom version.
*/
-export async function getTsLibCode(customHelperCode: string | Promise) {
+export async function getTsLibCode(customHelperCode: string | Promise | undefined) {
if (customHelperCode) return customHelperCode;
const defaultPath = await resolveIdAsync('tslib/tslib.es6.js', { basedir: __dirname });
diff --git a/packages/typescript/test/fixtures/dom/main.ts b/packages/typescript/test/fixtures/dom/main.ts
new file mode 100644
index 000000000..7d22f6aed
--- /dev/null
+++ b/packages/typescript/test/fixtures/dom/main.ts
@@ -0,0 +1 @@
+navigator.clipboard.readText();
diff --git a/packages/typescript/test/fixtures/export-class/main.ts b/packages/typescript/test/fixtures/export-class/main.ts
index 3aafd0fe2..acd29838c 100644
--- a/packages/typescript/test/fixtures/export-class/main.ts
+++ b/packages/typescript/test/fixtures/export-class/main.ts
@@ -1,4 +1,3 @@
-// eslint-disable-next-line import/extensions
-import { Foo } from './Foo.ts';
+import { Foo } from './Foo';
export default new Foo();
diff --git a/packages/typescript/test/fixtures/jsx/main.tsx b/packages/typescript/test/fixtures/jsx/main.tsx
index 37ccc3ce8..d62ec86a9 100644
--- a/packages/typescript/test/fixtures/jsx/main.tsx
+++ b/packages/typescript/test/fixtures/jsx/main.tsx
@@ -1 +1,3 @@
-export default Yo!
+const props = {};
+// @ts-ignore
+export default Yo!;
diff --git a/packages/typescript/test/fixtures/reexport-type/Bar.ts b/packages/typescript/test/fixtures/reexport-type/Bar.ts
new file mode 100644
index 000000000..e6d163486
--- /dev/null
+++ b/packages/typescript/test/fixtures/reexport-type/Bar.ts
@@ -0,0 +1 @@
+export type Bar = object;
diff --git a/packages/typescript/test/fixtures/reexport-type/Foo.ts b/packages/typescript/test/fixtures/reexport-type/Foo.ts
new file mode 100644
index 000000000..39df3b83f
--- /dev/null
+++ b/packages/typescript/test/fixtures/reexport-type/Foo.ts
@@ -0,0 +1 @@
+export interface Foo {}
diff --git a/packages/typescript/test/fixtures/reexport-type/main.ts b/packages/typescript/test/fixtures/reexport-type/main.ts
new file mode 100644
index 000000000..82c744bcf
--- /dev/null
+++ b/packages/typescript/test/fixtures/reexport-type/main.ts
@@ -0,0 +1,4 @@
+import { Foo } from './Foo';
+
+export { Foo };
+export { Bar } from './Bar';
diff --git a/packages/typescript/test/fixtures/syntax-error/missing-type.ts b/packages/typescript/test/fixtures/syntax-error/missing-type.ts
index 49d803c65..4a9a6a144 100644
--- a/packages/typescript/test/fixtures/syntax-error/missing-type.ts
+++ b/packages/typescript/test/fixtures/syntax-error/missing-type.ts
@@ -1 +1,2 @@
var a: ;
+console.log('hello world');
diff --git a/packages/typescript/test/fixtures/tsconfig-extends/main.tsx b/packages/typescript/test/fixtures/tsconfig-extends/main.tsx
index 37ccc3ce8..d62ec86a9 100644
--- a/packages/typescript/test/fixtures/tsconfig-extends/main.tsx
+++ b/packages/typescript/test/fixtures/tsconfig-extends/main.tsx
@@ -1 +1,3 @@
-export default Yo!
+const props = {};
+// @ts-ignore
+export default Yo!;
diff --git a/packages/typescript/test/fixtures/tsconfig-extends/ts-config-extends-child/main.tsx b/packages/typescript/test/fixtures/tsconfig-extends/ts-config-extends-child/main.tsx
index 37ccc3ce8..d62ec86a9 100644
--- a/packages/typescript/test/fixtures/tsconfig-extends/ts-config-extends-child/main.tsx
+++ b/packages/typescript/test/fixtures/tsconfig-extends/ts-config-extends-child/main.tsx
@@ -1 +1,3 @@
-export default Yo!
+const props = {};
+// @ts-ignore
+export default Yo!;
diff --git a/packages/typescript/test/fixtures/tsconfig-jsx/main.tsx b/packages/typescript/test/fixtures/tsconfig-jsx/main.tsx
index 37ccc3ce8..d62ec86a9 100644
--- a/packages/typescript/test/fixtures/tsconfig-jsx/main.tsx
+++ b/packages/typescript/test/fixtures/tsconfig-jsx/main.tsx
@@ -1 +1,3 @@
-export default Yo!
+const props = {};
+// @ts-ignore
+export default Yo!;
diff --git a/packages/typescript/test/test.js b/packages/typescript/test/test.js
index 3373f8e8a..9a6cf98c6 100644
--- a/packages/typescript/test/test.js
+++ b/packages/typescript/test/test.js
@@ -1,9 +1,9 @@
const path = require('path');
+const commonjs = require('@rollup/plugin-commonjs');
const test = require('ava');
const { rollup } = require('rollup');
-
-const commonjs = require('@rollup/plugin-commonjs');
+const ts = require('typescript');
const { getCode, testBundle } = require('../../../util/test');
@@ -18,10 +18,16 @@ async function evaluateBundle(bundle) {
return module.exports;
}
+function onwarn(warning) {
+ // eslint-disable-next-line no-console
+ console.warn(warning.toString());
+}
+
test('runs code through typescript', async (t) => {
const bundle = await rollup({
input: 'fixtures/basic/main.ts',
- plugins: [typescript()]
+ plugins: [typescript({ target: 'es5' })],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -33,16 +39,18 @@ test('ignores the declaration option', async (t) => {
await t.notThrowsAsync(
rollup({
input: 'fixtures/basic/main.ts',
- plugins: [typescript({ declaration: true })]
+ plugins: [typescript({ declaration: true })],
+ onwarn
})
);
});
test('throws for unsupported module types', async (t) => {
- const caughtError = t.throws(() =>
+ const caughtError = await t.throws(() =>
rollup({
input: 'fixtures/basic/main.ts',
- plugins: [typescript({ module: 'amd' })]
+ plugins: [typescript({ module: 'AMD' })],
+ onwarn
})
);
@@ -54,12 +62,11 @@ test('throws for unsupported module types', async (t) => {
test('warns for invalid module types', async (t) => {
const warnings = [];
- const caughtError = await t.throwsAsync(() =>
+ await t.throwsAsync(() =>
rollup({
input: 'fixtures/basic/main.ts',
plugins: [typescript({ module: 'ES5' })],
onwarn({ toString, ...warning }) {
- // Can't match toString with deepEqual, so remove it here
warnings.push(warning);
}
})
@@ -73,17 +80,14 @@ test('warns for invalid module types', async (t) => {
message: `@rollup/plugin-typescript TS6046: Argument for '--module' option must be: 'none', 'commonjs', 'amd', 'system', 'umd', 'es6', 'es2015', 'esnext'.`
}
]);
- t.true(
- caughtError.message.includes(`@rollup/plugin-typescript: Couldn't process compiler options`),
- `Unexpected error message: ${caughtError.message}`
- );
});
test('ignores case of module types', async (t) => {
await t.notThrowsAsync(
rollup({
input: 'fixtures/basic/main.ts',
- plugins: [typescript({ module: 'eSnExT' })]
+ plugins: [typescript({ module: 'eSnExT' })],
+ onwarn
})
);
});
@@ -91,7 +95,8 @@ test('ignores case of module types', async (t) => {
test('handles async functions', async (t) => {
const bundle = await rollup({
input: 'fixtures/async/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const wait = await evaluateBundle(bundle);
await wait(3);
@@ -101,7 +106,8 @@ test('handles async functions', async (t) => {
test('does not duplicate helpers', async (t) => {
const bundle = await rollup({
input: 'fixtures/dedup-helpers/main.ts',
- plugins: [typescript()]
+ plugins: [typescript({ target: 'es5' })],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -115,7 +121,8 @@ test('does not duplicate helpers', async (t) => {
test('transpiles `export class A` correctly', async (t) => {
const bundle = await rollup({
input: 'fixtures/export-class-fix/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -131,7 +138,8 @@ test('transpiles `export class A` correctly', async (t) => {
test('transpiles ES6 features to ES5 with source maps', async (t) => {
const bundle = await rollup({
input: 'fixtures/import-class/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -144,7 +152,8 @@ test('reports diagnostics and throws if errors occur during transpilation', asyn
const caughtError = await t.throwsAsync(
rollup({
input: 'fixtures/syntax-error/missing-type.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
})
);
@@ -152,10 +161,37 @@ test('reports diagnostics and throws if errors occur during transpilation', asyn
t.is(caughtError.pluginCode, 'TS1110');
});
+test('ignore type errors if noEmitOnError is false', async (t) => {
+ const warnings = [];
+ const bundle = await rollup({
+ input: 'fixtures/syntax-error/missing-type.ts',
+ plugins: [typescript({ noEmitOnError: false })],
+ onwarn(warning) {
+ warnings.push(warning);
+ }
+ });
+ const code = await getCode(bundle, outputOptions);
+
+ t.true(code.includes(`console.log('hello world')`));
+
+ t.is(warnings.length, 1);
+
+ t.is(warnings[0].code, 'PLUGIN_WARNING');
+ t.is(warnings[0].plugin, 'typescript');
+ t.is(warnings[0].pluginCode, 'TS1110');
+ t.is(warnings[0].message, '@rollup/plugin-typescript TS1110: Type expected.');
+
+ t.is(warnings[0].loc.line, 1);
+ t.is(warnings[0].loc.column, 8);
+ t.true(warnings[0].loc.file.includes('missing-type.ts'));
+ t.true(warnings[0].frame.includes('var a: ;'));
+});
+
test('works with named exports for abstract classes', async (t) => {
const bundle = await rollup({
input: 'fixtures/export-abstract-class/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
t.true(code.length > 0, code);
@@ -164,7 +200,8 @@ test('works with named exports for abstract classes', async (t) => {
test('should use named exports for classes', async (t) => {
const bundle = await rollup({
input: 'fixtures/export-class/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
t.is((await evaluateBundle(bundle)).foo, 'bar');
});
@@ -172,6 +209,7 @@ test('should use named exports for classes', async (t) => {
test('supports overriding the TypeScript version', async (t) => {
const bundle = await rollup({
input: 'fixtures/overriding-typescript/main.ts',
+ onwarn,
plugins: [
typescript({
// Don't use `tsconfig.json`
@@ -181,12 +219,23 @@ test('supports overriding the TypeScript version', async (t) => {
typescript: fakeTypescript({
version: '1.8.0-fake',
- transpileModule: () => {
- // Ignore the code to transpile. Always return the same thing.
+ createLanguageService() {
return {
- outputText: 'export default 1337;',
- diagnostics: [],
- sourceMapText: JSON.stringify({ mappings: '' })
+ getProgram: () => null,
+ getSyntacticDiagnostics: () => [],
+ getSemanticDiagnostics: () => [],
+ getEmitOutput() {
+ // Ignore the code to transpile. Always return the same thing.
+ return {
+ outputFiles: [
+ {
+ name: 'whatever.js',
+ text: 'export default 1337;'
+ }
+ ],
+ emitSkipped: false
+ };
+ }
};
}
})
@@ -203,7 +252,8 @@ test('supports overriding tslib with a string', async (t) => {
input: 'fixtures/overriding-tslib/main.ts',
plugins: [
typescript({ tslib: 'export const __extends = (Main, Super) => Main.myParent = Super' })
- ]
+ ],
+ onwarn
});
const code = await evaluateBundle(bundle);
@@ -217,7 +267,8 @@ test('supports overriding tslib with a promise', async (t) => {
typescript({
tslib: Promise.resolve('export const __extends = (Main, Super) => Main.myParent = Super')
})
- ]
+ ],
+ onwarn
});
const code = await evaluateBundle(bundle);
@@ -227,7 +278,9 @@ test('supports overriding tslib with a promise', async (t) => {
test('should not resolve .d.ts files', async (t) => {
const bundle = await rollup({
input: 'fixtures/dts/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn,
+ external: ['an-import']
});
const imports = bundle.cache.modules[0].dependencies;
t.deepEqual(imports, ['an-import']);
@@ -236,7 +289,8 @@ test('should not resolve .d.ts files', async (t) => {
test('should transpile JSX if enabled', async (t) => {
const bundle = await rollup({
input: 'fixtures/jsx/main.tsx',
- plugins: [typescript({ jsx: 'react' })]
+ plugins: [typescript({ jsx: 'react' })],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -252,7 +306,8 @@ test.serial('automatically loads tsconfig.json from the current directory', asyn
const bundle = await rollup({
input: 'main.tsx',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -265,7 +320,8 @@ test.serial('should support extends property in tsconfig', async (t) => {
const bundle = await rollup({
input: 'main.tsx',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -282,7 +338,8 @@ test.serial('should support extends property with given tsconfig', async (t) =>
typescript({
tsconfig: './tsconfig.json'
})
- ]
+ ],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -295,7 +352,8 @@ test('allows specifying a path for tsconfig.json', async (t) => {
input: 'fixtures/tsconfig-jsx/main.tsx',
plugins: [
typescript({ tsconfig: path.resolve(__dirname, 'fixtures/tsconfig-jsx/tsconfig.json') })
- ]
+ ],
+ onwarn
});
const code = await getCode(bundle, outputOptions);
@@ -304,17 +362,14 @@ test('allows specifying a path for tsconfig.json', async (t) => {
});
test('throws if tsconfig cannot be found', async (t) => {
- let caughtError = null;
- try {
- await rollup({
+ const caughtError = await t.throws(() =>
+ rollup({
input: 'fixtures/tsconfig-jsx/main.tsx',
- plugins: [typescript({ tsconfig: path.resolve(__dirname, 'does-not-exist.json') })]
- });
- } catch (error) {
- caughtError = error;
- }
+ plugins: [typescript({ tsconfig: path.resolve(__dirname, 'does-not-exist.json') })],
+ onwarn
+ })
+ );
- t.truthy(caughtError, 'Throws an error.');
t.true(
caughtError.message.includes('Could not find specified tsconfig.json'),
`Unexpected error message: ${caughtError.message}`
@@ -328,23 +383,39 @@ test('should throw on bad options', async (t) => {
rollup({
input: 'does-not-matter.ts',
plugins: [typescript({ foo: 'bar' })],
- onwarn(warning) {
+ onwarn({ toString, ...warning }) {
+ // Can't match toString function, so omit it
warnings.push(warning);
}
}),
"@rollup/plugin-typescript: Couldn't process compiler options"
);
- t.is(warnings.length, 1);
- t.is(warnings[0].plugin, 'typescript');
- t.is(warnings[0].message, `@rollup/plugin-typescript TS5023: Unknown compiler option 'foo'.`);
+ t.deepEqual(warnings, [
+ {
+ code: 'PLUGIN_WARNING',
+ plugin: 'typescript',
+ pluginCode: 'TS5023',
+ message: `@rollup/plugin-typescript TS5023: Unknown compiler option 'foo'.`
+ }
+ ]);
+});
+
+test('should handle re-exporting types', async (t) => {
+ const bundle = await rollup({
+ input: 'fixtures/reexport-type/main.ts',
+ plugins: [typescript()],
+ onwarn
+ });
+ await t.notThrowsAsync(getCode(bundle, outputOptions));
});
test('prevents errors due to conflicting `sourceMap`/`inlineSourceMap` options', async (t) => {
await t.notThrowsAsync(
rollup({
input: 'fixtures/overriding-typescript/main.ts',
- plugins: [typescript({ inlineSourceMap: true })]
+ plugins: [typescript({ inlineSourceMap: true })],
+ onwarn
})
);
});
@@ -358,7 +429,8 @@ test('should not fail if source maps are off', async (t) => {
inlineSourceMap: false,
sourceMap: false
})
- ]
+ ],
+ onwarn
})
);
});
@@ -366,7 +438,8 @@ test('should not fail if source maps are off', async (t) => {
test('does not include helpers in source maps', async (t) => {
const bundle = await rollup({
input: 'fixtures/dedup-helpers/main.ts',
- plugins: [typescript({ sourceMap: true })]
+ plugins: [typescript({ sourceMap: true })],
+ onwarn
});
const { output } = await bundle.generate({
format: 'es',
@@ -380,7 +453,8 @@ test('does not include helpers in source maps', async (t) => {
test('should allow a namespace containing a class', async (t) => {
const bundle = await rollup({
input: 'fixtures/export-namespace-export-class/test.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const { MODE } = (await evaluateBundle(bundle)).MODE;
const mode = new MODE();
@@ -391,7 +465,8 @@ test('should allow a namespace containing a class', async (t) => {
test('should allow merging an exported function and namespace', async (t) => {
const bundle = await rollup({
input: 'fixtures/export-fodule/main.ts',
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
});
const f = (await evaluateBundle(bundle)).test;
@@ -404,34 +479,44 @@ test('supports dynamic imports', async (t) => {
await rollup({
input: 'fixtures/dynamic-imports/main.ts',
inlineDynamicImports: true,
- plugins: [typescript()]
+ plugins: [typescript()],
+ onwarn
}),
outputOptions
);
t.true(code.includes("console.log('dynamic')"));
});
-test.serial('supports CommonJS imports when the output format is CommonJS', async (t) => {
+test('supports CommonJS imports when the output format is CommonJS', async (t) => {
const bundle = await rollup({
input: 'fixtures/commonjs-imports/main.ts',
- plugins: [typescript({ module: 'CommonJS' }), commonjs({ extensions: ['.ts', '.js'] })]
+ plugins: [typescript({ module: 'CommonJS' }), commonjs({ extensions: ['.ts', '.js'] })],
+ onwarn
});
const output = await evaluateBundle(bundle);
t.is(output, 'exported from commonjs');
});
+test('complies code that uses browser functions', async (t) => {
+ const bundle = await rollup({
+ input: 'fixtures/dom/main.ts',
+ plugins: [typescript({ lib: ['dom'] })],
+ onwarn
+ });
+
+ const code = await getCode(bundle, outputOptions);
+
+ t.true(code.includes('navigator.clipboard.readText()'), code);
+});
+
function fakeTypescript(custom) {
return Object.assign(
{
- ModuleKind: {
- None: 0,
- CommonJS: 1,
- AMD: 2,
- UMD: 3,
- System: 4,
- ES2015: 5,
- ESNext: 99
- },
+ sys: ts.sys,
+ createModuleResolutionCache: ts.createModuleResolutionCache,
+ createDocumentRegistry: ts.createDocumentRegistry,
+ ModuleKind: ts.ModuleKind,
+ ScriptSnapshot: ts.ScriptSnapshot,
transpileModule() {
return {
@@ -456,9 +541,12 @@ function fakeTypescript(custom) {
parseJsonConfigFileContent(json, host, basePath, existingOptions) {
return {
- options: existingOptions,
- errors: [],
- fileNames: []
+ options: {
+ ...json.compilerOptions,
+ ...existingOptions
+ },
+ fileNames: [],
+ errors: []
};
}
},
diff --git a/packages/typescript/tsconfig.json b/packages/typescript/tsconfig.json
index 3a0b8cf71..f29910f60 100644
--- a/packages/typescript/tsconfig.json
+++ b/packages/typescript/tsconfig.json
@@ -1,16 +1,13 @@
{
- "compilerOptions": {
- "lib": [
- "es6"
- ],
- "noImplicitAny": true,
- "noImplicitThis": true,
- "strict": true,
- "noEmit": true,
- "allowJs": true
- },
- "files": [
- "types",
- "typings-test.js"
- ]
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "lib": ["es6"],
+ "module": "esnext",
+ "allowJs": true,
+ "target": "es2017"
+ },
+ "files": [
+ "types",
+ "typings-test.js"
+ ]
}
diff --git a/tsconfig.base.json b/tsconfig.base.json
index 71e5ad1cd..1cb8e0c2c 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"allowSyntheticDefaultImports": true,
- "lib": ["es6", "dom"],
+ "lib": ["es6"],
"module": "commonjs",
"moduleResolution": "node",
"noEmitOnError": true,
diff --git a/util/test.d.ts b/util/test.d.ts
index e4c9aed45..2cc0070aa 100644
--- a/util/test.d.ts
+++ b/util/test.d.ts
@@ -3,17 +3,19 @@ import { RollupBuild, OutputOptions, OutputChunk, OutputAsset } from 'rollup';
import { Assertions } from 'ava';
interface GetCode {
- (bundle: RollupBuild, outputOptions: OutputOptions, allFiles?: false): string;
- (bundle: RollupBuild, outputOptions: OutputOptions, allFiles: true): Array<{
- code: OutputChunk['code'] | undefined;
- fileName: OutputChunk['fileName'] | OutputAsset['fileName'];
- source: OutputAsset['source'] | undefined;
- }>;
+ (bundle: RollupBuild, outputOptions: OutputOptions, allFiles?: false): Promise;
+ (bundle: RollupBuild, outputOptions: OutputOptions, allFiles: true): Promise<
+ Array<{
+ code: OutputChunk['code'] | undefined;
+ fileName: OutputChunk['fileName'] | OutputAsset['fileName'];
+ source: OutputAsset['source'] | undefined;
+ }>
+ >;
}
export const getCode: GetCode;
-export function getImports(bundle: RollupBuild): string[];
+export function getImports(bundle: RollupBuild): Promise;
export function testBundle(
t: Assertions,