From 007ec72310be856c6ea011dfc491bdab4a9d8a05 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 29 Jul 2022 11:38:58 +0200 Subject: [PATCH 1/3] Add shared dynamic api --- .../components/shared/dynamic/index.tsx | 50 ++++ .../components/shared/dynamic/loadable.d.ts | 15 + .../components/shared/dynamic/loadable.js | 259 ++++++++++++++++++ .../index/dynamic-imports/dynamic.client.js | 7 + .../index/dynamic-imports/dynamic.server.js | 7 + .../react-lazy.client.js | 2 +- .../app/app/dashboard/index/dynamic.client.js | 4 +- .../app/app/dashboard/index/dynamic.server.js | 9 + .../dashboard/index/next-dynamic.client.js | 7 - .../app/app/dashboard/index/page.server.js | 6 +- test/e2e/app-dir/index.test.ts | 7 +- 11 files changed, 359 insertions(+), 14 deletions(-) create mode 100644 packages/next/client/components/shared/dynamic/index.tsx create mode 100644 packages/next/client/components/shared/dynamic/loadable.d.ts create mode 100644 packages/next/client/components/shared/dynamic/loadable.js create mode 100644 test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js create mode 100644 test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js rename test/e2e/app-dir/app/app/dashboard/index/{ => dynamic-imports}/react-lazy.client.js (80%) create mode 100644 test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js delete mode 100644 test/e2e/app-dir/app/app/dashboard/index/next-dynamic.client.js diff --git a/packages/next/client/components/shared/dynamic/index.tsx b/packages/next/client/components/shared/dynamic/index.tsx new file mode 100644 index 000000000000..6af3e82771b3 --- /dev/null +++ b/packages/next/client/components/shared/dynamic/index.tsx @@ -0,0 +1,50 @@ +import React from 'react' +import Loadable from './loadable' + +export type LoaderComponent

= Promise< + React.ComponentType

| { default: React.ComponentType

} +> + +export type Loader

= (() => LoaderComponent

) | LoaderComponent

+ +export type LoaderMap = { [mdule: string]: () => Loader } + +export type LoadableGeneratedOptions = { + webpack?(): any + modules?(): LoaderMap +} + +export type DynamicOptions

= LoadableGeneratedOptions & { + loader?: Loader

+ loadableGenerated?: LoadableGeneratedOptions +} + +export type LoadableOptions

= DynamicOptions

+ +export type LoadableFn

= ( + opts: LoadableOptions

+) => React.ComponentType

+ +export type LoadableComponent

= React.ComponentType

+ +export default function dynamic

( + loader: Loader

, + options?: DynamicOptions

+): React.ComponentType

