Skip to content

Commit

Permalink
Create React Refresh Utils (#12006)
Browse files Browse the repository at this point in the history
* Create React Refresh Utils

* Fix Linting

* Update Prettier Ignore

* fix rules
  • Loading branch information
Timer authored Apr 18, 2020
1 parent 55ffb96 commit ec3f8e5
Show file tree
Hide file tree
Showing 12 changed files with 418 additions and 2 deletions.
3 changes: 2 additions & 1 deletion .eslintignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ node_modules
**/dist/**
examples/with-ioc/**
examples/with-kea/**
packages/next/compiled/**/*
packages/next/compiled/**/*
packages/react-refresh-utils/**/*.js
4 changes: 3 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ node_modules
**/.next/**
**/_next/**
**/dist/**
packages/next/compiled/**
packages/next/compiled/**
packages/react-refresh-utils/**/*.js
packages/react-refresh-utils/**/*.d.ts
2 changes: 2 additions & 0 deletions packages/react-refresh-utils/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.d.ts
*.js
17 changes: 17 additions & 0 deletions packages/react-refresh-utils/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# `@next/react-refresh-utils`

This is an **experimental** package that provides utilities for React Refresh.

Its API is not stable as that of Next.js, nor does it follow semver rules.

**Use it at your own risk**.

## Usage

All entrypoints below must wired into your build tooling for this to work.

### `@next/react-refresh-utils/loader`

### `@next/react-refresh-utils/ReactRefreshWebpackPlugin`

### `@next/react-refresh-utils/runtime`
69 changes: 69 additions & 0 deletions packages/react-refresh-utils/ReactRefreshWebpackPlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Compiler, Template, version } from 'webpack'

function webpack4(compiler: Compiler) {
// Webpack 4 does not have a method to handle interception of module
// execution.
// The closest thing we have to emulating this is mimicking the behavior of
// `strictModuleExceptionHandling` in `MainTemplate`:
// https://github.com/webpack/webpack/blob/4c644bf1f7cb067c748a52614500e0e2182b2700/lib/MainTemplate.js#L200

compiler.hooks.compilation.tap('ReactFreshWebpackPlugin', compilation => {
const hookRequire: typeof compilation['mainTemplate']['hooks']['requireExtensions'] = (compilation
.mainTemplate.hooks as any).require

hookRequire.tap('ReactFreshWebpackPlugin', (source, chunk, hash) => {
// Webpack 4 evaluates module code on the following line:
// ```
// modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));
// ```
// https://github.com/webpack/webpack/blob/4c644bf1f7cb067c748a52614500e0e2182b2700/lib/MainTemplate.js#L200

const lines = source.split('\n')
const evalIndex = lines.findIndex(l =>
l.includes('modules[moduleId].call(')
)
// Unable to find the module execution, that's OK:
if (evalIndex === -1) {
return source
}

return Template.asString([
...lines.slice(0, evalIndex),
`
var hasRefresh = !!self.$RefreshInterceptModuleExecution$;
var cleanup = hasRefresh
? self.$RefreshInterceptModuleExecution$(moduleId)
: function() {};
try {
`,
lines[evalIndex],
`
} finally {
cleanup();
}
`,
...lines.slice(evalIndex + 1),
])
})
})
}

class ReactFreshWebpackPlugin {
apply(compiler: Compiler) {
const webpackMajorVersion = parseInt(version ?? '', 10)

switch (webpackMajorVersion) {
case 4: {
webpack4(compiler)
break
}
default: {
throw new Error(
`ReactFreshWebpackPlugin does not support webpack v${webpackMajorVersion}.`
)
}
}
}
}

export default ReactFreshWebpackPlugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { RefreshRuntimeGlobals } from '../runtime'

declare const self: Window & RefreshRuntimeGlobals

type Dictionary = { [key: string]: unknown }
declare const module: {
id: string
__proto__: { exports: unknown }
hot: {
accept: () => void
dispose: (onDispose: (data: Dictionary) => void) => void
invalidate: () => void
data?: Dictionary
}
}

export default function() {
const currentExports = module.__proto__.exports
const prevExports = module.hot.data?.prevExports ?? null

// This cannot happen in MainTemplate because the exports mismatch between
// templating and execution.
self.$RefreshHelpers$.registerExportsForReactRefresh(
currentExports,
module.id
)

// A module can be accepted automatically based on its exports, e.g. when it
// is a Refresh Boundary.
if (self.$RefreshHelpers$.isReactRefreshBoundary(currentExports)) {
// Save the previous exports on update so we can compare the boundary
// signatures.
module.hot.dispose(data => {
data.prevExports = currentExports
})
// Unconditionally accept an update to this module, we'll check if it's
// still a Refresh Boundary later.
module.hot.accept()

// This field is set when the previous version of this module was a Refresh
// Boundary, letting us know we need to check for invalidation or enqueue
// an update.
if (prevExports !== null) {
// A boundary can become ineligible if its exports are incompatible with
// the previous exports.
//
// For example, if you add/remove/change exports, we'll want to
// re-execute the importing modules, and force those components to
// re-render. Similarly, if you convert a class component to a function,
// we want to invalidate the boundary.
if (
self.$RefreshHelpers$.shouldInvalidateReactRefreshBoundary(
prevExports,
currentExports
)
) {
module.hot.invalidate()
} else {
self.$RefreshHelpers$.scheduleUpdate()
}
}
} else {
// Since we just executed the code for the module, it's possible that the
// new exports made it ineligible for being a boundary.
// We only care about the case when we were _previously_ a boundary,
// because we already accepted this update (accidental side effect).
const isNoLongerABoundary = prevExports !== null
if (isNoLongerABoundary) {
module.hot.invalidate()
}
}
}
151 changes: 151 additions & 0 deletions packages/react-refresh-utils/internal/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* MIT License
*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* 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.
*/

