diff --git a/.npmignore b/.npmignore index 4e36cad..05225b7 100644 --- a/.npmignore +++ b/.npmignore @@ -10,7 +10,7 @@ docs src ROADMAP.MD CONTRIBUTING.MD -CHANGELOG.MD +CODE_OF_CONDUCT.md node_modules # from docusaurus diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 5d54e8e..eea3047 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,5 +1,29 @@ # Changelog +# 2.0.0 + +## Breaking changes + +This release contains no breaking changes, but it does include some important changes, including: + +- proposed to use `loader` instead of `require` (however you can still use `require` - this version has backward compatibility); +- `investigate` returns only initially loaded modules; +- screen gets mounted in `async` way; + +## Added + +- support for `web` platform (with `react-native-web` usage); +- support for `macOS` platform; +- support for `windows` platform; + +## Improved + +- types compatibility; +- internal naming convention; + +## Fixed + - `requires a peer of react-native@^0.59.1 but none is installed. You must install peer dependencies yourself.` warning; + # 1.0.9 ## Fixed diff --git a/README.MD b/README.MD index ec117a6..51e4f4e 100644 --- a/README.MD +++ b/README.MD @@ -13,4 +13,5 @@ Decrease your start up time and RAM memory consumption by an application via spl ## Quick Links - [Documentation](https://kirillzyusko.github.io/react-native-bundle-splitter/) +- [Example](https://github.com/kirillzyusko/react-native-bundle-splitter-example) - [Contributing](https://github.com/kirillzyusko/react-native-bundle-splitter/blob/master/CONTRIBUTING.MD) diff --git a/ROADMAP.MD b/ROADMAP.MD index fe6baf2..1f1c280 100644 --- a/ROADMAP.MD +++ b/ROADMAP.MD @@ -1,5 +1,6 @@ # Features +- [x] async import instead of require +- [x] more platform support; - [ ] timeToInteraction feature -- [ ] async import instead of require - [ ] add `preload` on rendered component (`Loadable.preload()` <=> ``) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c25842a..95049e9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-native-bundle-splitter", - "version": "1.0.4", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -20,6 +20,16 @@ "csstype": "^2.2.0" } }, + "@types/react-native": { + "version": "0.60.0", + "resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.60.0.tgz", + "integrity": "sha512-9av+Wgh3j7nQzK6MXIGMqc57M53Ilfcyhq49SRzO/Jv9e7PdQNjJrCiXoHSvtKwuQpwxMkomAfnbcxxkp0zzBw==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, "csstype": { "version": "2.6.6", "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.6.tgz", diff --git a/package.json b/package.json index 9e88ce7..0326a66 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-bundle-splitter", "author": "Kiryl Ziusko", - "version": "1.0.9", + "version": "2.0.0", "description": "Decrease your start up time and RAM memory consumption by an application via splitting JS bundle by components and navigation routes", "repository": "https://github.com/kirillzyusko/react-native-bundle-splitter", "homepage": "https://kirillzyusko.github.io/react-native-bundle-splitter/", @@ -13,16 +13,18 @@ }, "devDependencies": { "@types/react": "^16.6.1", + "@types/react-native": "^0.60.0", "typescript": "3.6.3" }, "peerDependencies": { - "react": "^16.6.1", - "react-native": "^0.59.1" + "react": "*", + "react-native": ">=0.59.1" }, "keywords": [ "react", "native", "react-native", + "react-native-web", "bundle", "bundles", "separate", diff --git a/src/index.ts b/src/index.ts index e16d921..95ee2f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,18 +28,7 @@ const register = (component: PreLoadable & Partial) => { return optimized(name); }; -const component = (name: string) => new Promise((resolve: Function, reject: Function) => { - try { - if (isCached(name)) { - resolve(); - } else { - getComponent(name); - resolve(); - } - } catch (e) { - reject(e); - } -}); +const component = (name: string) => getComponent(name); const group = (name: string) => { const components = Object.keys(mapLoadable).filter((componentName) => mapLoadable[componentName].group === name); diff --git a/src/interface.ts b/src/interface.ts index 743d658..9ea0dde 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,12 +1,34 @@ import * as React from 'react'; -export type PreLoadable = { +type ImportReturnType = {}; + +export type RequireLoader = () => NodeRequire; + +export type ImportLoader = () => Promise; + +type BasePreLoadable = { + require?: RequireLoader; + loader?: ImportLoader; name?: string; - require: () => ({}); group?: string; static?: object; }; +// helper, which transforms optional params to mandatory +// useful for creating conditional types - see usage below +// https://stackoverflow.com/a/49725198/9272042 +type RequireOnlyOne = + Pick> + & { + [K in Keys]-?: + Required> + & Partial, undefined>> + }[Keys]; + +// it describes the type, where `loader` or `require` should be defined, but not +// two of them at the same time +export type PreLoadable = RequireOnlyOne; + export type EnhancedPreLoadable = { cached: boolean; placeholder: React.ElementType | null, diff --git a/src/map.ts b/src/map.ts index a7fa852..1592b4f 100644 --- a/src/map.ts +++ b/src/map.ts @@ -1,20 +1,52 @@ import { mapLoadable } from './bundler'; +import { RequireLoader, ImportLoader } from './interface'; const cache = {} as any; export const isCached = (componentName: string) => !!cache[componentName]; -export const getComponent = (name: string) => { +const DEPRECATED_API_MESSAGE = "You are using a deprecated API that will be removed in a future releases. Please consider using `loader` instead of `require`"; +const ERROR_WHILE_LOADING = "An error occurred while lazy loading a component. Perhaps the path where you are trying to load the component does not exist? Stacktrace: "; + +// In react-native world call of `require` or `loader` will block thread (since it's sync operation) +// As a result if screen is not loaded yet and you trigger a navigation - app will freeze for a time, +// until screen is not loaded. That's why `setTimeout(..., 0)` code is used here. We simply call this +// function in next event loop iteration. Such approach will not block transition animations. +const nonBlockingLoader = (loader: RequireLoader | ImportLoader) => new Promise((resolve) => { + setTimeout(async () => { + try { + const file = await loader(); + resolve(file); + } catch (e) { + console.error(ERROR_WHILE_LOADING + e); + // resolve it as a `null` - another error will be thrown + // when it will evaluate `component[rest.extract]` + resolve(null); + } + }, 0); +}); + +export const getComponent = async (name: string) => { if (!isCached(name)) { - const { require: load, ...rest } = mapLoadable[name]; + const { require: load, loader, ...rest } = mapLoadable[name]; + let component = null; + + if (loader) { + component = await nonBlockingLoader(loader); + } else if (load) { + console.warn(DEPRECATED_API_MESSAGE); + + component = await nonBlockingLoader(load); + } - // @ts-ignore - const component = load()[rest.extract]; cache[name] = { ...rest, - component, + // @ts-ignore + component: component[rest.extract], }; } return cache[name]; -}; \ No newline at end of file +}; + +export const getComponentFromCache = (name: string) => cache[name]; \ No newline at end of file diff --git a/src/optimized.tsx b/src/optimized.tsx index 7324fec..cf8ea28 100644 --- a/src/optimized.tsx +++ b/src/optimized.tsx @@ -1,11 +1,11 @@ import * as React from 'react'; -import { getComponent, isCached } from './map'; +import { getComponent, isCached, getComponentFromCache } from './map'; import { mapLoadable } from './bundler'; type Props = {}; type State = { - needsExpensive: boolean; + isComponentAvailable: boolean; }; const optimized = (screenName: string): any => { @@ -18,21 +18,21 @@ const optimized = (screenName: string): any => { const cached = isCached(screenName); if (cached) { - const { component } = getComponent(screenName); + const { component } = getComponentFromCache(screenName); this.component = component; } this.state = { - needsExpensive: cached + isComponentAvailable: cached }; } - public componentDidMount(): void { + public async componentDidMount(): Promise { if (this.component === null) { - const { component } = getComponent(screenName); + const { component } = await getComponent(screenName); this.component = component; - this.setState({ needsExpensive: true }); + this.setState({ isComponentAvailable: true }); } } @@ -41,7 +41,7 @@ const optimized = (screenName: string): any => { const Placeholder = this.placeholder; const PlaceholderComponent = Placeholder ? : Placeholder; - return this.state.needsExpensive && BundleComponent ? + return this.state.isComponentAvailable && BundleComponent ? : PlaceholderComponent; } } diff --git a/src/utils.ts b/src/utils.ts index 643af53..7cfa247 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,7 +1,14 @@ +import { Platform } from 'react-native'; // quick and dirty declare var require: any; export const investigate = () => { + if (Platform.OS === 'web' || !__DEV__) { + // prevent crash in release and web + // this function will not work on web and in release + return { loaded: [], waiting: [] } + } + const modules = require.getModules(); const moduleIds = Object.keys(modules); const loaded = moduleIds diff --git a/tsconfig.json b/tsconfig.json index de4e8e9..540dbb4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,7 @@ "lib": ["es2015"], /* Specify library files to be included in the compilation. */ // "allowJs": true, /* Allow javascript files to be compiled. */ // "checkJs": true, /* Report errors in .js files. */ - "jsx": "react-native", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": true, /* Generates corresponding '.d.ts' file. */ // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ // "sourceMap": true, /* Generates corresponding '.map' file. */