Skip to content

Commit

Permalink
feat(typescript): better error when tslib is not installed (#793)
Browse files Browse the repository at this point in the history
* feat(typescript): better error when tslib is not installed

* test: trim snapshot file path
  • Loading branch information
shellscape committed Feb 5, 2021
1 parent a927ed8 commit 031b566
Show file tree
Hide file tree
Showing 15 changed files with 473 additions and 262 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ module.exports = {
tsx: 'never'
}
],
'import/prefer-default-export': 'off',
'import/no-namespace': 'off',
'import/no-named-export': 'off',
'no-redeclare': 'off',
Expand Down
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,12 @@
"@ava/babel": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^4.9.0",
"@typescript-eslint/parser": "^4.9.0",
"ava": "^3.13.0",
"ava": "^3.15.0",
"chalk": "^4.1.0",
"codecov-lite": "^1.0.3",
"del-cli": "^3.0.1",
"eslint-config-rollup": "^1.0.0",
"esm": "^3.2.25",
"execa": "^4.0.3",
"globby": "^11.0.1",
"husky": "^4.2.5",
"lint-staged": "^10.5.2",
Expand All @@ -42,7 +41,6 @@
"prettier-plugin-package": "^1.3.0",
"ts-node": "^8.10.2",
"tsconfig-paths": "^3.9.0",
"tslib": "^2.0.0",
"typescript": "^3.9.7",
"yaml": "^1.10.0"
},
Expand Down
8 changes: 4 additions & 4 deletions packages/typescript/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import * as path from 'path';

import { Plugin, SourceDescription } from 'rollup';
import { Plugin, RollupOptions, SourceDescription } from 'rollup';
import type { Watch } from 'typescript';

import { RollupTypescriptOptions } from '../types';

