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/dom/tsconfig.json b/packages/typescript/test/fixtures/dom/tsconfig.json new file mode 100644 index 000000000..10830e07b --- /dev/null +++ b/packages/typescript/test/fixtures/dom/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["dom"] + } +} 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..3bba4ca11 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); @@ -290,12 +347,25 @@ test.serial('should support extends property with given tsconfig', async (t) => t.not(usage, -1, 'should contain usage'); }); +test('complies code that uses browser functions', async (t) => { + const bundle = await rollup({ + input: 'fixtures/dom/main.ts', + plugins: [typescript({ tsconfig: './fixtures/dom/tsconfig.json' })], + onwarn + }); + + const code = await getCode(bundle, outputOptions); + + t.true(code.includes('navigator.clipboard.readText()'), code); +}); + test('allows specifying a path for tsconfig.json', async (t) => { const bundle = await rollup({ 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 +374,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 +395,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 +441,8 @@ test('should not fail if source maps are off', async (t) => { inlineSourceMap: false, sourceMap: false }) - ] + ], + onwarn }) ); }); @@ -366,7 +450,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 +465,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 +477,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,17 +491,19 @@ 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'); @@ -423,15 +512,11 @@ test.serial('supports CommonJS imports when the output format is CommonJS', asyn 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,