diff --git a/src/index.ts b/src/index.ts index fb786434..7c115a92 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import imported, { lazy } from './HOC'; import LazyBoundary from './LazyBoundary'; import { assignImportedComponents, + clearImportedCache, done as whenComponentsReady, dryRender, getLoadable as loadableResource, @@ -43,5 +44,6 @@ export { useImported, useLazy, addPreloader, + clearImportedCache, }; export default imported; diff --git a/src/loadable.ts b/src/loadable.ts index 4896c064..02a21a52 100644 --- a/src/loadable.ts +++ b/src/loadable.ts @@ -1,14 +1,15 @@ -import {assingLoadableMark} from './marks'; -import {isBackend} from './detectBackend'; -import {DefaultImport, Loadable, Mark, MarkMeta, Promised} from './types'; -import {settings} from "./config"; -import {getPreloaders} from "./preloaders"; +import { settings } from './config'; +import { isBackend } from './detectBackend'; +import { assingLoadableMark } from './marks'; +import { getPreloaders } from './preloaders'; +import { DefaultImport, Loadable, Mark, MarkMeta, Promised } from './types'; type AnyFunction = (x: any) => any; -interface InnerLoadable extends Loadable { +export interface InnerLoadable extends Loadable { ok: boolean; promise: Promise | undefined; + _probeChanges(): Promise; } let pending: Array> = []; @@ -17,30 +18,22 @@ const LOADABLE_WEAK_SIGNATURE = new WeakMap>(); const LOADABLE_SIGNATURE = new Map>(); const addPending = (promise: Promise) => pending.push(promise); -const removeFromPending = (promise: Promise) => pending = pending.filter(a => a !== promise); +const removeFromPending = (promise: Promise) => (pending = pending.filter(a => a !== promise)); const trimImport = (str: string) => str.replace(/['"]/g, ''); export const importMatch = (functionString: string): Mark => { const markMatches = functionString.match(/`imported_(.*?)_component`/g) || []; - return markMatches - .map(match => match && trimImport((match.match(/`imported_(.*?)_component`/i) || [])[1])); + return markMatches.map(match => match && trimImport((match.match(/`imported_(.*?)_component`/i) || [])[1])); }; -export const getFunctionSignature = (fn: AnyFunction | string) => ( +export const getFunctionSignature = (fn: AnyFunction | string) => String(fn) .replace(/(["'])/g, '`') - .replace(/\/\*([^\*]*)\*\//ig, '') -); - + .replace(/\/\*([^\*]*)\*\//gi, ''); export function toLoadable(firstImportFunction: Promised, autoImport = true): Loadable { let importFunction = firstImportFunction; - const _load = (): Promise => ( - Promise.all([ - importFunction(), - ...getPreloaders(), - ]).then(([result]) => result) - ); + const callLoad = (): Promise => Promise.all([importFunction(), ...getPreloaders()]).then(([result]) => result); const functionSignature = getFunctionSignature(importFunction); const mark = importMatch(functionSignature); @@ -102,8 +95,8 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru // synchronous thenable - https://github.com/facebook/react/pull/14626 cb(result); return Promise.resolve(result); - } - } as any + }, + } as any; } return this.loadIfNeeded().then(then); @@ -118,10 +111,18 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru return Promise.resolve(); }, + _probeChanges() { + return callLoad() + .then(payload => payload !== this.payload) + .catch(err => { + throw err; + }); + }, + load() { if (!this.promise) { - const promise = this.promise = _load() - .then((payload) => { + const promise = (this.promise = callLoad().then( + payload => { this.done = true; this.ok = true; this.payload = payload; @@ -129,23 +130,26 @@ export function toLoadable(firstImportFunction: Promised, autoImport = tru removeFromPending(promise); resolveResolution(payload); return payload; - }, (err) => { + }, + err => { this.done = true; this.ok = false; this.error = err; removeFromPending(promise); throw err; - }); + } + )); addPending(promise); } return this.promise; - } + }, }; if (mark && mark.length) { LOADABLE_SIGNATURE.set(functionSignature, loadable); assingLoadableMark(mark, loadable); } else { + // tslint:disable-next-line:no-console console.warn( 'react-imported-component: no mark found at', importFunction, @@ -167,8 +171,7 @@ export const isItReady = () => readyFlag; export const done = (): Promise => { if (pending.length) { readyFlag = false; - return Promise - .all(pending) + return Promise.all(pending) .then(a => a[1]) .then(done); } @@ -181,20 +184,18 @@ export const done = (): Promise => { export const dryRender = (renderFunction: () => void) => { renderFunction(); - return Promise - .resolve() - .then(done); + return Promise.resolve().then(done); }; export const markMeta: MarkMeta[] = []; const assignMetaData = (mark: Mark, loadable: Loadable, chunkName: string, fileName: string) => { - markMeta.push({mark, loadable, chunkName, fileName}); + markMeta.push({ mark, loadable, chunkName, fileName }); }; type ImportedDefinition = [Promised, string, string, boolean]; -export const assignImportedComponents = (set: Array) => { +export const assignImportedComponents = (set: ImportedDefinition[]) => { const countBefore = LOADABLE_SIGNATURE.size; set.forEach(imported => { const allowAutoLoad = !(imported[3] || !settings.fileFilter(imported[2])); @@ -203,7 +204,8 @@ export const assignImportedComponents = (set: Array) => { }); if (countBefore === LOADABLE_SIGNATURE.size) { - console.error('react-imported-component: no import-marks found, please check babel plugin') + // tslint:disable-next-line:no-console + console.error('react-imported-component: no import-marks found, please check babel plugin'); } done(); @@ -232,6 +234,7 @@ export function getLoadable(importFunction: DefaultImport | Loadable): const functionSignature = getFunctionSignature(importFunction); if (LOADABLE_SIGNATURE.has(functionSignature)) { + // tslint:disable-next-line:no-shadowed-variable const loadable = LOADABLE_SIGNATURE.get(functionSignature)!; loadable.replaceImportFunction(importFunction); diff --git a/src/useImported.ts b/src/useImported.ts index e05b41fa..3b553bdb 100644 --- a/src/useImported.ts +++ b/src/useImported.ts @@ -1,8 +1,8 @@ -import { ComponentType, lazy, LazyExoticComponent, useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ComponentType, lazy, LazyExoticComponent, useCallback, useContext, useEffect, useState } from 'react'; import { settings } from './config'; import { streamContext } from './context'; import { isBackend } from './detectBackend'; -import { getLoadable, isItReady } from './loadable'; +import { getLoadable, InnerLoadable, isItReady } from './loadable'; import { useMark } from './marks'; import { DefaultComponentImport, DefaultImport, DefaultImportedComponent, Loadable } from './types'; import { es6import } from './utils'; @@ -13,20 +13,6 @@ function loadLoadable(loadable: Loadable, callback: (l: any) => void) { loadable.loadIfNeeded().then(upd, upd); } -const WEAK_MAP = new WeakMap, any>(); - -function executeLoadableEventually(loadable: Loadable, cb: any) { - const tracker = {}; - WEAK_MAP.set(loadable, tracker); - - // execute loadable after some timeout to let HMR propagate thought - setTimeout(() => { - if (WEAK_MAP.get(loadable) === tracker) { - loadable.reload().then(cb); - } - }, 16); -} - interface ImportedShape { imported?: T; error?: any; @@ -107,17 +93,17 @@ export function useImported( options: HookOptions = {} ): ImportedShape { const [topLoadable] = useState(getLoadable(importer)); - const postEffectRef = useRef(false); const { loadable, retry, update } = useLoadable(topLoadable, options); - // kick loading effect - useEffect(() => { - if (postEffectRef.current) { - executeLoadableEventually(loadable, () => settings.updateOnReload && update({})); - } - - postEffectRef.current = true; - }, ['hot']); + // support HMR + if (process.env.NODE_ENV === 'development') { + // kick loading effect + useEffect(() => { + if (settings.updateOnReload) { + (topLoadable as InnerLoadable)._probeChanges().then(changed => changed && update({})); + } + }, []); + } if (loadable.error) { return {