From e1c544cd758d4410b0cd4c2548dc597bb5bda9f1 Mon Sep 17 00:00:00 2001 From: Tim Neutkens Date: Thu, 30 Aug 2018 14:02:18 +0200 Subject: [PATCH] Fix loading... issue (#5058) This fixes intermittent `loading...` on some requests which surfaced on zeit.co when upgrading --- build/babel/plugins/react-loadable-plugin.js | 23 +- .../webpack/plugins/react-loadable-plugin.js | 23 +- .../format-webpack-messages.js | 26 +- client/dev-error-overlay/hot-dev-client.js | 23 ++ client/index.js | 2 +- flow-typed/npm/react-loadable_vx.x.x.js | 60 ---- lib/dynamic.js | 2 +- lib/loadable.js | 336 ++++++++++++++++++ package.json | 1 - server/render.js | 2 +- yarn.lock | 8 +- 11 files changed, 430 insertions(+), 76 deletions(-) delete mode 100644 flow-typed/npm/react-loadable_vx.x.x.js create mode 100644 lib/loadable.js diff --git a/build/babel/plugins/react-loadable-plugin.js b/build/babel/plugins/react-loadable-plugin.js index c9f42a67..29159ac4 100644 --- a/build/babel/plugins/react-loadable-plugin.js +++ b/build/babel/plugins/react-loadable-plugin.js @@ -1,13 +1,34 @@ +/** +COPYRIGHT (c) 2017-present James Kyle + MIT License + 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 SOFTWAR +*/ // This file is https://github.com/jamiebuilds/react-loadable/blob/master/src/babel.js // Modified to also look for `next/dynamic` // Modified to put `webpack` and `modules` under `loadableGenerated` to be backwards compatible with next/dynamic which has a `modules` key // Modified to support `dynamic(import('something'))` and `dynamic(import('something'), options) + export default function ({ types: t, template }) { return { visitor: { ImportDeclaration (path) { let source = path.node.source.value - if (source !== 'next/dynamic' && source !== 'react-loadable') return + if (source !== 'next/dynamic') return let defaultSpecifier = path.get('specifiers').find(specifier => { return specifier.isImportDefaultSpecifier() diff --git a/build/webpack/plugins/react-loadable-plugin.js b/build/webpack/plugins/react-loadable-plugin.js index 7f516c95..15999944 100644 --- a/build/webpack/plugins/react-loadable-plugin.js +++ b/build/webpack/plugins/react-loadable-plugin.js @@ -1,6 +1,27 @@ +/** +COPYRIGHT (c) 2017-present James Kyle + MIT License + 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 SOFTWAR +*/ // Implementation of this PR: https://github.com/jamiebuilds/react-loadable/pull/132 // Modified to strip out unneeded results for Next's specific use case -const url = require('url') + +import url from 'url' function buildManifest (compiler, compilation) { let context = compiler.options.context diff --git a/client/dev-error-overlay/format-webpack-messages.js b/client/dev-error-overlay/format-webpack-messages.js index 2814d300..d26bcb39 100644 --- a/client/dev-error-overlay/format-webpack-messages.js +++ b/client/dev-error-overlay/format-webpack-messages.js @@ -1,11 +1,31 @@ +/** +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +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 based on https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/formatWebpackMessages.js // It's been edited to remove chalk 'use strict' -// WARNING: this code is untranspiled and is used in browser too. -// Please make sure any changes are in ES5 or contribute a Babel compile step. - // Some custom utilities to prettify Webpack output. // This is quite hacky and hopefully won't be needed when Webpack fixes this. // https://github.com/webpack/webpack/issues/2878 diff --git a/client/dev-error-overlay/hot-dev-client.js b/client/dev-error-overlay/hot-dev-client.js index 29b65e74..3c93256a 100644 --- a/client/dev-error-overlay/hot-dev-client.js +++ b/client/dev-error-overlay/hot-dev-client.js @@ -1,4 +1,27 @@ /* eslint-disable camelcase */ +/** +MIT License + +Copyright (c) 2013-present, Facebook, Inc. + +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 based on https://github.com/facebook/create-react-app/blob/v1.1.4/packages/react-dev-utils/webpackHotDevClient.js // It's been edited to rely on webpack-hot-middleware and to be more compatible with SSR / Next.js diff --git a/client/index.js b/client/index.js index 9a0aaeb5..691d7f9f 100644 --- a/client/index.js +++ b/client/index.js @@ -8,7 +8,7 @@ import PageLoader from '../lib/page-loader' import * as asset from '../lib/asset' import * as envConfig from '../lib/runtime-config' import ErrorBoundary from './error-boundary' -import Loadable from 'react-loadable' +import Loadable from '../lib/loadable' // Polyfill Promise globally // This is needed because Webpack's dynamic loading(common chunks) code diff --git a/flow-typed/npm/react-loadable_vx.x.x.js b/flow-typed/npm/react-loadable_vx.x.x.js deleted file mode 100644 index 8d368f32..00000000 --- a/flow-typed/npm/react-loadable_vx.x.x.js +++ /dev/null @@ -1,60 +0,0 @@ -// flow-typed signature: 5c815d97bc322b44aa30656fc2619bb0 -// flow-typed version: <>/react-loadable_v5.x/flow_v0.73.0 - -/** - * This is an autogenerated libdef stub for: - * - * 'react-loadable' - * - * Fill this stub out by replacing all the `any` types. - * - * Once filled out, we encourage you to share your work with the - * community by sending a pull request to: - * https://github.com/flowtype/flow-typed - */ - -declare module 'react-loadable' { - declare module.exports: any; -} - -/** - * We include stubs for each file inside this npm package in case you need to - * require those files directly. Feel free to delete any files that aren't - * needed. - */ -declare module 'react-loadable/babel' { - declare module.exports: any; -} - -declare module 'react-loadable/lib/babel' { - declare module.exports: any; -} - -declare module 'react-loadable/lib/index' { - declare module.exports: any; -} - -declare module 'react-loadable/lib/webpack' { - declare module.exports: any; -} - -declare module 'react-loadable/webpack' { - declare module.exports: any; -} - -// Filename aliases -declare module 'react-loadable/babel.js' { - declare module.exports: $Exports<'react-loadable/babel'>; -} -declare module 'react-loadable/lib/babel.js' { - declare module.exports: $Exports<'react-loadable/lib/babel'>; -} -declare module 'react-loadable/lib/index.js' { - declare module.exports: $Exports<'react-loadable/lib/index'>; -} -declare module 'react-loadable/lib/webpack.js' { - declare module.exports: $Exports<'react-loadable/lib/webpack'>; -} -declare module 'react-loadable/webpack.js' { - declare module.exports: $Exports<'react-loadable/webpack'>; -} diff --git a/lib/dynamic.js b/lib/dynamic.js index 1fda4be2..74ef2a00 100644 --- a/lib/dynamic.js +++ b/lib/dynamic.js @@ -2,7 +2,7 @@ import type {ElementType} from 'react' import React from 'react' -import Loadable from 'react-loadable' +import Loadable from './loadable' type ImportedComponent = Promise diff --git a/lib/loadable.js b/lib/loadable.js new file mode 100644 index 00000000..e53f7e07 --- /dev/null +++ b/lib/loadable.js @@ -0,0 +1,336 @@ +/** +@copyright (c) 2017-present James Kyle + MIT License + 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 SOFTWAR +*/ +// https://github.com/jamiebuilds/react-loadable/blob/v5.5.0/src/index.js +// Modified to be compatible with webpack 4 / Next.js + +import React from 'react' +import PropTypes from 'prop-types' + +const ALL_INITIALIZERS = [] +const READY_INITIALIZERS = [] + +function isWebpackReady (getModuleIds) { + return getModuleIds().every(moduleId => { + return ( + typeof moduleId !== 'undefined' + ) + }) +} + +function load (loader) { + let promise = loader() + + let state = { + loading: true, + loaded: null, + error: null + } + + state.promise = promise + .then(loaded => { + state.loading = false + state.loaded = loaded + return loaded + }) + .catch(err => { + state.loading = false + state.error = err + throw err + }) + + return state +} + +function loadMap (obj) { + let state = { + loading: false, + loaded: {}, + error: null + } + + let promises = [] + + try { + Object.keys(obj).forEach(key => { + let result = load(obj[key]) + + if (!result.loading) { + state.loaded[key] = result.loaded + state.error = result.error + } else { + state.loading = true + } + + promises.push(result.promise) + + result.promise + .then(res => { + state.loaded[key] = res + }) + .catch(err => { + state.error = err + }) + }) + } catch (err) { + state.error = err + } + + state.promise = Promise.all(promises) + .then(res => { + state.loading = false + return res + }) + .catch(err => { + state.loading = false + throw err + }) + + return state +} + +function resolve (obj) { + return obj && obj.__esModule ? obj.default : obj +} + +function render (loaded, props) { + return React.createElement(resolve(loaded), props) +} + +function createLoadableComponent (loadFn, options) { + if (!options.loading) { + throw new Error('react-loadable requires a `loading` component') + } + + let opts = Object.assign( + { + loader: null, + loading: null, + delay: 200, + timeout: null, + render: render, + webpack: null, + modules: null + }, + options + ) + + let res = null + + function init () { + if (!res) { + res = loadFn(opts.loader) + } + return res.promise + } + + ALL_INITIALIZERS.push(init) + + if (typeof opts.webpack === 'function') { + READY_INITIALIZERS.push(() => { + if (isWebpackReady(opts.webpack)) { + return init() + } + }) + } + + return class LoadableComponent extends React.Component { + constructor (props) { + super(props) + init() + + this.state = { + error: res.error, + pastDelay: false, + timedOut: false, + loading: res.loading, + loaded: res.loaded + } + } + + static contextTypes = { + loadable: PropTypes.shape({ + report: PropTypes.func.isRequired + }) + }; + + static preload () { + return init() + } + + componentWillMount () { + this._mounted = true + this._loadModule() + } + + _loadModule () { + if (this.context.loadable && Array.isArray(opts.modules)) { + opts.modules.forEach(moduleName => { + this.context.loadable.report(moduleName) + }) + } + + if (!res.loading) { + return + } + + if (typeof opts.delay === 'number') { + if (opts.delay === 0) { + this.setState({ pastDelay: true }) + } else { + this._delay = setTimeout(() => { + this.setState({ pastDelay: true }) + }, opts.delay) + } + } + + if (typeof opts.timeout === 'number') { + this._timeout = setTimeout(() => { + this.setState({ timedOut: true }) + }, opts.timeout) + } + + let update = () => { + if (!this._mounted) { + return + } + + this.setState({ + error: res.error, + loaded: res.loaded, + loading: res.loading + }) + + this._clearTimeouts() + } + + res.promise + .then(() => { + update() + }) + // eslint-disable-next-line handle-callback-err + .catch(err => { + update() + }) + } + + componentWillUnmount () { + this._mounted = false + this._clearTimeouts() + } + + _clearTimeouts () { + clearTimeout(this._delay) + clearTimeout(this._timeout) + } + + retry = () => { + this.setState({ error: null, loading: true, timedOut: false }) + res = loadFn(opts.loader) + this._loadModule() + }; + + render () { + if (this.state.loading || this.state.error) { + return React.createElement(opts.loading, { + isLoading: this.state.loading, + pastDelay: this.state.pastDelay, + timedOut: this.state.timedOut, + error: this.state.error, + retry: this.retry + }) + } else if (this.state.loaded) { + return opts.render(this.state.loaded, this.props) + } else { + return null + } + } + } +} + +function Loadable (opts) { + return createLoadableComponent(load, opts) +} + +function LoadableMap (opts) { + if (typeof opts.render !== 'function') { + throw new Error('LoadableMap requires a `render(loaded, props)` function') + } + + return createLoadableComponent(loadMap, opts) +} + +Loadable.Map = LoadableMap + +class Capture extends React.Component { + static propTypes = { + report: PropTypes.func.isRequired + }; + + static childContextTypes = { + loadable: PropTypes.shape({ + report: PropTypes.func.isRequired + }).isRequired + }; + + getChildContext () { + return { + loadable: { + report: this.props.report + } + } + } + + render () { + return React.Children.only(this.props.children) + } +} + +Loadable.Capture = Capture + +function flushInitializers (initializers) { + let promises = [] + + while (initializers.length) { + let init = initializers.pop() + promises.push(init()) + } + + return Promise.all(promises).then(() => { + if (initializers.length) { + return flushInitializers(initializers) + } + }) +} + +Loadable.preloadAll = () => { + return new Promise((resolve, reject) => { + flushInitializers(ALL_INITIALIZERS).then(resolve, reject) + }) +} + +Loadable.preloadReady = () => { + return new Promise((resolve, reject) => { + // We always will resolve, errors should be handled within loading UIs. + flushInitializers(READY_INITIALIZERS).then(resolve, resolve) + }) +} + +module.exports = Loadable diff --git a/package.json b/package.json index 780538de..48e71807 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,6 @@ "prop-types": "15.6.0", "prop-types-exact": "1.1.1", "react-error-overlay": "4.0.0", - "react-loadable": "5.4.0", "recursive-copy": "2.0.6", "resolve": "1.5.0", "send": "0.16.1", diff --git a/server/render.js b/server/render.js index c1d89a83..38bb7f57 100644 --- a/server/render.js +++ b/server/render.js @@ -9,7 +9,7 @@ import { Router } from '../lib/router' import { loadGetInitialProps, isResSent } from '../lib/utils' import Head, { defaultHead } from '../lib/head' import ErrorDebug from '../lib/error-debug' -import Loadable from 'react-loadable' +import Loadable from '../lib/loadable' import { BUILD_MANIFEST, REACT_LOADABLE_MANIFEST, SERVER_DIRECTORY, CLIENT_STATIC_FILES_PATH } from '../lib/constants' // Based on https://github.com/jamiebuilds/react-loadable/pull/132 diff --git a/yarn.lock b/yarn.lock index 4d4cc0a3..d4946413 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6363,7 +6363,7 @@ prop-types@15.6.0: loose-envify "^1.3.1" object-assign "^4.1.1" -prop-types@^15.5.0, prop-types@^15.6.0: +prop-types@^15.6.0: version "15.6.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102" dependencies: @@ -6517,12 +6517,6 @@ react-error-overlay@4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-4.0.0.tgz#d198408a85b4070937a98667f500c832f86bd5d4" -react-loadable@5.4.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/react-loadable/-/react-loadable-5.4.0.tgz#3b6b7d51121a7868fd155be848a36e02084742c9" - dependencies: - prop-types "^15.5.0" - react@16.4.2: version "16.4.2" resolved "https://registry.yarnpkg.com/react/-/react-16.4.2.tgz#2cd90154e3a9d9dd8da2991149fdca3c260e129f"