Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ This is a [lerna](https://github.com/lerna/lerna) powered mono-repo, composed of

- [flow-runtime](./packages/flow-runtime): The core runtime type system.
- [babel-plugin-flow-runtime](./packages/babel-plugin-flow-runtime): A babel plugin which transforms Flow type annotations into `flow-runtime` invocations.
- [flow-runtime-loader](./packages/flow-runtime-loader): A webpack loader for importing Flow type modules as `flow-runtime` declarations.
- [flow-runtime-validators](./packages/flow-runtime-validators): A collection of common validators for use with flow-runtime.
- [flow-config-parser](./packages/flow-config-parser): Parses flow configuration files.
- [flow-runtime-mobx](./packages/flow-runtime-mobx): Adds mobx support to flow-runtime.
Expand Down
16 changes: 16 additions & 0 deletions packages/flow-runtime-loader/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"presets": [
["@babel/preset-env", {
"targets": {
"node": 4
},
"exclude": [
"transform-regenerator"
]
}],
"@babel/preset-flow"
],
"plugins": [
"@babel/plugin-proposal-object-rest-spread"
]
}
61 changes: 61 additions & 0 deletions packages/flow-runtime-loader/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# flow-runtime-loader

A webpack loader for turning Flow type modules into `flow-runtime` declarations.

This is useful when a dependency ships Flow definitions next to its source, for example `*.js.flow` files, and you want to import those types as runtime validators.

## Installation

```sh
npm install --save-dev flow-runtime-loader
```

## Usage

Inline usage:

```js
import { RawDraftContentState } from 'flow-runtime-loader!draft-js/lib/RawDraftContentState.js.flow';

RawDraftContentState.assert(value);
```

Webpack rule:

```js
module.exports = {
resolve: {
extensions: ['.js.flow', '.js', '.json']
},
module: {
rules: [
{
test: /\.js\.flow$/,
use: [
{
loader: 'flow-runtime-loader',
options: {
assert: true,
annotate: true
}
}
]
}
]
}
};
```

Loader options are passed to `babel-plugin-flow-runtime`. An optional `babel` option can be used to pass additional Babel transform options:

```js
{
loader: 'flow-runtime-loader',
options: {
libraryName: './custom-flow-runtime',
babel: {
sourceMaps: true
}
}
}
```
3 changes: 3 additions & 0 deletions packages/flow-runtime-loader/flow-runtime-loader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
var loader = require('./lib');

module.exports = loader.default || loader;
30 changes: 30 additions & 0 deletions packages/flow-runtime-loader/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "flow-runtime-loader",
"homepage": "https://codemix.github.io/flow-runtime",
"repository": "https://github.com/codemix/flow-runtime.git",
"version": "0.20.0",
"description": "Webpack loader for turning Flow type modules into flow-runtime declarations.",
"main": "flow-runtime-loader.js",
"scripts": {
"prepublishOnly": "npm run test && npm run build",
"build": "rimraf ./lib && babel -d ./lib ./src",
"test": "mocha",
"watch": "mocha --watch"
},
"author": "",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.12.0",
"@babel/preset-flow": "^7.12.0",
"babel-plugin-flow-runtime": "^0.20.0"
},
"devDependencies": {
"@babel/cli": "^7.12.0",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/polyfill": "^7.12.0",
"@babel/preset-env": "^7.12.0",
"@babel/register": "^7.12.0",
"mocha": "^5.0.0",
"rimraf": "^2.6.3"
}
}
126 changes: 126 additions & 0 deletions packages/flow-runtime-loader/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* @flow */

import {transformSync} from '@babel/core';
import flowRuntimePlugin from 'babel-plugin-flow-runtime';

type LoaderContext = {
async?: () => (error: ?Error, result?: string, sourceMap?: Object) => void;
cacheable?: () => void;
getOptions?: () => Object;
query?: string | Object;
resourcePath?: string;
sourceMap?: boolean;
};

type LoaderOptions = {
babel?: Object;
};

export default function flowRuntimeLoader (source: string | Buffer, inputSourceMap?: Object) {
if (this.cacheable) {
this.cacheable();
}

const callback = this.async && this.async();
const input = Buffer.isBuffer(source) ? source.toString() : source;

try {
const options = getOptions(this);
const babelOptions = options.babel || {};
const flowRuntimeOptions = getFlowRuntimeOptions(options);
const result = transformSync(input, {
...babelOptions,
babelrc: babelOptions.babelrc === undefined ? false : babelOptions.babelrc,
configFile: babelOptions.configFile === undefined ? false : babelOptions.configFile,
filename: babelOptions.filename || this.resourcePath,
inputSourceMap,
plugins: getPlugins(babelOptions.plugins, flowRuntimeOptions),
presets: getPresets(babelOptions.presets),
sourceMaps: babelOptions.sourceMaps === undefined ? this.sourceMap || Boolean(inputSourceMap) : babelOptions.sourceMaps
});

const code = result && result.code ? result.code : input;
const map = result && result.map ? result.map : inputSourceMap;

if (callback) {
callback(null, code, map);
return;
}
return code;
}
catch (error) {
if (callback) {
callback(error);
return;
}
throw error;
}
}