// This file is copied from the Metro JavaScript bundler, with minor tweaks for
// webpack 4 compatibility.
//
// https://github.com/facebook/metro/blob/d6b9685c730d0d63577db40f41369157f28dfa3a/packages/metro/src/lib/polyfills/require.js

import RefreshRuntime from 'react-refresh/runtime'

declare const module: {
hot: {
status: () =>
| 'idle'
| 'check'
| 'prepare'
| 'ready'
| 'dispose'
| 'apply'
| 'abort'
| 'fail'
}
}

function registerExportsForReactRefresh(
moduleExports: unknown,
moduleID: string
) {
RefreshRuntime.register(moduleExports, moduleID + ' %exports%')
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
// (This is important for legacy environments.)
return
}
for (const key in moduleExports) {
const exportValue = moduleExports[key]
const typeID = moduleID + ' %exports% ' + key
RefreshRuntime.register(exportValue, typeID)
}
}

function isReactRefreshBoundary(moduleExports: unknown): boolean {
if (RefreshRuntime.isLikelyComponentType(moduleExports)) {
return true
}
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
return false
}
let hasExports = false
let areAllExportsComponents = true
for (const key in moduleExports) {
hasExports = true
if (key === '__esModule') {
continue
}
const exportValue = moduleExports[key]
if (!RefreshRuntime.isLikelyComponentType(exportValue)) {
areAllExportsComponents = false
}
}
return hasExports && areAllExportsComponents
}

function shouldInvalidateReactRefreshBoundary(
prevExports: unknown,
nextExports: unknown
): boolean {
const prevSignature = getRefreshBoundarySignature(prevExports)
const nextSignature = getRefreshBoundarySignature(nextExports)
if (prevSignature.length !== nextSignature.length) {
return true
}
for (let i = 0; i < nextSignature.length; i++) {
if (prevSignature[i] !== nextSignature[i]) {
return true
}
}
return false
}

function getRefreshBoundarySignature(moduleExports: unknown): Array<unknown> {
const signature = []
signature.push(RefreshRuntime.getFamilyByType(moduleExports))
if (moduleExports == null || typeof moduleExports !== 'object') {
// Exit if we can't iterate over exports.
// (This is important for legacy environments.)
return signature
}
for (const key in moduleExports) {
if (key === '__esModule') {
continue
}
const exportValue = moduleExports[key]
signature.push(key)
signature.push(RefreshRuntime.getFamilyByType(exportValue))
}
return signature
}

let isUpdateScheduled: boolean = false
function scheduleUpdate() {
if (isUpdateScheduled) {
return
}

function canApplyUpdate() {
return module.hot.status() === 'idle'
}

isUpdateScheduled = true
setTimeout(() => {
isUpdateScheduled = false

// Only trigger refresh if the webpack HMR state is idle
if (canApplyUpdate()) {
return RefreshRuntime.performReactRefresh()
}

return scheduleUpdate()
}, 30)
}

export default {
registerExportsForReactRefresh,
isReactRefreshBoundary,
shouldInvalidateReactRefreshBoundary,
getRefreshBoundarySignature,
scheduleUpdate,
}
17 changes: 17 additions & 0 deletions packages/react-refresh-utils/loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { loader } from 'webpack'
import RefreshModuleRuntime from './internal/ReactRefreshModule.runtime'

let refreshModuleRuntime = RefreshModuleRuntime.toString()
refreshModuleRuntime = refreshModuleRuntime.slice(
refreshModuleRuntime.indexOf('{') + 1,
refreshModuleRuntime.lastIndexOf('}')
)

const ReactRefreshLoader: loader.Loader = function ReactRefreshLoader(
source,
inputSourceMap
) {
this.callback(null, `${source}\n\n;${refreshModuleRuntime}`, inputSourceMap)
}

export default ReactRefreshLoader
30 changes: 30 additions & 0 deletions packages/react-refresh-utils/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@next/react-refresh-utils",
"version": "9.3.6-canary.3",
"description": "An experimental package providing utilities for React Refresh.",
"repository": {
"url": "zeit/next.js",
"directory": "packages/react-refresh-utils"
},
"files": [
"internal/*.d.ts",
"internal/*.js",
"*.d.ts",
"*.js"
],
"author": "Joe Haddad <timer@zeit.co>",
"license": "MIT",
"scripts": {
"prepublish": "tsc -d -p tsconfig.json",
"build": "tsc -d -w -p tsconfig.json"
},
"dependencies": {},
"peerDependencies": {
"react-refresh": "0.8.1",
"webpack": "^4"
},
"devDependencies": {
"react-refresh": "0.8.1",
"webpack": "4.42.1"
}
}
Loading

0 comments on commit ec3f8e5

Please sign in to comment.