Skip to content

Commit

Permalink
Extensible Rollup configuration (#183)
Browse files Browse the repository at this point in the history
* Just default to React's url for errors by default

* Add ability to extend babel and rollup

* Document customization

* update docs for boolean extractErrors

* fix minor misunderstanding in docs

* make warning and invariant docs more descriptive

* document error extraction warning

* add fixture
  • Loading branch information
jaredpalmer authored Aug 26, 2019
1 parent 7bf3032 commit cb2cf7c
Show file tree
Hide file tree
Showing 23 changed files with 1,868 additions and 113 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
.rts2_cache_cjs
.rts2_cache_esm
.rts2_cache_umd
dist
dist
tester
103 changes: 91 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ Despite all the recent hype, setting up a new TypeScript (x React) library can b
- [Development-only Expressions + Treeshaking](#development-only-expressions--treeshaking)
- [Rollup Treeshaking](#rollup-treeshaking)
- [Advanced `babel-plugin-dev-expressions`](#advanced-babel-plugin-dev-expressions)
- [`__DEV__`](#dev)
- [`__DEV__`](#__dev__)
- [`invariant`](#invariant)
- [`warning`](#warning)
- [Using lodash](#using-lodash)
- [Error extraction](#error-extraction)
- [Customization](#customization)
- [Rollup](#rollup)
- [Example: Adding Postcss](#example-adding-postcss)
- [Babel](#babel)
- [Inspiration](#inspiration)
- [Comparison to Microbundle](#comparison-to-microbundle)
- [API Reference](#api-reference)
Expand All @@ -44,6 +48,7 @@ TSDX comes with the "battery-pack included" and is part of a complete TypeScript
- Works with React
- Human readable error messages (and in VSCode-friendly format)
- Bundle size snapshots
- Opt-in to extract `invariant` error codes
- Jest test runner setup with sensible defaults via `tsdx test`
- Zero-config, single dependency

Expand Down Expand Up @@ -194,15 +199,15 @@ declare var __DEV__: boolean;
Replaces

```js
invariant(condition, argument, argument);
invariant(condition, 'error message here');
```

with

```js
if (!condition) {
if ('production' !== process.env.NODE_ENV) {
invariant(false, argument, argument);
invariant(false, 'error message here');
} else {
invariant(false);
}
Expand All @@ -211,21 +216,21 @@ if (!condition) {

Note: TSDX doesn't supply an `invariant` function for you, you need to import one yourself. We recommend https://github.com/alexreardon/tiny-invariant.

To extract and minify error codes in production into a static `codes.json` file, pass an `extractErrors` flag with a URL where you will decode the error code. Example: `tsdx build --extractErrors=https://your-url.com/?invariant=`
To extract and minify `invariant` error codes in production into a static `codes.json` file, specify the `--extractErrors` flag in command line. For more details see [Error extraction docs](#error-extraction).

##### `warning`

Replaces

```js
warning(condition, argument, argument);
warning(condition, 'dev warning here');
```

with

```js
if ('production' !== process.env.NODE_ENV) {
warning(condition, argument, argument);
warning(condition, 'dev warning here');
}
```

Expand Down Expand Up @@ -272,13 +277,87 @@ TSDX will rewrite your `import kebabCase from 'lodash/kebabCase'` to `import o f
### Error extraction

_This feature is still under development_
After running `--extractErrors`, you will have a `./errors/codes.json` file with all your extracted `invariant` error codes. This process scans your production code and swaps out your `invariant` error message strings for a corresponding error code (just like React!). This extraction only works if your error checking/warning is done by a function called `invariant`.

After running `--extractErrors`, you will have a `./errors/codes.json` file with all your extracted error codes. This process scans your production code and swaps out your error message strings for a corresponding error code (just like React!). This extraction only works if your error checking/warning is done by a function called `invariant`. Note: you can use either `tiny-invariant` or `tiny-warning`, but you must then import the module as a variable called `invariant` and it should have the same type signature.
Note: We don't provide this function for you, it is up to you how you want it to behave. For example, you can use either `tiny-invariant` or `tiny-warning`, but you must then import the module as a variable called `invariant` and it should have the same type signature.

After that, you will need to host the decoder somewhere (with the URL that you passed in to `--extractErrors`).
⚠️Don't forget: you will need to host the decoder somewhere. Once you have a URL, look at `./errors/ErrorProd.js` and replace the `reactjs.org` URL with yours.

_Simple guide to host error codes to be completed_
> Known issue: our `transformErrorMessages` babel plugin currently doesn't have sourcemap support, so you will see "Sourcemap is likely to be incorrect" warnings. [We would love your help on this.](https://github.com/palmerhq/tsdx/issues/184)
_TODO: Simple guide to host error codes to be completed_

## Customization

### Rollup

TSDX uses Rollup under the hood. The defaults are solid for most packages (Formik uses the defaults!). However, if you do wish to alter the rollup configuration, you can do so by creating a file called `tsdx.config.js` at the root of your project like so:

```js
// Not transpiled with TypeScript or Babel, so use plain Es6/Node.js!
module.exports = {
// This function will run for each entry/format/env combination
rollup(config, options) {
return config; // always return a config.
},
};
```

The `options` object contains the following:

```tsx
export interface TsdxOptions {
// path to file
input: string;
// Safe name (for UMD)
name: string;
// JS target
target: 'node' | 'browser';
// Module format
format: 'cjs' | 'umd' | 'esm';
// Environment
env: 'development' | 'production';
// Path to tsconfig file
tsconfig?: string;
// Is opt-in invariant error extraction active?
extractErrors?: boolean;
// Is minifying?
minify?: boolean;
// Is this the very first rollup config (and thus should one-off metadata be extracted)?
writeMeta?: boolean;
}
```

#### Example: Adding Postcss

```js
const postcss = require('rollup-plugin-postcss');
const autoprefixer = require('autoprefixer');
const cssnano = require('cssnano');

module.exports = {
rollup(config, options) {
config.plugins.push(
postcss({
plugins: [
autoprefixer(),
cssnano({
preset: 'default',
}),
],
inject: false,
// only write out CSS for the first bundle (avoids pointless extra files):
extract: !!options.writeMeta,
})
);
return config;
},
};
```

### Babel

You can add your own `.babelrc` to the root of your project and TSDX will **merge** it with its own babel transforms (which are mostly for optimization).

## Inspiration

Expand Down Expand Up @@ -333,7 +412,7 @@ Options
--target Specify your target environment (default web)
--name Specify name exposed in UMD builds
--format Specify module format(s) (default cjs,esm)
--extractErrors Specify url for extracting error codes
--extractErrors Opt-in to extracting invariant error codes
--tsconfig Specify your custom tsconfig path (default <root-folder>/tsconfig.json)
-h, --help Displays this message

Expand All @@ -342,7 +421,7 @@ Examples
$ tsdx build --target node
$ tsdx build --name Foo
$ tsdx build --format cjs,esm,umd
$ tsdx build --extractErrors=https://reactjs.org/docs/error-decoder.html?invariant=
$ tsdx build --extractErrors
$ tsdx build --tsconfig ./tsconfig.foo.json
```

Expand Down
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"@babel/core": "^7.4.4",
"@babel/helper-module-imports": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.4.4",
"@babel/plugin-transform-regenerator": "^7.4.5",
"@babel/polyfill": "^7.4.4",
"@babel/preset-env": "^7.4.4",
"@typescript-eslint/eslint-plugin": "^1.13.0",
Expand All @@ -43,6 +44,7 @@
"asyncro": "^3.0.0",
"babel-plugin-annotate-pure-calls": "^0.4.0",
"babel-plugin-dev-expression": "^0.2.1",
"babel-plugin-macros": "^2.6.1",
"babel-plugin-transform-async-to-promises": "^0.8.14",
"babel-plugin-transform-rename-import": "^2.3.0",
"babel-traverse": "^6.26.0",
Expand All @@ -65,6 +67,7 @@
"jest": "^24.8.0",
"jest-watch-typeahead": "^0.3.1",
"jpjs": "^1.2.1",
"lodash.merge": "^4.6.2",
"mkdirp": "^0.5.1",
"ora": "^3.4.0",
"pascal-case": "^2.0.1",
Expand Down
153 changes: 153 additions & 0 deletions src/babelPluginTsdx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { createConfigItem } from '@babel/core';
import babelPlugin from 'rollup-plugin-babel';
import merge from 'lodash.merge';

export const isTruthy = (obj?: any) => {
if (!obj) {
return false;
}

return obj.constructor !== Object || Object.keys(obj).length > 0;
};

const replacements = [{ original: 'lodash', replacement: 'lodash-es' }];

export const mergeConfigItems = (type: any, ...configItemsToMerge: any[]) => {
const mergedItems: any[] = [];

configItemsToMerge.forEach(configItemToMerge => {
configItemToMerge.forEach((item: any) => {
const itemToMergeWithIndex = mergedItems.findIndex(
mergedItem => mergedItem.file.resolved === item.file.resolved
);

if (itemToMergeWithIndex === -1) {
mergedItems.push(item);
return;
}

mergedItems[itemToMergeWithIndex] = createConfigItem(
[
mergedItems[itemToMergeWithIndex].file.resolved,
merge(mergedItems[itemToMergeWithIndex].options, item.options),
],
{
type,
}
);
});
});

return mergedItems;
};

export const createConfigItems = (type: any, items: any[]) => {
return items.map(({ name, ...options }) => {
return createConfigItem([require.resolve(name), options], { type });
});
};

export const babelPluginTsdx = babelPlugin.custom((babelCore: any) => ({
// Passed the plugin options.
options({ custom: customOptions, ...pluginOptions }: any) {
return {
// Pull out any custom options that the plugin might have.
customOptions,

// Pass the options back with the two custom options removed.
pluginOptions,
};
},
config(config: any, { customOptions }: any) {
const defaultPlugins = createConfigItems(
'plugin',
[
// {
// name: '@babel/plugin-transform-react-jsx',
// pragma: customOptions.jsx || 'h',
// pragmaFrag: customOptions.jsxFragment || 'Fragment',
// },
{ name: 'babel-plugin-annotate-pure-calls' },
{ name: 'babel-plugin-dev-expression' },
{
name: 'babel-plugin-transform-rename-import',
replacements,
},
isTruthy(customOptions.defines) && {
name: 'babel-plugin-transform-replace-expressions',
replace: customOptions.defines,
},
{
name: 'babel-plugin-transform-async-to-promises',
inlineHelpers: true,
externalHelpers: true,
},
{
name: '@babel/plugin-proposal-class-properties',
loose: true,
},
{
name: '@babel/plugin-transform-regenerator',
async: false,
},
{
name: 'babel-plugin-macros',
},
isTruthy(customOptions.extractErrors) && {
name: './errors/transformErrorMessages',
},
].filter(Boolean)
);

const babelOptions = config.options || {};

const envIdx = (babelOptions.presets || []).findIndex((preset: any) =>
preset.file.request.includes('@babel/preset-env')
);

if (envIdx !== -1) {
const preset = babelOptions.presets[envIdx];
babelOptions.presets[envIdx] = createConfigItem(
[
preset.file.resolved,
merge(
{
loose: true,
targets: customOptions.targets,
},
preset.options,
{
modules: false,
exclude: merge(
['transform-async-to-generator', 'transform-regenerator'],
preset.options.exclude || []
),
}
),
],
{
type: `preset`,
}
);
} else {
babelOptions.presets = createConfigItems('preset', [
{
name: '@babel/preset-env',
targets: customOptions.targets,
modules: false,
loose: true,
exclude: ['transform-async-to-generator', 'transform-regenerator'],
},
]);
}

// Merge babelrc & our plugins together
babelOptions.plugins = mergeConfigItems(
'plugin',
defaultPlugins,
babelOptions.plugins || []
);

return babelOptions;
},
}));
4 changes: 3 additions & 1 deletion src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export const paths = {
testsSetup: resolveApp('test/setupTests.ts'),
appRoot: resolveApp('.'),
appSrc: resolveApp('src'),
appErrorsJson: resolveApp('./codes.json'),
appErrorsJson: resolveApp('errors/codes.json'),
appErrors: resolveApp('errors'),
appDist: resolveApp('dist'),
appConfig: resolveApp('tsdx.config.js'),
};
Loading

0 comments on commit cb2cf7c

Please sign in to comment.