{ + let loadableFn: LoadableFn

= Loadable + let loadableOptions: LoadableOptions

= { + ...options, + loader, + } + + // coming from build/babel/plugins/react-loadable-plugin.js + if (loadableOptions.loadableGenerated) { + loadableOptions = { + ...loadableOptions, + ...loadableOptions.loadableGenerated, + } + delete loadableOptions.loadableGenerated + } + + return loadableFn(loadableOptions) +} diff --git a/packages/next/client/components/shared/dynamic/loadable.d.ts b/packages/next/client/components/shared/dynamic/loadable.d.ts new file mode 100644 index 000000000000..505fa09be9c3 --- /dev/null +++ b/packages/next/client/components/shared/dynamic/loadable.d.ts @@ -0,0 +1,15 @@ +/* tslint:disable */ +import React from 'react' + +declare namespace LoadableExport { + interface ILoadable { +

(opts: any): React.ComponentClass

+ Map

(opts: any): React.ComponentType

+ preloadAll(): Promise + } +} + +// eslint-disable-next-line no-redeclare +declare const LoadableExport: LoadableExport.ILoadable + +export = LoadableExport diff --git a/packages/next/client/components/shared/dynamic/loadable.js b/packages/next/client/components/shared/dynamic/loadable.js new file mode 100644 index 000000000000..6ee29a906e93 --- /dev/null +++ b/packages/next/client/components/shared/dynamic/loadable.js @@ -0,0 +1,259 @@ +/** +@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 SOFTWARE +*/ +// https://github.com/jamiebuilds/react-loadable/blob/v5.5.0/src/index.js +// Modified to be compatible with webpack 4 / Next.js +// Modified to only work with React.lazy for app layout + +import React from 'react' +import { LoadableContext } from '../../../../shared/lib/loadable-context' + +const ALL_INITIALIZERS = [] +const READY_INITIALIZERS = [] +let initialized = false + +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 createLoadableComponent(loadFn, options) { + let opts = Object.assign( + { + loader: null, + loading: null, + delay: 200, + timeout: null, + webpack: null, + modules: null, + suspense: false, + }, + options + ) + + opts.lazy = React.lazy(opts.loader) + + /** @type LoadableSubscription */ + let subscription = null + function init() { + if (!subscription) { + const sub = new LoadableSubscription(loadFn, opts) + subscription = { + getCurrentValue: sub.getCurrentValue.bind(sub), + subscribe: sub.subscribe.bind(sub), + retry: sub.retry.bind(sub), + promise: sub.promise.bind(sub), + } + } + return subscription.promise() + } + + // Server only + if (typeof window === 'undefined') { + ALL_INITIALIZERS.push(init) + } + + // Client only + if (!initialized && typeof window !== 'undefined') { + // require.resolveWeak check is needed for environments that don't have it available like Jest + const moduleIds = + opts.webpack && typeof require.resolveWeak === 'function' + ? opts.webpack() + : opts.modules + if (moduleIds) { + READY_INITIALIZERS.push((ids) => { + for (const moduleId of moduleIds) { + if (ids.indexOf(moduleId) !== -1) { + return init() + } + } + }) + } + } + + function useLoadableModule() { + init() + + const context = React.useContext(LoadableContext) + if (context && Array.isArray(opts.modules)) { + opts.modules.forEach((moduleName) => { + context(moduleName) + }) + } + } + + function LoadableComponent(props, ref) { + useLoadableModule() + + return React.createElement(opts.lazy, { ...props, ref }) + } + + LoadableComponent.preload = () => init() + LoadableComponent.displayName = 'LoadableComponent' + + return React.forwardRef(LoadableComponent) +} + +class LoadableSubscription { + constructor(loadFn, opts) { + this._loadFn = loadFn + this._opts = opts + this._callbacks = new Set() + this._delay = null + this._timeout = null + + this.retry() + } + + promise() { + return this._res.promise + } + + retry() { + this._clearTimeouts() + this._res = this._loadFn(this._opts.loader) + + this._state = { + pastDelay: false, + timedOut: false, + } + + const { _res: res, _opts: opts } = this + + if (res.loading) { + if (typeof opts.delay === 'number') { + if (opts.delay === 0) { + this._state.pastDelay = true + } else { + this._delay = setTimeout(() => { + this._update({ + pastDelay: true, + }) + }, opts.delay) + } + } + + if (typeof opts.timeout === 'number') { + this._timeout = setTimeout(() => { + this._update({ timedOut: true }) + }, opts.timeout) + } + } + + this._res.promise + .then(() => { + this._update({}) + this._clearTimeouts() + }) + .catch((_err) => { + this._update({}) + this._clearTimeouts() + }) + this._update({}) + } + + _update(partial) { + this._state = { + ...this._state, + error: this._res.error, + loaded: this._res.loaded, + loading: this._res.loading, + ...partial, + } + this._callbacks.forEach((callback) => callback()) + } + + _clearTimeouts() { + clearTimeout(this._delay) + clearTimeout(this._timeout) + } + + getCurrentValue() { + return this._state + } + + subscribe(callback) { + this._callbacks.add(callback) + return () => { + this._callbacks.delete(callback) + } + } +} + +function Loadable(opts) { + return createLoadableComponent(load, opts) +} + +function flushInitializers(initializers, ids) { + let promises = [] + + while (initializers.length) { + let init = initializers.pop() + promises.push(init(ids)) + } + + return Promise.all(promises).then(() => { + if (initializers.length) { + return flushInitializers(initializers, ids) + } + }) +} + +Loadable.preloadAll = () => { + return new Promise((resolveInitializers, reject) => { + flushInitializers(ALL_INITIALIZERS).then(resolveInitializers, reject) + }) +} + +Loadable.preloadReady = (ids = []) => { + return new Promise((resolvePreload) => { + const res = () => { + initialized = true + return resolvePreload() + } + // We always will resolve, errors should be handled within loading UIs. + flushInitializers(READY_INITIALIZERS, ids).then(res, res) + }) +} + +if (typeof window !== 'undefined') { + window.__NEXT_PRELOADREADY = Loadable.preloadReady +} + +export default Loadable diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js new file mode 100644 index 000000000000..8d2ae10e0250 --- /dev/null +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dist/client/components/shared/dynamic' + +const Dynamic = dynamic(() => import('../dynamic.client')) + +export function NextDynamicClientComponent() { + return +} diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js new file mode 100644 index 000000000000..5dd0d7e671b4 --- /dev/null +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js @@ -0,0 +1,7 @@ +import dynamic from 'next/dist/client/components/shared/dynamic' + +const Dynamic = dynamic(() => import('../dynamic.server')) + +export function NextDynamicServerComponent() { + return +} diff --git a/test/e2e/app-dir/app/app/dashboard/index/react-lazy.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js similarity index 80% rename from test/e2e/app-dir/app/app/dashboard/index/react-lazy.client.js rename to test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js index 7188966f4c8e..eb8074523d9d 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/react-lazy.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js @@ -1,6 +1,6 @@ import { useState, lazy } from 'react' -const Lazy = lazy(() => import('./lazy.client.js')) +const Lazy = lazy(() => import('../lazy.client.js')) export function LazyClientComponent() { let [state] = useState('client') diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js index a754ecb2082a..7b8c0ec09b04 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js @@ -2,9 +2,9 @@ import { useState } from 'react' import styles from './dynamic.module.css' export default function Dynamic() { - let [state] = useState('next dynamic') + let [state] = useState('dynamic on client') return ( -

