UNPKG

23.6 kBJavaScriptView Raw
1import { resolve, dirname, relative, win32, posix } from 'path';
2import { createFilter } from '@rollup/pluginutils';
3import * as defaultTs from 'typescript';
4import resolveId from 'resolve';
5import { readFileSync } from 'fs';
6
7/**
8 * Create a format diagnostics host to use with the Typescript type checking APIs.
9 * Typescript hosts are used to represent the user's system,
10 * with an API for checking case sensitivity etc.
11 * @param compilerOptions Typescript compiler options. Affects functions such as `getNewLine`.
12 * @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
13 */
14function createFormattingHost(ts, compilerOptions) {
15 return {
16 /** Returns the compiler options for the project. */
17 getCompilationSettings: () => compilerOptions,
18 /** Returns the current working directory. */
19 getCurrentDirectory: () => process.cwd(),
20 /** Returns the string that corresponds with the selected `NewLineKind`. */
21 getNewLine() {
22 switch (compilerOptions.newLine) {
23 case ts.NewLineKind.CarriageReturnLineFeed:
24 return '\r\n';
25 case ts.NewLineKind.LineFeed:
26 return '\n';
27 default:
28 return ts.sys.newLine;
29 }
30 },
31 /** Returns a lower case name on case insensitive systems, otherwise the original name. */
32 getCanonicalFileName: (fileName) => ts.sys.useCaseSensitiveFileNames ? fileName : fileName.toLowerCase()
33 };
34}
35
36/**
37 * Create a helper for resolving modules using Typescript.
38 * @param host Typescript host that extends `ModuleResolutionHost`
39 * with methods for sanitizing filenames and getting compiler options.
40 */
41function createModuleResolver(ts, host) {
42 const compilerOptions = host.getCompilationSettings();
43 const cache = ts.createModuleResolutionCache(process.cwd(), host.getCanonicalFileName, compilerOptions);
44 const moduleHost = Object.assign(Object.assign({}, ts.sys), host);
45 return (moduleName, containingFile) => {
46 const resolved = ts.nodeModuleNameResolver(moduleName, containingFile, compilerOptions, moduleHost, cache);
47 return resolved.resolvedModule;
48 };
49}
50
51/*! *****************************************************************************
52Copyright (c) Microsoft Corporation. All rights reserved.
53Licensed under the Apache License, Version 2.0 (the "License"); you may not use
54this file except in compliance with the License. You may obtain a copy of the
55License at http://www.apache.org/licenses/LICENSE-2.0
56
57THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
58KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
59WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
60MERCHANTABLITY OR NON-INFRINGEMENT.
61
62See the Apache Version 2.0 License for specific language governing permissions
63and limitations under the License.
64***************************************************************************** */
65
66function __rest(s, e) {
67 var t = {};
68 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
69 t[p] = s[p];
70 if (s != null && typeof Object.getOwnPropertySymbols === "function")
71 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
72 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
73 t[p[i]] = s[p[i]];
74 }
75 return t;
76}
77
78const resolveIdAsync = (file, opts) => new Promise((fulfil, reject) => resolveId(file, opts, (err, contents) => (err ? reject(err) : fulfil(contents))));
79/**
80 * Returns code asynchronously for the tslib helper library.
81 */
82function getTsLibPath() {
83 return resolveIdAsync('tslib/tslib.es6.js', { basedir: __dirname });
84}
85
86/**
87 * Separate the Rollup plugin options from the Typescript compiler options,
88 * and normalize the Rollup options.
89 * @returns Object with normalized options:
90 * - `filter`: Checks if a file should be included.
91 * - `tsconfig`: Path to a tsconfig, or directive to ignore tsconfig.
92 * - `compilerOptions`: Custom Typescript compiler options that override tsconfig.
93 * - `typescript`: Instance of Typescript library (possibly custom).
94 * - `tslib`: ESM code from the tslib helper library (possibly custom).
95 */
96function getPluginOptions(options) {
97 const { include, exclude, tsconfig, typescript, tslib } = options, compilerOptions = __rest(options, ["include", "exclude", "tsconfig", "typescript", "tslib"]);
98 const filter = createFilter(include || ['*.ts+(|x)', '**/*.ts+(|x)'], exclude);
99 return {
100 filter,
101 tsconfig,
102 compilerOptions: compilerOptions,
103 typescript: typescript || defaultTs,
104 tslib: tslib || getTsLibPath()
105 };
106}
107
108/**
109 * Converts a Typescript type error into an equivalent Rollup warning object.
110 */
111function diagnosticToWarning(ts, host, diagnostic) {
112 const pluginCode = `TS${diagnostic.code}`;
113 const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n');
114 // Build a Rollup warning object from the diagnostics object.
115 const warning = {
116 pluginCode,
117 message: `@rollup/plugin-typescript ${pluginCode}: ${message}`
118 };
119 if (diagnostic.file) {
120 // Add information about the file location
121 const { line, character } = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start);
122 warning.loc = {
123 column: character + 1,
124 line: line + 1,
125 file: diagnostic.file.fileName
126 };
127 if (host) {
128 // Extract a code frame from Typescript
129 const formatted = ts.formatDiagnosticsWithColorAndContext([diagnostic], host);
130 // Typescript only exposes this formatter as a string prefixed with the flattened message.
131 // We need to remove it here since Rollup treats the properties as separate parts.
132 let frame = formatted.slice(formatted.indexOf(message) + message.length);
133 const newLine = host.getNewLine();
134 if (frame.startsWith(newLine)) {
135 frame = frame.slice(frame.indexOf(newLine) + newLine.length);
136 }
137 warning.frame = frame;
138 }
139 }
140 return warning;
141}
142
143const DEFAULT_COMPILER_OPTIONS = {
144 module: 'esnext',
145 noEmitOnError: true,
146 skipLibCheck: true
147};
148const FORCED_COMPILER_OPTIONS = {
149 // Always use tslib
150 noEmitHelpers: true,
151 importHelpers: true,
152 // Typescript needs to emit the code for us to work with
153 noEmit: false,
154 emitDeclarationOnly: false,
155 // Preventing Typescript from resolving code may break compilation
156 noResolve: false
157};
158
159/* eslint-disable no-param-reassign */
160const DIRECTORY_PROPS = ['outDir', 'declarationDir'];
161/**
162 * Mutates the compiler options to convert paths from relative to absolute.
163 * This should be used with compiler options passed through the Rollup plugin options,
164 * not those found from loading a tsconfig.json file.
165 * @param compilerOptions Compiler options to _mutate_.
166 * @param relativeTo Paths are resolved relative to this path.
167 */
168function makePathsAbsolute(compilerOptions, relativeTo) {
169 for (const pathProp of DIRECTORY_PROPS) {
170 if (compilerOptions[pathProp]) {
171 compilerOptions[pathProp] = resolve(relativeTo, compilerOptions[pathProp]);
172 }
173 }
174}
175/**
176 * Mutates the compiler options to normalize some values for Rollup.
177 * @param compilerOptions Compiler options to _mutate_.
178 * @returns True if the source map compiler option was not initially set.
179 */
180function normalizeCompilerOptions(ts, compilerOptions) {
181 let autoSetSourceMap = false;
182 if (compilerOptions.inlineSourceMap) {
183 // Force separate source map files for Rollup to work with.
184 compilerOptions.sourceMap = true;
185 compilerOptions.inlineSourceMap = false;
186 }
187 else if (typeof compilerOptions.sourceMap !== 'boolean') {
188 // Default to using source maps.
189 // If the plugin user sets sourceMap to false we keep that option.
190 compilerOptions.sourceMap = true;
191 // Using inlineSources to make sure typescript generate source content
192 // instead of source path.
193 compilerOptions.inlineSources = true;
194 autoSetSourceMap = true;
195 }
196 switch (compilerOptions.module) {
197 case ts.ModuleKind.ES2015:
198 case ts.ModuleKind.ESNext:
199 case ts.ModuleKind.CommonJS:
200 // OK module type
201 return autoSetSourceMap;
202 case ts.ModuleKind.None:
203 case ts.ModuleKind.AMD:
204 case ts.ModuleKind.UMD:
205 case ts.ModuleKind.System: {
206 // Invalid module type
207 const moduleType = ts.ModuleKind[compilerOptions.module];
208 throw new Error(`@rollup/plugin-typescript: The module kind should be 'ES2015' or 'ESNext, found: '${moduleType}'`);
209 }
210 default:
211 // Unknown or unspecified module type, force ESNext
212 compilerOptions.module = ts.ModuleKind.ESNext;
213 }
214 return autoSetSourceMap;
215}
216
217/**
218 * Finds the path to the tsconfig file relative to the current working directory.
219 * @param relativePath Relative tsconfig path given by the user.
220 * If `false` is passed, then a null path is returned.
221 * @returns The absolute path, or null if the file does not exist.
222 */
223function getTsConfigPath(ts, relativePath) {
224 if (relativePath === false)
225 return null;
226 // Resolve path to file. `tsConfigOption` defaults to 'tsconfig.json'.
227 const tsConfigPath = resolve(process.cwd(), relativePath || 'tsconfig.json');
228 if (!ts.sys.fileExists(tsConfigPath)) {
229 if (relativePath) {
230 // If an explicit path was provided but no file was found, throw
231 throw new Error(`Could not find specified tsconfig.json at ${tsConfigPath}`);
232 }
233 else {
234 return null;
235 }
236 }
237 return tsConfigPath;
238}
239/**
240 * Tries to read the tsconfig file at `tsConfigPath`.
241 * @param tsConfigPath Absolute path to tsconfig JSON file.
242 * @param explicitPath If true, the path was set by the plugin user.
243 * If false, the path was computed automatically.
244 */
245function readTsConfigFile(ts, tsConfigPath) {
246 const { config, error } = ts.readConfigFile(tsConfigPath, (path) => readFileSync(path, 'utf8'));
247 if (error) {
248 throw Object.assign(Error(), diagnosticToWarning(ts, null, error));
249 }
250 return config || {};
251}
252/**
253 * Returns true if any of the `compilerOptions` contain an enum value (i.e.: ts.ScriptKind) rather than a string.
254 * This indicates that the internal CompilerOptions type is used rather than the JsonCompilerOptions.
255 */
256function containsEnumOptions(compilerOptions) {
257 const enums = [
258 'module',
259 'target',
260 'jsx',
261 'moduleResolution',
262 'newLine'
263 ];
264 return enums.some((prop) => prop in compilerOptions && typeof compilerOptions[prop] === 'number');
265}
266const configCache = new Map();
267/**
268 * Parse the Typescript config to use with the plugin.
269 * @param ts Typescript library instance.
270 * @param tsconfig Path to the tsconfig file, or `false` to ignore the file.
271 * @param compilerOptions Options passed to the plugin directly for Typescript.
272 *
273 * @returns Parsed tsconfig.json file with some important properties:
274 * - `options`: Parsed compiler options.
275 * - `fileNames` Type definition files that should be included in the build.
276 * - `errors`: Any errors from parsing the config file.
277 */
278function parseTypescriptConfig(ts, tsconfig, compilerOptions) {
279 /* eslint-disable no-undefined */
280 const cwd = process.cwd();
281 makePathsAbsolute(compilerOptions, cwd);
282 let parsedConfig;
283 // Resolve path to file. If file is not found, pass undefined path to `parseJsonConfigFileContent`.
284 // eslint-disable-next-line no-undefined
285 const tsConfigPath = getTsConfigPath(ts, tsconfig) || undefined;
286 const tsConfigFile = tsConfigPath ? readTsConfigFile(ts, tsConfigPath) : {};
287 const basePath = tsConfigPath ? dirname(tsConfigPath) : cwd;
288 // If compilerOptions has enums, it represents an CompilerOptions object instead of parsed JSON.
289 // This determines where the data is passed to the parser.
290 if (containsEnumOptions(compilerOptions)) {
291 parsedConfig = ts.parseJsonConfigFileContent(Object.assign(Object.assign({}, tsConfigFile), { compilerOptions: Object.assign(Object.assign({}, DEFAULT_COMPILER_OPTIONS), tsConfigFile.compilerOptions) }), ts.sys, basePath, Object.assign(Object.assign({}, compilerOptions), FORCED_COMPILER_OPTIONS), tsConfigPath, undefined, undefined, configCache);
292 }
293 else {
294 parsedConfig = ts.parseJsonConfigFileContent(Object.assign(Object.assign({}, tsConfigFile), { compilerOptions: Object.assign(Object.assign(Object.assign({}, DEFAULT_COMPILER_OPTIONS), tsConfigFile.compilerOptions), compilerOptions) }), ts.sys, basePath, FORCED_COMPILER_OPTIONS, tsConfigPath, undefined, undefined, configCache);
295 }
296 const autoSetSourceMap = normalizeCompilerOptions(ts, parsedConfig.options);
297 return Object.assign(Object.assign({}, parsedConfig), { autoSetSourceMap });
298}
299/**
300 * If errors are detected in the parsed options,
301 * display all of them as warnings then emit an error.
302 */
303function emitParsedOptionsErrors(ts, context, parsedOptions) {
304 if (parsedOptions.errors.length > 0) {
305 parsedOptions.errors.forEach((error) => context.warn(diagnosticToWarning(ts, null, error)));
306 context.error(`@rollup/plugin-typescript: Couldn't process compiler options`);
307 }
308}
309
310/**
311 * Validate that the `compilerOptions.sourceMap` option matches `outputOptions.sourcemap`.
312 * @param context Rollup plugin context used to emit warnings.
313 * @param compilerOptions Typescript compiler options.
314 * @param outputOptions Rollup output options.
315 * @param autoSetSourceMap True if the `compilerOptions.sourceMap` property was set to `true`
316 * by the plugin, not the user.
317 */
318function validateSourceMap(context, compilerOptions, outputOptions, autoSetSourceMap) {
319 if (compilerOptions.sourceMap && !outputOptions.sourcemap && !autoSetSourceMap) {
320 context.warn(`@rollup/plugin-typescript: Rollup 'sourcemap' option must be set to generate source maps.`);
321 }
322 else if (!compilerOptions.sourceMap && outputOptions.sourcemap) {
323 context.warn(`@rollup/plugin-typescript: Typescript 'sourceMap' compiler option must be set to generate source maps.`);
324 }
325}
326/**
327 * Validate that the out directory used by Typescript can be controlled by Rollup.
328 * @param context Rollup plugin context used to emit errors.
329 * @param compilerOptions Typescript compiler options.
330 * @param outputOptions Rollup output options.
331 */
332function validatePaths(ts, context, compilerOptions, outputOptions) {
333 if (compilerOptions.out) {
334 context.error(`@rollup/plugin-typescript: Deprecated 'out' option is not supported. Use 'outDir' instead.`);
335 }
336 else if (compilerOptions.outFile) {
337 context.error(`@rollup/plugin-typescript: 'outFile' option is not supported. Use 'outDir' instead.`);
338 }
339 for (const dirProperty of DIRECTORY_PROPS) {
340 if (compilerOptions[dirProperty]) {
341 if (!outputOptions.dir) {
342 context.error(`@rollup/plugin-typescript: 'dir' must be used when '${dirProperty}' is specified.`);
343 }
344 // Checks if the given path lies within Rollup output dir
345 const fromRollupDirToTs = relative(outputOptions.dir, compilerOptions[dirProperty]);
346 if (fromRollupDirToTs.startsWith('..')) {
347 context.error(`@rollup/plugin-typescript: '${dirProperty}' must be located inside 'dir'.`);
348 }
349 }
350 }
351 const tsBuildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(compilerOptions);
352 if (tsBuildInfoPath && compilerOptions.incremental) {
353 if (!outputOptions.dir) {
354 context.error(`@rollup/plugin-typescript: 'dir' must be used when 'tsBuildInfoFile' or 'incremental' are specified.`);
355 }
356 // Checks if the given path lies within Rollup output dir
357 const fromRollupDirToTs = relative(outputOptions.dir, tsBuildInfoPath);
358 if (fromRollupDirToTs.startsWith('..')) {
359 context.error(`@rollup/plugin-typescript: 'tsBuildInfoFile' must be located inside 'dir'.`);
360 }
361 }
362 if (compilerOptions.declaration || compilerOptions.declarationMap) {
363 if (DIRECTORY_PROPS.every((dirProperty) => !compilerOptions[dirProperty])) {
364 context.error(`@rollup/plugin-typescript: 'outDir' or 'declarationDir' must be specified to generate declaration files.`);
365 }
366 }
367}
368
369/**
370 * Checks if the given OutputFile represents some code
371 */
372function isCodeOutputFile(name) {
373 return !isMapOutputFile(name) && !name.endsWith('.d.ts');
374}
375/**
376 * Checks if the given OutputFile represents some source map
377 */
378function isMapOutputFile(name) {
379 return name.endsWith('.map');
380}
381/**
382 * Finds the corresponding emitted Javascript files for a given Typescript file.
383 * @param id Path to the Typescript file.
384 * @param emittedFiles Map of file names to source code,
385 * containing files emitted by the Typescript compiler.
386 */
387function findTypescriptOutput(ts, parsedOptions, id, emittedFiles) {
388 const emittedFileNames = ts.getOutputFileNames(parsedOptions, id, !ts.sys.useCaseSensitiveFileNames);
389 const codeFile = emittedFileNames.find(isCodeOutputFile);
390 const mapFile = emittedFileNames.find(isMapOutputFile);
391 return {
392 code: emittedFiles.get(codeFile),
393 map: emittedFiles.get(mapFile),
394 declarations: emittedFileNames.filter((name) => name !== codeFile && name !== mapFile)
395 };
396}
397
398// `Cannot compile modules into 'es6' when targeting 'ES5' or lower.`
399const CANNOT_COMPILE_ESM = 1204;
400/**
401 * Emit a Rollup warning or error for a Typescript type error.
402 */
403function emitDiagnostic(ts, context, host, diagnostic) {
404 if (diagnostic.code === CANNOT_COMPILE_ESM)
405 return;
406 const { noEmitOnError } = host.getCompilationSettings();
407 // Build a Rollup warning object from the diagnostics object.
408 const warning = diagnosticToWarning(ts, host, diagnostic);
409 // Errors are fatal. Otherwise emit warnings.
410 if (noEmitOnError && diagnostic.category === ts.DiagnosticCategory.Error) {
411 context.error(warning);
412 }
413 else {
414 context.warn(warning);
415 }
416}
417function buildDiagnosticReporter(ts, context, host) {
418 return function reportDiagnostics(diagnostic) {
419 emitDiagnostic(ts, context, host, diagnostic);
420 };
421}
422
423/**
424 * Create a language service host to use with the Typescript compiler & type checking APIs.
425 * Typescript hosts are used to represent the user's system,
426 * with an API for reading files, checking directories and case sensitivity etc.
427 * @see https://github.com/Microsoft/TypeScript/wiki/Using-the-Compiler-API
428 */
429function createWatchHost(ts, context, { formatHost, parsedOptions, writeFile, resolveModule }) {
430 const createProgram = ts.createEmitAndSemanticDiagnosticsBuilderProgram;
431 const baseHost = ts.createWatchCompilerHost(parsedOptions.fileNames, parsedOptions.options, ts.sys, createProgram, buildDiagnosticReporter(ts, context, formatHost),
432 // Ignore watch status changes
433 () => { }, parsedOptions.projectReferences);
434 return Object.assign(Object.assign({}, baseHost), {
435 /** Override the created program so an in-memory emit is used */
436 afterProgramCreate(program) {
437 const origEmit = program.emit;
438 // eslint-disable-next-line no-param-reassign
439 program.emit = (targetSourceFile, _, ...args) => origEmit(targetSourceFile, writeFile, ...args);
440 return baseHost.afterProgramCreate(program);
441 },
442 /** Add helper to deal with module resolution */
443 resolveModuleNames(moduleNames, containingFile) {
444 return moduleNames.map((moduleName) => resolveModule(moduleName, containingFile));
445 } });
446}
447function createWatchProgram(ts, context, options) {
448 return ts.createWatchProgram(createWatchHost(ts, context, options));
449}
450
451function typescript(options = {}) {
452 const { filter, tsconfig, compilerOptions, tslib, typescript: ts } = getPluginOptions(options);
453 const emittedFiles = new Map();
454 const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions);
455 parsedOptions.fileNames = parsedOptions.fileNames.filter(filter);
456 const formatHost = createFormattingHost(ts, parsedOptions.options);
457 const resolveModule = createModuleResolver(ts, formatHost);
458 let program = null;
459 function normalizePath(fileName) {
460 return fileName.split(win32.sep).join(posix.sep);
461 }
462 return {
463 name: 'typescript',
464 buildStart() {
465 emitParsedOptionsErrors(ts, this, parsedOptions);
466 program = createWatchProgram(ts, this, {
467 formatHost,
468 resolveModule,
469 parsedOptions,
470 writeFile(fileName, data) {
471 emittedFiles.set(fileName, data);
472 }
473 });
474 },
475 buildEnd() {
476 var _a;
477 if (process.env.ROLLUP_WATCH !== 'true') {
478 // ESLint doesn't understand optional chaining
479 // eslint-disable-next-line
480 (_a = program) === null || _a === void 0 ? void 0 : _a.close();
481 }
482 },
483 renderStart(outputOptions) {
484 validateSourceMap(this, parsedOptions.options, outputOptions, parsedOptions.autoSetSourceMap);
485 validatePaths(ts, this, parsedOptions.options, outputOptions);
486 },
487 resolveId(importee, importer) {
488 if (importee === 'tslib') {
489 return tslib;
490 }
491 if (!importer)
492 return null;
493 // Convert path from windows separators to posix separators
494 const containingFile = normalizePath(importer);
495 const resolved = resolveModule(importee, containingFile);
496 if (resolved) {
497 if (resolved.extension === '.d.ts')
498 return null;
499 return resolved.resolvedFileName;
500 }
501 return null;
502 },
503 load(id) {
504 if (!filter(id))
505 return null;
506 const output = findTypescriptOutput(ts, parsedOptions, id, emittedFiles);
507 return output.code ? output : null;
508 },
509 generateBundle(outputOptions) {
510 parsedOptions.fileNames.forEach(fileName => {
511 const output = findTypescriptOutput(ts, parsedOptions, fileName, emittedFiles);
512 output.declarations.forEach((id) => {
513 const code = emittedFiles.get(id);
514 if (!code)
515 return;
516 this.emitFile({
517 type: 'asset',
518 fileName: normalizePath(relative(outputOptions.dir, id)),
519 source: code
520 });
521 });
522 });
523 const tsBuildInfoPath = ts.getTsBuildInfoEmitOutputFilePath(parsedOptions.options);
524 if (tsBuildInfoPath) {
525 this.emitFile({
526 type: 'asset',
527 fileName: normalizePath(relative(outputOptions.dir, tsBuildInfoPath)),
528 source: emittedFiles.get(tsBuildInfoPath)
529 });
530 }
531 }
532 };
533}
534
535export default typescript;