From 0956f5f7bcbf46c62c564c5e9703fe02633b0b3c Mon Sep 17 00:00:00 2001 From: Molang Date: Sat, 9 May 2026 12:06:30 +0800 Subject: [PATCH] Add flow-runtime webpack loader --- README.md | 1 + packages/flow-runtime-loader/.babelrc | 16 +++ packages/flow-runtime-loader/README.md | 61 +++++++++ .../flow-runtime-loader.js | 3 + packages/flow-runtime-loader/package.json | 30 +++++ packages/flow-runtime-loader/src/index.js | 126 ++++++++++++++++++ .../flow-runtime-loader/src/index.test.js | 108 +++++++++++++++ packages/flow-runtime-loader/test-polyfill.js | 11 ++ packages/flow-runtime-loader/test/mocha.opts | 4 + 9 files changed, 360 insertions(+) create mode 100644 packages/flow-runtime-loader/.babelrc create mode 100644 packages/flow-runtime-loader/README.md create mode 100644 packages/flow-runtime-loader/flow-runtime-loader.js create mode 100644 packages/flow-runtime-loader/package.json create mode 100644 packages/flow-runtime-loader/src/index.js create mode 100644 packages/flow-runtime-loader/src/index.test.js create mode 100644 packages/flow-runtime-loader/test-polyfill.js create mode 100644 packages/flow-runtime-loader/test/mocha.opts diff --git a/README.md b/README.md index 2c833ec0..1417990f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/packages/flow-runtime-loader/.babelrc b/packages/flow-runtime-loader/.babelrc new file mode 100644 index 00000000..e4a44698 --- /dev/null +++ b/packages/flow-runtime-loader/.babelrc @@ -0,0 +1,16 @@ +{ + "presets": [ + ["@babel/preset-env", { + "targets": { + "node": 4 + }, + "exclude": [ + "transform-regenerator" + ] + }], + "@babel/preset-flow" + ], + "plugins": [ + "@babel/plugin-proposal-object-rest-spread" + ] +} diff --git a/packages/flow-runtime-loader/README.md b/packages/flow-runtime-loader/README.md new file mode 100644 index 00000000..845660c4 --- /dev/null +++ b/packages/flow-runtime-loader/README.md @@ -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 + } + } +} +``` diff --git a/packages/flow-runtime-loader/flow-runtime-loader.js b/packages/flow-runtime-loader/flow-runtime-loader.js new file mode 100644 index 00000000..c24cc26c --- /dev/null +++ b/packages/flow-runtime-loader/flow-runtime-loader.js @@ -0,0 +1,3 @@ +var loader = require('./lib'); + +module.exports = loader.default || loader; diff --git a/packages/flow-runtime-loader/package.json b/packages/flow-runtime-loader/package.json new file mode 100644 index 00000000..7a1e6e76 --- /dev/null +++ b/packages/flow-runtime-loader/package.json @@ -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" + } +} diff --git a/packages/flow-runtime-loader/src/index.js b/packages/flow-runtime-loader/src/index.js new file mode 100644 index 00000000..af5f2a4e --- /dev/null +++ b/packages/flow-runtime-loader/src/index.js @@ -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, flowRuntimeOptions: Object): Array { + const result = plugins ? plugins.slice() : []; + result.push([flowRuntimePlugin, flowRuntimeOptions]); + return result; +} + +function getPresets (presets?: Array): Array { + 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; +} diff --git a/packages/flow-runtime-loader/src/index.test.js b/packages/flow-runtime-loader/src/index.test.js new file mode 100644 index 00000000..40e9fdf4 --- /dev/null +++ b/packages/flow-runtime-loader/src/index.test.js @@ -0,0 +1,108 @@ +/* @flow */ + +import {equal, ok} from 'assert'; + +import flowRuntimeLoader from './index'; + +function runLoader (source: string | Buffer, options: Object = {}, context: Object = {}): Promise { + 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 + }; + `); + + 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"')); + }); +}); diff --git a/packages/flow-runtime-loader/test-polyfill.js b/packages/flow-runtime-loader/test-polyfill.js new file mode 100644 index 00000000..6e06028d --- /dev/null +++ b/packages/flow-runtime-loader/test-polyfill.js @@ -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"); +} diff --git a/packages/flow-runtime-loader/test/mocha.opts b/packages/flow-runtime-loader/test/mocha.opts new file mode 100644 index 00000000..c7c549e5 --- /dev/null +++ b/packages/flow-runtime-loader/test/mocha.opts @@ -0,0 +1,4 @@ +--reporter=dot +--require @babel/polyfill +--require ./test-polyfill.js +src/**/*test.js