function getOptions (context: LoaderContext): LoaderOptions {
if (typeof context.getOptions === 'function') {
return context.getOptions() || {};
}
const {query} = context;
if (!query) {
return {};
}
if (typeof query === 'object') {
return query;
}
return parseQuery(query);
}

function getFlowRuntimeOptions (options: LoaderOptions): Object {
const pluginOptions = {...options};
delete pluginOptions.babel;
return pluginOptions;
}

function getPlugins (plugins?: Array<any>, flowRuntimeOptions: Object): Array<any> {
const result = plugins ? plugins.slice() : [];
result.push([flowRuntimePlugin, flowRuntimeOptions]);
return result;
}

function getPresets (presets?: Array<any>): Array<any> {
const result = presets ? presets.slice() : [];
result.push(require.resolve('@babel/preset-flow'));
return result;
}

function parseQuery (query: string): Object {
const raw = query.charAt(0) === '?' ? query.slice(1) : query;
if (!raw) {
return {};
}
if (raw.charAt(0) === '{') {
return JSON.parse(raw);
}
return raw.split('&').reduce((options, segment) => {
if (!segment) {
return options;
}
const pair = segment.split('=');
const key = decodeURIComponent(pair[0]);
const value = pair.length > 1 ? decodeURIComponent(pair.slice(1).join('=')) : undefined;
options[key] = coerceQueryValue(value);
return options;
}, {});
}

function coerceQueryValue (value?: string): any {
if (value === undefined || value === '') {
return true;
}
if (value === 'true') {
return true;
}
if (value === 'false') {
return false;
}
if (value === 'null') {
return null;
}
return value;
}
108 changes: 108 additions & 0 deletions packages/flow-runtime-loader/src/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* @flow */

import {equal, ok} from 'assert';

import flowRuntimeLoader from './index';

function runLoader (source: string | Buffer, options: Object = {}, context: Object = {}): Promise<Object> {
let cacheable = false;

return new Promise((resolve, reject) => {
const loaderContext = {
resourcePath: '/project/types.js.flow',
sourceMap: false,
getOptions: () => options,
cacheable: () => {
cacheable = true;
},
async: () => (error, code, sourceMap) => {
if (error) {
reject(error);
}
else {
resolve({cacheable, code, sourceMap});
}
},
...context
};

flowRuntimeLoader.call(loaderContext, source);
});
}

describe('flow-runtime-loader', () => {
it('converts exported Flow types into runtime declarations', async () => {
const result = await runLoader(`
// @flow
export type User = {
id: number,
name: string
};
`);

ok(result.cacheable);
ok(result.code.includes('import t from "flow-runtime";'));
ok(result.code.includes('export const User = t.type("User"'));
ok(result.code.includes('t.property("id", t.number())'));
ok(!result.code.includes('export type User'));
});

it('passes loader options to babel-plugin-flow-runtime', async () => {
const result = await runLoader(`
// @flow
export type User = {
name: string
};
`, {
libraryName: './custom-flow-runtime'
});

ok(result.code.includes('import t from "./custom-flow-runtime";'));
});

it('keeps imported Flow types available as runtime references', async () => {
const result = await runLoader(`
// @flow
import type {RawDraftContentBlock} from './RawDraftContentBlock';

export type RawDraftContentState = {
blocks: Array<RawDraftContentBlock>
};
`);

ok(result.code.includes('import { RawDraftContentBlock as _RawDraftContentBlock } from \'./RawDraftContentBlock\';'));
ok(result.code.includes('const RawDraftContentBlock = t.tdz(() => _RawDraftContentBlock);'));
ok(result.code.includes('export const RawDraftContentState = t.type("RawDraftContentState"'));
});

it('supports legacy webpack query options', async () => {
const result = await runLoader(`
// @flow
export type User = {
name: string
};
`, {}, {
getOptions: undefined,
query: '?libraryName=./typed&annotate=false'
});

ok(result.code.includes('import t from "./typed";'));
ok(!result.code.includes('t.annotate'));
});

it('returns synchronously when no async callback is available', () => {
const code = flowRuntimeLoader.call({
resourcePath: '/project/types.js.flow',
sourceMap: false,
getOptions: () => ({})
}, `
// @flow
export type User = {
name: string
};
`);

equal(typeof code, 'string');
ok(code.includes('export const User = t.type("User"'));
});
});
11 changes: 11 additions & 0 deletions packages/flow-runtime-loader/test-polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// istanbul ignore next
try {
(new Function('var a = (args) => true; var b = []; b.push(...b);'))();
console.log('Using modern ES environment.');
require("@babel/register")();
}
catch (e) {
console.log('Using legacy ES environment.');
// Legacy environment.
require("@babel/register");
}
4 changes: 4 additions & 0 deletions packages/flow-runtime-loader/test/mocha.opts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--reporter=dot
--require @babel/polyfill
--require ./test-polyfill.js
src/**/*test.js