-
Notifications
You must be signed in to change notification settings - Fork 27k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Create React Refresh Utils * Fix Linting * Update Prettier Ignore * fix rules
- Loading branch information
Showing
12 changed files
with
418 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
*.d.ts | ||
*.js |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
72 changes: 72 additions & 0 deletions
72
packages/react-refresh-utils/internal/ReactRefreshModule.runtime.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
Oops, something went wrong.