diff --git a/packages/plugin-react/CHANGELOG.md b/packages/plugin-react/CHANGELOG.md new file mode 100644 index 00000000..00a03783 --- /dev/null +++ b/packages/plugin-react/CHANGELOG.md @@ -0,0 +1,133 @@ +## [1.3.6](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.5...plugin-react-refresh@1.3.6) (2021-07-27) + + +### Bug Fixes + +* **deps:** update all non-major dependencies ([#4387](https://github.com/vitejs/vite/issues/4387)) ([2f900ba](https://github.com/vitejs/vite/commit/2f900ba4d4ad8061e0046898e8d1de3129e7f784)) + + + +## [1.3.5](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.4...plugin-react-refresh@1.3.5) (2021-07-05) + + + +## [1.3.4](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.3...plugin-react-refresh@1.3.4) (2021-06-27) + + +### Bug Fixes + +* **ci:** fix ci lint step ([#2988](https://github.com/vitejs/vite/issues/2988)) ([4e8ffd8](https://github.com/vitejs/vite/commit/4e8ffd8865e6303d19b5a5ea4501fc54bff4e180)) +* **deps:** update all non-major dependencies ([#3791](https://github.com/vitejs/vite/issues/3791)) ([74d409e](https://github.com/vitejs/vite/commit/74d409eafca8d74ec4a6ece621ea2895bc1f2a32)) + + +### Features + +* **plugin-react-refresh:** add include / exclude options ([#3916](https://github.com/vitejs/vite/issues/3916)) ([c0a4ea1](https://github.com/vitejs/vite/commit/c0a4ea122794973f2e147f9778e5666f6aaca464)) + + + +## [1.3.3](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.2...plugin-react-refresh@1.3.3) (2021-04-24) + + +### Bug Fixes + +* **plugin-react-refresh:** ensure decorators before export, fix [#2776](https://github.com/vitejs/vite/issues/2776) ([#2855](https://github.com/vitejs/vite/issues/2855)) ([16412e3](https://github.com/vitejs/vite/commit/16412e3a9452cbb7d82f72dd3cebfbc822061f05)) +* **react-refresh:** check FunctionDeclaration nodes properly ([#2903](https://github.com/vitejs/vite/issues/2903)) ([2ee017c](https://github.com/vitejs/vite/commit/2ee017c2637a953aa8219571666e4934e78a195e)) + + + +## [1.3.2](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.1...plugin-react-refresh@1.3.2) (2021-03-31) + + +### Bug Fixes + +* ignore babelrc ([#2766](https://github.com/vitejs/vite/issues/2766)) ([23c4114](https://github.com/vitejs/vite/commit/23c41149ddf74261f7615d22e59b39a017b79509)), closes [#2722](https://github.com/vitejs/vite/issues/2722) + + + +## [1.3.1](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.3.0...plugin-react-refresh@1.3.1) (2021-02-10) + + +### Bug Fixes + +* **plugin-react-refresh:** enable parsing for stage 3 and decorators syntax ([565bcb4](https://github.com/vitejs/vite/commit/565bcb4121e678310c26bb249b119da504d13ada)), closes [#1970](https://github.com/vitejs/vite/issues/1970) + + + +# [1.3.0](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.2.2...plugin-react-refresh@1.3.0) (2021-02-08) + + +### Features + +* **plugin-react-refresh:** add jsx metadata ([#1933](https://github.com/vitejs/vite/issues/1933)) ([4037c55](https://github.com/vitejs/vite/commit/4037c55015e74d5e19176bd6ae6bb1c4df157802)) + + + +## [1.2.2](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.2.1...plugin-react-refresh@1.2.2) (2021-02-02) + + +### Bug Fixes + +* **plugin-react-refresh:** do not pick up config file in react-refresh plugin ([9d560d8](https://github.com/vitejs/vite/commit/9d560d8ed23d02c8ce4ec8c4cfa2aab8d30e89f0)) + + + +## [1.2.1](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.2.0...plugin-react-refresh@1.2.1) (2021-02-02) + + +### Bug Fixes + +* **plugin-react-refresh:** avoid using optional chaining for Node 12 compat ([9d9fa17](https://github.com/vitejs/vite/commit/9d9fa1787558f3dcb1866644c0ebbfaa3f208e5d)), closes [#1851](https://github.com/vitejs/vite/issues/1851) + + + +# [1.2.0](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.1.3...plugin-react-refresh@1.2.0) (2021-02-02) + + +### Features + +* **plugin-react-refresh:** allow specifying additional parser plugins ([435da60](https://github.com/vitejs/vite/commit/435da60785aac2d1336cf62e3c5335523606fd7a)) + + + +## [1.1.3](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.1.2...plugin-react-refresh@1.1.3) (2021-01-29) + + +### Bug Fixes + +* **plugin-react-refresh:** fix react refresh with base option ([59c4e7f](https://github.com/vitejs/vite/commit/59c4e7f824a7d7db689215568b66d68570e3f3da)), closes [#1787](https://github.com/vitejs/vite/issues/1787) + + + +## [1.1.2](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.1.1...plugin-react-refresh@1.1.2) (2021-01-19) + + +### Bug Fixes + +* **plugin-react-refresh:** skip during ssr ([d1383ed](https://github.com/vitejs/vite/commit/d1383ed126b37b922a532ff6cb59b32c0a97e1a2)) + + + +## [1.1.1](https://github.com/vitejs/vite/compare/plugin-react-refresh@1.1.0...plugin-react-refresh@1.1.1) (2021-01-06) + + +### Bug Fixes + +* **plugin-react-refresh:** publish d.ts file ([#1347](https://github.com/vitejs/vite/issues/1347)) ([1865e46](https://github.com/vitejs/vite/commit/1865e4683a6b6504f485f565f65ba2f330722018)) + + +### Features + +* vue-jsx support ([e756c48](https://github.com/vitejs/vite/commit/e756c48ed4c7372d4c8e26016ba4b91880e7e248)) + + + +# 1.1.0 (2021-01-02) + + +### Features + +* **plugin-react-refresh:** types ([25d68c1](https://github.com/vitejs/vite/commit/25d68c17228be866152c719f7e2a4fe93cd88b8e)) + + + diff --git a/packages/plugin-react/LICENSE b/packages/plugin-react/LICENSE new file mode 100644 index 00000000..9c1b313d --- /dev/null +++ b/packages/plugin-react/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019-present, Yuxi (Evan) You and Vite contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/plugin-react/README.md b/packages/plugin-react/README.md new file mode 100644 index 00000000..f09b16de --- /dev/null +++ b/packages/plugin-react/README.md @@ -0,0 +1,83 @@ +# @vitejs/plugin-react-refresh [![npm](https://img.shields.io/npm/v/@vitejs/plugin-react-refresh.svg)](https://npmjs.com/package/@vitejs/plugin-react-refresh) + +Provides [React Refresh](https://www.npmjs.com/package/react-refresh) support for Vite. + +```js +// vite.config.js +import reactRefresh from '@vitejs/plugin-react-refresh' + +export default { + plugins: [reactRefresh()] +} +``` + +## Specifying Additional Parser Plugins + +If you are using ES syntax that are still in proposal status (e.g. class properties), you can selectively enable them via the `parserPlugins` option: + +```js +export default { + plugins: [ + reactRefresh({ + parserPlugins: ['classProperties', 'classPrivateProperties'] + }) + ] +} +``` + +## Specifying Additional Babel Plugins + +```js +export default { + plugins: [reactRefresh({ + plugins: ['@emotion/babel-plugin'] + })] +} +``` + +[Full list of Babel parser plugins](https://babeljs.io/docs/en/babel-parser#ecmascript-proposalshttpsgithubcombabelproposals). + +## Specifying files to include or exclude from refreshing + +By default, @vite/plugin-react-refresh will process files ending with `.js`, `.jsx`, `.ts`, and `.tsx`, and excludes all files in `node_modules`. + +In some situations you may not want a file to act as an HMR boundary, instead preferring that the changes propagate higher in the stack before being handled. In these cases, you can provide an `include` and/or `exclude` option, which can be regex or a [picomatch](https://github.com/micromatch/picomatch#globbing-features) pattern, or array of either. Files must match include and not exclude to be processed. Note, when using either `include`, or `exclude`, the defaults will not be merged in, so re-apply them if necessary. + +```js +export default { + plugins: [ + reactRefresh({ + // Exclude storybook stories and node_modules + exclude: [/\.stories\.(t|j)sx?$/, /node_modules/], + // Only .tsx files + include: '**/*.tsx' + }) + ] +} +``` + +### Notes + +- If using TSX, any TS-supported syntax will already be transpiled away so you won't need to specify them here. + +- This option only enables the plugin to parse these syntax - it does not perform any transpilation since this plugin is dev-only. + +- If you wish to transpile the syntax for production, you will need to configure the transform separately using [@rollup/plugin-babel](https://github.com/rollup/plugins/tree/master/packages/babel) as a build-only plugin. + +## Middleware Mode Notes + +When Vite is launched in **Middleware Mode**, you need to make sure your entry `index.html` file is transformed with `ViteDevServer.transformIndexHtml`. Otherwise, you may get an error prompting `Uncaught Error: @vitejs/plugin-react-refresh can't detect preamble. Something is wrong.` + +To mitigate this issue, you can explicitly transform your `index.html` like this when configuring your express server: + +```js +app.get('/', async (req, res, next) => { + try { + let html = fs.readFileSync(path.resolve(root, 'index.html'), 'utf-8') + html = await viteServer.transformIndexHtml(req.url, html) + res.send(html) + } catch (e) { + return next(e) + } +}) +``` diff --git a/packages/plugin-react/package.json b/packages/plugin-react/package.json new file mode 100644 index 00000000..9679df95 --- /dev/null +++ b/packages/plugin-react/package.json @@ -0,0 +1,37 @@ +{ + "name": "@vitejs/plugin-react", + "version": "1.0.0", + "license": "MIT", + "author": "Evan You", + "contributors": [ + "Alec Larson" + ], + "files": [ + "src" + ], + "main": "src/index.js", + "types": "src/index.d.ts", + "scripts": { + "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s --commit-path . --lerna-package plugin-react", + "release": "node ../../scripts/release.js --skipBuild" + }, + "engines": { + "node": ">=12.0.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vitejs/vite.git", + "directory": "packages/plugin-react" + }, + "bugs": { + "url": "https://github.com/vitejs/vite/issues" + }, + "homepage": "https://github.com/vitejs/vite/tree/main/packages/plugin-react#readme", + "dependencies": { + "@babel/core": "^7.14.8", + "@babel/plugin-transform-react-jsx-self": "^7.14.9", + "@babel/plugin-transform-react-jsx-source": "^7.14.5", + "@rollup/pluginutils": "^4.1.1", + "react-refresh": "^0.10.0" + } +} diff --git a/packages/plugin-react/src/index.d.ts b/packages/plugin-react/src/index.d.ts new file mode 100644 index 00000000..20fd573b --- /dev/null +++ b/packages/plugin-react/src/index.d.ts @@ -0,0 +1,14 @@ +import { Plugin } from 'vite' +import { ParserOptions } from '@babel/core' + +type PluginFactory = (options?: Options) => Plugin + +declare const createPlugin: PluginFactory & { preambleCode: string } + +export interface Options { + parserPlugins?: ParserOptions['plugins'] + include?: string | RegExp | Array + exclude?: string | RegExp | Array +} + +export default createPlugin diff --git a/packages/plugin-react/src/index.js b/packages/plugin-react/src/index.js new file mode 100644 index 00000000..5c69a58c --- /dev/null +++ b/packages/plugin-react/src/index.js @@ -0,0 +1,241 @@ +// @ts-check +const fs = require('fs') +const { transformSync, ParserOptions } = require('@babel/core') +const { createFilter } = require('@rollup/pluginutils') + +const runtimePublicPath = '/@react-refresh' +const runtimeFilePath = require.resolve( + 'react-refresh/cjs/react-refresh-runtime.development.js' +) + +const runtimeCode = ` +const exports = {} +${fs.readFileSync(runtimeFilePath, 'utf-8')} +function debounce(fn, delay) { + let handle + return () => { + clearTimeout(handle) + handle = setTimeout(fn, delay) + } +} +exports.performReactRefresh = debounce(exports.performReactRefresh, 16) +export default exports +` + +const preambleCode = ` +import RefreshRuntime from "__BASE__${runtimePublicPath.slice(1)}" +RefreshRuntime.injectIntoGlobalHook(window) +window.$RefreshReg$ = () => {} +window.$RefreshSig$ = () => (type) => type +window.__vite_plugin_react_preamble_installed__ = true +` + +/** + * Transform plugin for transforming and injecting per-file refresh code. + * + * @type {import('.').default} + */ +function reactRefreshPlugin(opts) { + let shouldSkip = false + let base = '/' + const filter = createFilter( + (opts && opts.include) || /\.(t|j)sx?$/, + (opts && opts.exclude) || /node_modules/ + ) + + return { + name: 'react-refresh', + + enforce: 'pre', + + configResolved(config) { + shouldSkip = config.command === 'build' || config.isProduction + base = config.base + }, + + resolveId(id) { + if (id === runtimePublicPath) { + return id + } + }, + + load(id) { + if (id === runtimePublicPath) { + return runtimeCode + } + }, + + transform(code, id, ssr) { + if (shouldSkip || ssr) { + return + } + + if (!filter(id)) { + return + } + + // plain js/ts files can't use React without importing it, so skip + // them whenever possible + if (!id.endsWith('x') && !code.includes('react')) { + return + } + + /** + * @type ParserOptions["plugins"] + */ + const parserPlugins = [ + 'jsx', + 'importMeta', + // since the plugin now applies before esbuild transforms the code, + // we need to enable some stage 3 syntax since they are supported in + // TS and some environments already. + 'topLevelAwait', + 'classProperties', + 'classPrivateProperties', + 'classPrivateMethods' + ] + if (/\.tsx?$/.test(id)) { + // it's a typescript file + // TODO: maybe we need to read tsconfig to determine parser plugins to + // enable here, but allowing decorators by default since it's very + // commonly used with TS. + parserPlugins.push('typescript', 'decorators-legacy') + } + if (opts && Array.isArray(opts.parserPlugins)) { + parserPlugins.push(...opts.parserPlugins) + } + + const isReasonReact = id.endsWith('.bs.js') + const result = transformSync(code, { + babelrc: false, + configFile: false, + filename: id, + parserOpts: { + sourceType: 'module', + allowAwaitOutsideFunction: true, + plugins: parserPlugins + }, + generatorOpts: { + decoratorsBeforeExport: true + }, + plugins: [ + require('@babel/plugin-transform-react-jsx-self'), + require('@babel/plugin-transform-react-jsx-source'), + [require('react-refresh/babel'), { skipEnvCheck: true }], + ...(opts && Array.isArray(opts.plugins) ? opts.plugins : []) + ], + ast: !isReasonReact, + sourceMaps: true, + sourceFileName: id + }) + + if (!/\$RefreshReg\$\(/.test(result.code)) { + // no component detected in the file + return code + } + + const header = ` + import RefreshRuntime from "${runtimePublicPath}"; + + let prevRefreshReg; + let prevRefreshSig; + + if (!window.__vite_plugin_react_preamble_installed__) { + throw new Error( + "@vitejs/plugin-react-refresh can't detect preamble. Something is wrong. " + + "See https://github.com/vitejs/vite-plugin-react/pull/11#discussion_r430879201" + ); + } + + if (import.meta.hot) { + prevRefreshReg = window.$RefreshReg$; + prevRefreshSig = window.$RefreshSig$; + window.$RefreshReg$ = (type, id) => { + RefreshRuntime.register(type, ${JSON.stringify(id)} + " " + id) + }; + window.$RefreshSig$ = RefreshRuntime.createSignatureFunctionForTransform; + }`.replace(/[\n]+/gm, '') + + const footer = ` + if (import.meta.hot) { + window.$RefreshReg$ = prevRefreshReg; + window.$RefreshSig$ = prevRefreshSig; + + ${ + isReasonReact || isRefreshBoundary(result.ast) + ? `import.meta.hot.accept();` + : `` + } + if (!window.__vite_plugin_react_timeout) { + window.__vite_plugin_react_timeout = setTimeout(() => { + window.__vite_plugin_react_timeout = 0; + RefreshRuntime.performReactRefresh(); + }, 30); + } + }` + + return { + code: `${header}${result.code}${footer}`, + map: result.map + } + }, + + transformIndexHtml() { + if (shouldSkip) { + return + } + + return [ + { + tag: 'script', + attrs: { type: 'module' }, + children: preambleCode.replace(`__BASE__`, base) + } + ] + } + } +} + +/** + * @param {import('@babel/core').BabelFileResult['ast']} ast + */ +function isRefreshBoundary(ast) { + // Every export must be a React component. + return ast.program.body.every((node) => { + if (node.type !== 'ExportNamedDeclaration') { + return true + } + const { declaration, specifiers } = node + if (declaration) { + if (declaration.type === 'VariableDeclaration') { + return declaration.declarations.every((variable) => + isComponentLikeIdentifier(variable.id) + ) + } + if (declaration.type === 'FunctionDeclaration') { + return isComponentLikeIdentifier(declaration.id) + } + } + return specifiers.every((spec) => { + return isComponentLikeIdentifier(spec.exported) + }) + }) +} + +/** + * @param {import('@babel/types').Node} node + */ +function isComponentLikeIdentifier(node) { + return node.type === 'Identifier' && isComponentLikeName(node.name) +} + +/** + * @param {string} name + */ +function isComponentLikeName(name) { + return typeof name === 'string' && name[0] >= 'A' && name[0] <= 'Z' +} + +module.exports = reactRefreshPlugin +reactRefreshPlugin['default'] = reactRefreshPlugin +reactRefreshPlugin.preambleCode = preambleCode