+

hello from modern the {state}

) diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js new file mode 100644 index 000000000000..6754c00c3239 --- /dev/null +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js @@ -0,0 +1,9 @@ +import styles from './dynamic.module.css' + +export default function Dynamic() { + return ( +

+ hello from modern the dynamic on server +

+ ) +} diff --git a/test/e2e/app-dir/app/app/dashboard/index/next-dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/next-dynamic.client.js deleted file mode 100644 index 6728a87f463c..000000000000 --- a/test/e2e/app-dir/app/app/dashboard/index/next-dynamic.client.js +++ /dev/null @@ -1,7 +0,0 @@ -import dynamic from 'next/dynamic' - -const Dynamic = dynamic(() => import('./dynamic.client')) - -export function NextDynamicClientComponent() { - return -} diff --git a/test/e2e/app-dir/app/app/dashboard/index/page.server.js b/test/e2e/app-dir/app/app/dashboard/index/page.server.js index d12da1104893..6dca33b19e80 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/page.server.js +++ b/test/e2e/app-dir/app/app/dashboard/index/page.server.js @@ -1,10 +1,12 @@ -import { LazyClientComponent } from './react-lazy.client' -import { NextDynamicClientComponent } from './next-dynamic.client' +import { LazyClientComponent } from './dynamic-imports/react-lazy.client' +import { NextDynamicServerComponent } from './dynamic-imports/dynamic.server' +import { NextDynamicClientComponent } from './dynamic-imports/dynamic.client' export default function DashboardIndexPage() { return ( <>

hello from app/dashboard/index

+ diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 3541809f8150..e637803e3435 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -67,8 +67,11 @@ describe('app dir', () => { it('should serve /index as separate page', async () => { const html = await renderViaHTTP(next.url, '/dashboard/index') expect(html).toContain('hello from app/dashboard/index') - // should load chunks generated via async import correctly + // should load chunks generated via async import correctly with React.lazy expect(html).toContain('hello from lazy') + // should support `dynamic` in both server and client components + expect(html).toContain('hello from modern the dynamic on server') + expect(html).toContain('hello from modern the dynamic on client') }) // TODO-APP: handle css modules fouc in dev @@ -77,7 +80,7 @@ describe('app dir', () => { expect( await browser.eval( - `window.getComputedStyle(document.querySelector('#css-text-dynamic')).color` + `window.getComputedStyle(document.querySelector('#css-text-dynamic-server')).color` ) ).toBe('rgb(0, 0, 255)') expect( From 0c6af8f250d0ff7881b29a84a1850f9daacb809e Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 29 Jul 2022 11:54:10 +0200 Subject: [PATCH 2/3] update test components --- .../app/app/dashboard/index/dynamic-imports/dynamic.client.js | 2 +- .../app/app/dashboard/index/dynamic-imports/dynamic.server.js | 2 +- .../app/dashboard/index/dynamic-imports/react-lazy.client.js | 4 ++-- .../index/{dynamic.client.js => text-dynamic.client.js} | 2 +- .../index/{dynamic.server.js => text-dynamic.server.js} | 2 +- .../dashboard/index/{lazy.client.js => text-lazy.client.js} | 0 test/e2e/app-dir/index.test.ts | 4 ++-- 7 files changed, 8 insertions(+), 8 deletions(-) rename test/e2e/app-dir/app/app/dashboard/index/{dynamic.client.js => text-dynamic.client.js} (87%) rename test/e2e/app-dir/app/app/dashboard/index/{dynamic.server.js => text-dynamic.server.js} (78%) rename test/e2e/app-dir/app/app/dashboard/index/{lazy.client.js => text-lazy.client.js} (100%) diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js index 8d2ae10e0250..8b487da2a4eb 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.client.js @@ -1,6 +1,6 @@ import dynamic from 'next/dist/client/components/shared/dynamic' -const Dynamic = dynamic(() => import('../dynamic.client')) +const Dynamic = dynamic(() => import('../text-dynamic.client')) export function NextDynamicClientComponent() { return diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js index 5dd0d7e671b4..18ac428cfc37 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/dynamic.server.js @@ -1,6 +1,6 @@ import dynamic from 'next/dist/client/components/shared/dynamic' -const Dynamic = dynamic(() => import('../dynamic.server')) +const Dynamic = dynamic(() => import('../text-dynamic.server')) export function NextDynamicServerComponent() { return diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js index eb8074523d9d..0b1870fdeaf9 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/dynamic-imports/react-lazy.client.js @@ -1,13 +1,13 @@ import { useState, lazy } from 'react' -const Lazy = lazy(() => import('../lazy.client.js')) +const Lazy = lazy(() => import('../text-lazy.client.js')) export function LazyClientComponent() { let [state] = useState('client') return ( <> -

hello from modern the {state}

+

hello from {state}

) } diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js similarity index 87% rename from test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js rename to test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js index 7b8c0ec09b04..660b8c5953c6 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic.client.js +++ b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.client.js @@ -5,7 +5,7 @@ export default function Dynamic() { let [state] = useState('dynamic on client') return (

- hello from modern the {state} + {`hello from ${state}`}

) } diff --git a/test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.server.js similarity index 78% rename from test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js rename to test/e2e/app-dir/app/app/dashboard/index/text-dynamic.server.js index 6754c00c3239..9f87f73b47df 100644 --- a/test/e2e/app-dir/app/app/dashboard/index/dynamic.server.js +++ b/test/e2e/app-dir/app/app/dashboard/index/text-dynamic.server.js @@ -3,7 +3,7 @@ import styles from './dynamic.module.css' export default function Dynamic() { return (

- hello from modern the dynamic on server + hello from dynamic on server

) } diff --git a/test/e2e/app-dir/app/app/dashboard/index/lazy.client.js b/test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js similarity index 100% rename from test/e2e/app-dir/app/app/dashboard/index/lazy.client.js rename to test/e2e/app-dir/app/app/dashboard/index/text-lazy.client.js diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index e637803e3435..9fe5df5af35a 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -70,8 +70,8 @@ describe('app dir', () => { // should load chunks generated via async import correctly with React.lazy expect(html).toContain('hello from lazy') // should support `dynamic` in both server and client components - expect(html).toContain('hello from modern the dynamic on server') - expect(html).toContain('hello from modern the dynamic on client') + expect(html).toContain('hello from dynamic on server') + expect(html).toContain('hello from dynamic on client') }) // TODO-APP: handle css modules fouc in dev From 79513de86254ffeeac68d4f1821c969240613226 Mon Sep 17 00:00:00 2001 From: Jiachi Liu Date: Fri, 29 Jul 2022 12:21:34 +0200 Subject: [PATCH 3/3] remove loadable --- .../components/shared/dynamic/index.tsx | 45 +-- .../components/shared/dynamic/loadable.d.ts | 15 - .../components/shared/dynamic/loadable.js | 259 ------------------ 3 files changed, 8 insertions(+), 311 deletions(-) delete mode 100644 packages/next/client/components/shared/dynamic/loadable.d.ts delete mode 100644 packages/next/client/components/shared/dynamic/loadable.js diff --git a/packages/next/client/components/shared/dynamic/index.tsx b/packages/next/client/components/shared/dynamic/index.tsx index 6af3e82771b3..f333b8fef6c2 100644 --- a/packages/next/client/components/shared/dynamic/index.tsx +++ b/packages/next/client/components/shared/dynamic/index.tsx @@ -1,50 +1,21 @@ import React from 'react' -import Loadable from './loadable' -export type LoaderComponent

= Promise< - React.ComponentType

| { default: React.ComponentType

} -> +export type LoaderComponent

= Promise<{ + default: React.ComponentType

+}> -export type Loader

= (() => LoaderComponent

) | LoaderComponent

+export type Loader

= () => LoaderComponent

-export type LoaderMap = { [mdule: string]: () => Loader } - -export type LoadableGeneratedOptions = { - webpack?(): any - modules?(): LoaderMap -} - -export type DynamicOptions

= LoadableGeneratedOptions & { +export type DynamicOptions

= { loader?: Loader

- loadableGenerated?: LoadableGeneratedOptions } -export type LoadableOptions

= DynamicOptions

- -export type LoadableFn

= ( - opts: LoadableOptions

-) => React.ComponentType

- export type LoadableComponent

= React.ComponentType

export default function dynamic

( - loader: Loader

, - options?: DynamicOptions

+ loader: Loader

): React.ComponentType

{ - let loadableFn: LoadableFn

= Loadable - let loadableOptions: LoadableOptions

= { - ...options, - loader, - } - - // coming from build/babel/plugins/react-loadable-plugin.js - if (loadableOptions.loadableGenerated) { - loadableOptions = { - ...loadableOptions, - ...loadableOptions.loadableGenerated, - } - delete loadableOptions.loadableGenerated - } + const LazyLoadable = React.lazy(loader) - return loadableFn(loadableOptions) + return LazyLoadable } diff --git a/packages/next/client/components/shared/dynamic/loadable.d.ts b/packages/next/client/components/shared/dynamic/loadable.d.ts deleted file mode 100644 index 505fa09be9c3..000000000000 --- a/packages/next/client/components/shared/dynamic/loadable.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* tslint:disable */ -import React from 'react' - -declare namespace LoadableExport { - interface ILoadable { -

(opts: any): React.ComponentClass

- Map

(opts: any): React.ComponentType

- preloadAll(): Promise - } -} - -// eslint-disable-next-line no-redeclare -declare const LoadableExport: LoadableExport.ILoadable - -export = LoadableExport diff --git a/packages/next/client/components/shared/dynamic/loadable.js b/packages/next/client/components/shared/dynamic/loadable.js deleted file mode 100644 index 6ee29a906e93..000000000000 --- a/packages/next/client/components/shared/dynamic/loadable.js +++ /dev/null @@ -1,259 +0,0 @@ -/** -@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 SOFTWARE -*/ -// https://github.com/jamiebuilds/react-loadable/blob/v5.5.0/src/index.js -// Modified to be compatible with webpack 4 / Next.js -// Modified to only work with React.lazy for app layout - -import React from 'react' -import { LoadableContext } from '../../../../shared/lib/loadable-context' - -const ALL_INITIALIZERS = [] -const READY_INITIALIZERS = [] -let initialized = false - -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 createLoadableComponent(loadFn, options) { - let opts = Object.assign( - { - loader: null, - loading: null, - delay: 200, - timeout: null, - webpack: null, - modules: null, - suspense: false, - }, - options - ) - - opts.lazy = React.lazy(opts.loader) - - /** @type LoadableSubscription */ - let subscription = null - function init() { - if (!subscription) { - const sub = new LoadableSubscription(loadFn, opts) - subscription = { - getCurrentValue: sub.getCurrentValue.bind(sub), - subscribe: sub.subscribe.bind(sub), - retry: sub.retry.bind(sub), - promise: sub.promise.bind(sub), - } - } - return subscription.promise() - } - - // Server only - if (typeof window === 'undefined') { - ALL_INITIALIZERS.push(init) - } - - // Client only - if (!initialized && typeof window !== 'undefined') { - // require.resolveWeak check is needed for environments that don't have it available like Jest - const moduleIds = - opts.webpack && typeof require.resolveWeak === 'function' - ? opts.webpack() - : opts.modules - if (moduleIds) { - READY_INITIALIZERS.push((ids) => { - for (const moduleId of moduleIds) { - if (ids.indexOf(moduleId) !== -1) { - return init() - } - } - }) - } - } - - function useLoadableModule() { - init() - - const context = React.useContext(LoadableContext) - if (context && Array.isArray(opts.modules)) { - opts.modules.forEach((moduleName) => { - context(moduleName) - }) - } - } - - function LoadableComponent(props, ref) { - useLoadableModule() - - return React.createElement(opts.lazy, { ...props, ref }) - } - - LoadableComponent.preload = () => init() - LoadableComponent.displayName = 'LoadableComponent' - - return React.forwardRef(LoadableComponent) -} - -class LoadableSubscription { - constructor(loadFn, opts) { - this._loadFn = loadFn - this._opts = opts - this._callbacks = new Set() - this._delay = null - this._timeout = null - - this.retry() - } - - promise() { - return this._res.promise - } - - retry() { - this._clearTimeouts() - this._res = this._loadFn(this._opts.loader) - - this._state = { - pastDelay: false, - timedOut: false, - } - - const { _res: res, _opts: opts } = this - - if (res.loading) { - if (typeof opts.delay === 'number') { - if (opts.delay === 0) { - this._state.pastDelay = true - } else { - this._delay = setTimeout(() => { - this._update({ - pastDelay: true, - }) - }, opts.delay) - } - } - - if (typeof opts.timeout === 'number') { - this._timeout = setTimeout(() => { - this._update({ timedOut: true }) - }, opts.timeout) - } - } - - this._res.promise - .then(() => { - this._update({}) - this._clearTimeouts() - }) - .catch((_err) => { - this._update({}) - this._clearTimeouts() - }) - this._update({}) - } - - _update(partial) { - this._state = { - ...this._state, - error: this._res.error, - loaded: this._res.loaded, - loading: this._res.loading, - ...partial, - } - this._callbacks.forEach((callback) => callback()) - } - - _clearTimeouts() { - clearTimeout(this._delay) - clearTimeout(this._timeout) - } - - getCurrentValue() { - return this._state - } - - subscribe(callback) { - this._callbacks.add(callback) - return () => { - this._callbacks.delete(callback) - } - } -} - -function Loadable(opts) { - return createLoadableComponent(load, opts) -} - -function flushInitializers(initializers, ids) { - let promises = [] - - while (initializers.length) { - let init = initializers.pop() - promises.push(init(ids)) - } - - return Promise.all(promises).then(() => { - if (initializers.length) { - return flushInitializers(initializers, ids) - } - }) -} - -Loadable.preloadAll = () => { - return new Promise((resolveInitializers, reject) => { - flushInitializers(ALL_INITIALIZERS).then(resolveInitializers, reject) - }) -} - -Loadable.preloadReady = (ids = []) => { - return new Promise((resolvePreload) => { - const res = () => { - initialized = true - return resolvePreload() - } - // We always will resolve, errors should be handled within loading UIs. - flushInitializers(READY_INITIALIZERS, ids).then(res, res) - }) -} - -if (typeof window !== 'undefined') { - window.__NEXT_PRELOADREADY = Loadable.preloadReady -} - -export default Loadable