import createFormattingHost from './diagnostics/host';
import createModuleResolver from './moduleResolution';
import getPluginOptions from './options/plugin';
import { getPluginOptions } from './options/plugin';
import { emitParsedOptionsErrors, parseTypescriptConfig } from './options/tsconfig';
import { validatePaths, validateSourceMap } from './options/validate';
import findTypescriptOutput, { getEmittedFile } from './outputFile';
Expand Down Expand Up @@ -44,10 +44,10 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi
return {
name: 'typescript',

buildStart() {
buildStart(rollupOptions: RollupOptions) {
emitParsedOptionsErrors(ts, this, parsedOptions);

preflight(parsedOptions, this);
preflight({ config: parsedOptions, context: this, rollupOptions, tslib });

// Fixes a memory leak https://github.com/rollup/plugins/issues/322
if (!program) {
Expand Down
6 changes: 3 additions & 3 deletions packages/typescript/src/options/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { createFilter } from '@rollup/pluginutils';
import * as defaultTs from 'typescript';

import { RollupTypescriptOptions, PartialCompilerOptions } from '../../types';
import getTsLibPath from '../tslib';
import { getTsLibPath } from '../tslib';

/**
* Separate the Rollup plugin options from the Typescript compiler options,
Expand All @@ -14,7 +14,7 @@ import getTsLibPath from '../tslib';
* - `typescript`: Instance of Typescript library (possibly custom).
* - `tslib`: ESM code from the tslib helper library (possibly custom).
*/
export default function getPluginOptions(options: RollupTypescriptOptions) {
export const getPluginOptions = (options: RollupTypescriptOptions) => {
const {
cacheDir,
exclude,
Expand All @@ -37,4 +37,4 @@ export default function getPluginOptions(options: RollupTypescriptOptions) {
tslib: tslib || getTsLibPath(),
transformers
};
}
};
25 changes: 20 additions & 5 deletions packages/typescript/src/preflight.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
import { PluginContext } from 'rollup';
import { PluginContext, RollupOptions } from 'rollup';
import { ModuleKind } from 'typescript';

import { TypeScriptConfig } from './options/tsconfig';
// import { resolveIdAsync } from './tslib';

const moduleError = `
Rollup requires that TypeScript produces ES Modules. Unfortunately your configuration specifies a
interface PreflightOptions {
config: TypeScriptConfig;
context: PluginContext;
rollupOptions: RollupOptions;
tslib: any;
}

const pluginName = '@rollup/plugin-typescript';
const moduleErrorMessage = `
${pluginName}: Rollup requires that TypeScript produces ES Modules. Unfortunately your configuration specifies a
"module" other than "esnext". Unless you know what you're doing, please change "module" to "esnext"
in the target tsconfig.json file or plugin options.`.replace(/\n/g, '');

const tsLibErrorMessage = `${pluginName}: Could not find module 'tslib', which is required by this plugin. Is it installed?`;

let undef;
const validModules = [ModuleKind.ES2015, ModuleKind.ES2020, ModuleKind.ESNext, undef];

// eslint-disable-next-line import/prefer-default-export
export const preflight = (config: TypeScriptConfig, context: PluginContext) => {
export const preflight = ({ config, context, rollupOptions, tslib }: PreflightOptions) => {
if (!validModules.includes(config.options.module)) {
context.warn(`@rollup/plugin-typescript: ${moduleError}`);
context.warn(moduleErrorMessage);
}

if (!rollupOptions.preserveModules && tslib === null) {
context.error(tsLibErrorMessage);
}
};
30 changes: 22 additions & 8 deletions packages/typescript/src/tslib.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import resolveId, { AsyncOpts } from 'resolve';
import resolve, { SyncOpts } from 'resolve';

const resolveIdAsync = (file: string, opts: AsyncOpts) =>
new Promise<string>((fulfil, reject) =>
resolveId(file, opts, (err, contents) => (err || typeof contents === 'undefined' ? reject(err) : fulfil(contents)))
);
// const resolveIdAsync = (file: string, opts: AsyncOpts) =>
// new Promise<string>((fulfil, reject) =>
// resolveId(file, opts, (err, contents) =>
// err || typeof contents === 'undefined' ? reject(err) : fulfil(contents)
// )
// );

const resolveId = (file: string, opts: SyncOpts) => resolve.sync(file, opts);

/**
* Returns code asynchronously for the tslib helper library.
*/
export default function getTsLibPath() {
return resolveIdAsync('tslib/tslib.es6.js', { basedir: __dirname });
}
export const getTsLibPath = () => {
// Note: This isn't preferable, but we've no other way to test this bit. Removing the tslib devDep
// during the test run doesn't work due to the nature of the pnpm flat node_modules, and
// other workspace dependencies that depenend upon tslib.
try {
// eslint-disable-next-line no-underscore-dangle
return resolveId(process.env.__TSLIB_TEST_PATH__ || 'tslib/tslib.es6.js', {
basedir: __dirname
});
} catch (_) {
return null;
}
};
32 changes: 32 additions & 0 deletions packages/typescript/test/snapshots/tslib.ts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Snapshot report for `test/tslib.ts`

The actual snapshot is saved in `tslib.ts.snap`.

Generated by [AVA](https://avajs.dev).

## fails on bad tslib path

> Snapshot 1
Error {
code: 'ENOENT',
errno: -2,
path: 'fixtures/joker/tslib.js',
syscall: 'open',
watchFiles: [
'packages/typescript/test/fixtures/overriding-tslib/main.ts',
'fixtures/joker/tslib.js',
],
message: 'Could not load fixtures/joker/tslib.js (imported by fixtures/overriding-tslib/main.ts): ENOENT: no such file or directory, open \'fixtures/joker/tslib.js\'',
}

## fails without tslib installed

> Snapshot 1
Error {
code: 'PLUGIN_ERROR',
hook: 'buildStart',
plugin: 'typescript',
message: '@rollup/plugin-typescript: Could not find module \'tslib\', which is required by this plugin. Is it installed?',
}
Binary file added packages/typescript/test/snapshots/tslib.ts.snap
Binary file not shown.
59 changes: 2 additions & 57 deletions packages/typescript/test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,14 @@ const test = require('ava');
const { rollup, watch } = require('rollup');
const ts = require('typescript');

const { getCode, testBundle } = require('../../../util/test');
const { evaluateBundle, getCode, onwarn } = require('../../../util/test');

const typescript = require('..');

test.beforeEach(() => process.chdir(__dirname));

const outputOptions = { format: 'esm' };

async function evaluateBundle(bundle) {
const { module } = await testBundle(null, bundle);
return module.exports;
}

function onwarn(warning) {
// eslint-disable-next-line no-console
console.warn(warning.toString());
}

test.serial('runs code through typescript', async (t) => {
const bundle = await rollup({
input: 'fixtures/basic/main.ts',
Expand Down Expand Up @@ -263,7 +253,7 @@ test.serial('ignore type errors if noEmitOnError is false', async (t) => {

t.true(code.includes(`console.log('hello world')`));

t.is(warnings.length, 1);
t.is(warnings.length, 2);

t.is(warnings[0].code, 'PLUGIN_WARNING');
t.is(warnings[0].plugin, 'typescript');
Expand Down Expand Up @@ -333,38 +323,6 @@ test.serial('supports overriding the TypeScript version', async (t) => {
t.is(result, 1337);
});

test.serial('supports overriding tslib with a custom path', async (t) => {
const bundle = await rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [
typescript({
tsconfig: 'fixtures/overriding-tslib/tsconfig.json',
tslib: 'fixtures/overriding-tslib/tslib.js'
})
],
onwarn
});
const code = await evaluateBundle(bundle);

t.is(code.myParent.baseMethod(), 'base method');
});

test.serial('supports overriding tslib with a custom path in a promise', async (t) => {
const bundle = await rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [
typescript({
tsconfig: 'fixtures/overriding-tslib/tsconfig.json',
tslib: Promise.resolve('fixtures/overriding-tslib/tslib.js')
})
],
onwarn
});
const code = await evaluateBundle(bundle);

t.is(code.myParent.baseMethod(), 'base method');
});

test.serial('should not resolve .d.ts files', async (t) => {
const bundle = await rollup({
input: 'fixtures/dts/main.ts',
Expand Down Expand Up @@ -521,19 +479,6 @@ test.serial('should throw on bad options', async (t) => {
]);
});

test.serial('creates _tslib.js file when preserveModules is used', async (t) => {
const bundle = await rollup({
input: 'fixtures/preserve-modules/main.ts',
plugins: [typescript({ tsconfig: 'fixtures/preserve-modules/tsconfig.json' })],
preserveModules: true,
onwarn
});

const files = await getCode(bundle, { format: 'es' }, true);
t.true(files[0].fileName.includes('main.js'), files[0].fileName);
t.true(files[1].fileName.includes('tslib.es6.js'), files[1].fileName);
});

test.serial('should handle re-exporting types', async (t) => {
const bundle = await rollup({
input: 'fixtures/reexport-type/main.ts',
Expand Down
4 changes: 4 additions & 0 deletions packages/typescript/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"extends": "../tsconfig.json",
"include": ["."]
}
103 changes: 103 additions & 0 deletions packages/typescript/test/tslib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { platform } from 'os';

import test from 'ava';
import { rollup, RollupError } from 'rollup';

import typescript from '..';

import { evaluateBundle, getCode, onwarn } from '../../../util/test';

test.beforeEach(() => process.chdir(__dirname));

test.serial('supports overriding tslib with a custom path', async (t) => {
const bundle = await rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [
typescript({
tsconfig: 'fixtures/overriding-tslib/tsconfig.json',
tslib: 'fixtures/overriding-tslib/tslib.js'
})
],
onwarn
});
const code = await evaluateBundle(bundle);

t.is((code as any).myParent.baseMethod(), 'base method');
});

test.serial('supports overriding tslib with a custom path in a promise', async (t) => {
const options = {
tsconfig: 'fixtures/overriding-tslib/tsconfig.json',
tslib: Promise.resolve('fixtures/overriding-tslib/tslib.js')
};
const bundle = await rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [typescript(options)],
onwarn
});
const code = await evaluateBundle(bundle);

t.is((code as any).myParent.baseMethod(), 'base method');
});

test.serial('fails on bad tslib path', async (t) => {
const fail = () =>
rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [
typescript({
tsconfig: 'fixtures/overriding-tslib/tsconfig.json',
tslib: 'fixtures/joker/tslib.js'
})
],
onwarn
});

const error = (await t.throwsAsync(fail)) as RollupError;

// Note: I'm done fucking around with Windows paths
if (platform() === 'win32') {
t.pass();
return;
}

if (error.watchFiles) {
let [filePath] = error.watchFiles;
filePath = filePath.substring(filePath.indexOf('packages'));
error.watchFiles[0] = filePath;
}

t.snapshot(error);
});

test.serial('fails without tslib installed', async (t) => {
const fail = () =>
rollup({
input: 'fixtures/overriding-tslib/main.ts',
plugins: [typescript({ tsconfig: 'fixtures/overriding-tslib/tsconfig.json' })],
onwarn
});

// eslint-disable-next-line no-underscore-dangle
process.env.__TSLIB_TEST_PATH__ = 'badtslib/tslib.es6.js';

const error = await t.throwsAsync(fail);

// eslint-disable-next-line no-underscore-dangle, no-undefined
process.env.__TSLIB_TEST_PATH__ = '';

t.snapshot(error);
});

test.serial('creates _tslib.js file when preserveModules is used', async (t) => {
const bundle = await rollup({
input: 'fixtures/preserve-modules/main.ts',
plugins: [typescript({ tsconfig: 'fixtures/preserve-modules/tsconfig.json' })],
preserveModules: true,
onwarn
});

const files = await getCode(bundle, { format: 'es' }, true);
t.true(files[0].fileName.includes('main.js'), files[0].fileName);
t.true(files[1].fileName.includes('tslib.es6.js'), files[1].fileName);
});
Loading

0 comments on commit 031b566

Please sign in to comment.