├── .babelrc ├── .eslintrc ├── .eslintrc.js ├── .gitignore ├── .npmignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .size-limit.js ├── .size.json ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── __tests__ ├── __fixtures__ │ └── babel │ │ ├── boot │ │ ├── actual.js │ │ └── expected.js │ │ ├── node │ │ ├── actual.js │ │ └── expected.js │ │ └── webpack │ │ ├── actual.js │ │ └── expected.js ├── __snapshots__ │ └── macro.spec.ts.snap ├── babel.spec.ts ├── componentLoader.spec.tsx ├── loadable.spec.ts ├── macro.spec.ts ├── module.spec.tsx ├── rehydrate.spec.tsx ├── signatures.spec.ts └── useImported.spec.tsx ├── assets └── imported-logo.png ├── babel.js ├── bin └── imported-components ├── boot ├── boot.d.ts └── package.json ├── doczrc.js ├── example ├── app.tsx ├── assets │ └── .gitkeep ├── index.html └── index.tsx ├── examples ├── CRA │ ├── .env │ ├── .gitignore │ ├── async-imports.js │ ├── package.json │ ├── public │ │ └── index.html │ └── src │ │ ├── async-imports.js │ │ ├── async.js │ │ ├── index.js │ │ ├── load.js │ │ └── styles.css ├── SSR │ ├── parcel-react-ssr │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── LICENSE │ │ ├── README.md │ │ ├── app │ │ │ ├── App.jsx │ │ │ ├── HelloWorld.jsx │ │ │ ├── HelloWorld.scss │ │ │ ├── HelloWorld2.jsx │ │ │ ├── HelloWorld3.jsx │ │ │ ├── base.css │ │ │ ├── client.js │ │ │ ├── codeSplitAssets │ │ │ │ ├── NyanCat.css │ │ │ │ ├── NyanCat.js │ │ │ │ └── utils.js │ │ │ ├── imported-chunk.js │ │ │ ├── index.html │ │ │ └── main.js │ │ ├── package.json │ │ ├── server │ │ │ ├── generateHtml.js │ │ │ ├── index.js │ │ │ └── middleware.js │ │ ├── stream-server │ │ │ ├── index.js │ │ │ ├── interleaved-middleware.js │ │ │ └── middleware.js │ │ └── yarn.lock │ ├── runtime-manifest │ │ ├── .babelrc │ │ ├── .circleci │ │ │ └── config.yml │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ │ ├── App.css │ │ │ ├── App.tsx │ │ │ ├── assets │ │ │ │ └── favicon.ico │ │ │ ├── client.tsx │ │ │ ├── components │ │ │ │ ├── Another.tsx │ │ │ │ ├── Home.tsx │ │ │ │ ├── LazyTarget.tsx │ │ │ │ ├── NotDefault.tsx │ │ │ │ ├── Other.tsx │ │ │ │ ├── OtherTween.tsx │ │ │ │ └── library.tsx │ │ │ ├── imported.ts │ │ │ ├── index.html │ │ │ └── store │ │ │ │ ├── index.ts │ │ │ │ └── reducers.ts │ │ ├── package.json │ │ ├── server │ │ │ ├── generateHtml.ts │ │ │ ├── index.ts │ │ │ └── middleware.tsx │ │ ├── tsconfig.json │ │ ├── tslint.json │ │ ├── types.d.ts │ │ ├── webpack-client.config.js │ │ ├── webpack-server.config.js │ │ ├── webpack-shared.config.js │ │ ├── webpack.config.js │ │ └── yarn.lock │ └── typescript-react │ │ ├── .babelrc │ │ ├── .circleci │ │ └── config.yml │ │ ├── .gitignore │ │ ├── README.md │ │ ├── app │ │ ├── App.css │ │ ├── App.tsx │ │ ├── assets │ │ │ └── favicon.ico │ │ ├── client.tsx │ │ ├── components │ │ │ ├── Another.tsx │ │ │ ├── Home.tsx │ │ │ ├── LazyTarget.tsx │ │ │ ├── NotDefault.tsx │ │ │ ├── Other.tsx │ │ │ ├── OtherTween.tsx │ │ │ └── library.tsx │ │ ├── imported.ts │ │ ├── index.html │ │ └── store │ │ │ ├── index.ts │ │ │ └── reducers.ts │ │ ├── package.json │ │ ├── server │ │ ├── generateHtml.ts │ │ ├── index.ts │ │ └── middleware.tsx │ │ ├── tsconfig.json │ │ ├── tslint.json │ │ ├── types.d.ts │ │ ├── webpack-client.config.js │ │ ├── webpack-server.config.js │ │ ├── webpack-shared.config.js │ │ ├── webpack.config.js │ │ └── yarn.lock ├── hybrid │ └── react-snap │ │ ├── .babelrc │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── src │ │ ├── app.js │ │ ├── imported-chunk.js │ │ ├── index.html │ │ ├── main.js │ │ ├── splitted-1.js │ │ ├── splitted-2.js │ │ └── splitted-3.js │ │ └── yarn.lock └── react-hot-loader │ ├── .babelrc │ ├── .flowconfig │ ├── .gitignore │ ├── package.json │ ├── src │ ├── App.js │ ├── Counter.js │ ├── DeferredRender.js │ ├── HiddenComponent.js │ ├── Lazy.js │ ├── NotImported.js │ ├── Portal.js │ ├── SubAsync.js │ ├── index.js │ ├── indirect.js │ ├── indirectTarget.js │ └── indirectUsage.js │ ├── webpack.config.babel.js │ └── yarn.lock ├── greenkeeper.json ├── jest.config.js ├── macro └── package.json ├── package.json ├── server └── package.json ├── src ├── babel │ ├── babel.ts │ ├── magic-comments.ts │ └── utils.ts ├── configuration │ ├── config.ts │ ├── configuration.ts │ └── constants.ts ├── entrypoints │ ├── babel.ts │ ├── boot.ts │ ├── index.ts │ ├── macro.ts │ └── server.ts ├── loadable │ ├── assignImportedComponents.ts │ ├── loadByChunkName.ts │ ├── loadable.ts │ ├── markerMapper.ts │ ├── marks.ts │ ├── metadata.ts │ ├── pending.ts │ ├── preloaders.ts │ ├── registry.ts │ ├── stream.ts │ ├── toLoadable.ts │ └── utils.ts ├── scanners │ ├── __tests__ │ │ └── parser.spec.ts │ ├── cli.ts │ ├── scanForImports.ts │ └── shared.ts ├── trackers │ └── globalTracker.ts ├── transformers │ └── loadableTransformer.ts ├── types.ts ├── ui │ ├── Component.tsx │ ├── HOC.tsx │ ├── ImportedController.tsx │ ├── LazyBoundary.tsx │ ├── Module.tsx │ ├── context.tsx │ └── useImported.ts └── utils │ ├── detectBackend.ts │ ├── helpers.ts │ ├── signatures.ts │ ├── useClientPhase.tsx │ └── utils.ts ├── tsconfig.json ├── tslint.json ├── wrapper.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "cjs": { 4 | "presets": [ 5 | "@babel/preset-env", 6 | "@babel/preset-react" 7 | ], 8 | "plugins": [ 9 | "dynamic-import-node", 10 | "@babel/plugin-proposal-class-properties", 11 | "@babel/plugin-transform-runtime", 12 | [ 13 | "transform-react-remove-prop-types", 14 | { 15 | "mode": "wrap" 16 | } 17 | ] 18 | ] 19 | }, 20 | "es2015": { 21 | "presets": [ 22 | [ 23 | "@babel/preset-env", 24 | { 25 | "modules": false, 26 | "loose": true 27 | } 28 | ], 29 | "@babel/preset-react" 30 | ], 31 | "plugins": [ 32 | "dynamic-import-node", 33 | "@babel/plugin-proposal-class-properties", 34 | "@babel/plugin-transform-runtime", 35 | [ 36 | "transform-react-remove-prop-types", 37 | { 38 | "mode": "wrap" 39 | } 40 | ] 41 | ] 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:react/recommended" 6 | ], 7 | "plugins": [ 8 | "react" 9 | ], 10 | "env": { 11 | "browser": true, 12 | "node": true, 13 | "es6": true 14 | } 15 | } -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/typescript', 'plugin:react-hooks/recommended'], 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint', 'prettier', 'import'], 5 | rules: { 6 | '@typescript-eslint/ban-ts-comment': 0, 7 | '@typescript-eslint/ban-ts-ignore': 0, 8 | '@typescript-eslint/no-var-requires': 0, 9 | '@typescript-eslint/camelcase': 0, 10 | 'import/order': [ 11 | 'error', 12 | { 13 | 'newlines-between': 'always-and-inside-groups', 14 | alphabetize: { 15 | order: 'asc', 16 | }, 17 | groups: ['builtin', 'external', 'internal', ['parent', 'index', 'sibling']], 18 | }, 19 | ], 20 | 'padding-line-between-statements': [ 21 | 'error', 22 | // IMPORT 23 | { 24 | blankLine: 'always', 25 | prev: 'import', 26 | next: '*', 27 | }, 28 | { 29 | blankLine: 'any', 30 | prev: 'import', 31 | next: 'import', 32 | }, 33 | // EXPORT 34 | { 35 | blankLine: 'always', 36 | prev: '*', 37 | next: 'export', 38 | }, 39 | { 40 | blankLine: 'any', 41 | prev: 'export', 42 | next: 'export', 43 | }, 44 | { 45 | blankLine: 'always', 46 | prev: '*', 47 | next: ['const', 'let'], 48 | }, 49 | { 50 | blankLine: 'any', 51 | prev: ['const', 'let'], 52 | next: ['const', 'let'], 53 | }, 54 | // BLOCKS 55 | { 56 | blankLine: 'always', 57 | prev: ['block', 'block-like', 'class', 'function', 'multiline-expression'], 58 | next: '*', 59 | }, 60 | { 61 | blankLine: 'always', 62 | prev: '*', 63 | next: ['block', 'block-like', 'class', 'function', 'return', 'multiline-expression'], 64 | }, 65 | ], 66 | }, 67 | settings: { 68 | 'import/parsers': { 69 | '@typescript-eslint/parser': ['.ts', '.tsx'], 70 | }, 71 | 'import/resolver': { 72 | typescript: { 73 | alwaysTryTypes: true, 74 | }, 75 | }, 76 | }, 77 | }; 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /lib/ 3 | /dist/ 4 | .DS_Store 5 | .nyc_output 6 | yarn-error.log 7 | *.tgz -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | src 3 | _tests -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | __tests__/__fixtures__ -------------------------------------------------------------------------------- /.size-limit.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | path: ['dist/es2015/entrypoints/index.js', 'dist/es2015/entrypoints/boot.js'], 4 | ignore: ['tslib'], 5 | limit: '4.3 KB', 6 | }, 7 | { 8 | path: 'dist/es2015/entrypoints/index.js', 9 | ignore: ['tslib'], 10 | limit: '3.9 KB', 11 | }, 12 | { 13 | path: 'dist/es2015/entrypoints/boot.js', 14 | ignore: ['tslib'], 15 | limit: '1.99 KB', 16 | }, 17 | ]; 18 | -------------------------------------------------------------------------------- /.size.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "dist/es2015/entrypoints/index.js, dist/es2015/entrypoints/boot.js", 4 | "passed": true, 5 | "size": 4212 6 | }, 7 | { 8 | "name": "dist/es2015/entrypoints/index.js", 9 | "passed": true, 10 | "size": 3825 11 | }, 12 | { 13 | "name": "dist/es2015/entrypoints/boot.js", 14 | "passed": true, 15 | "size": 1969 16 | } 17 | ] 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '10' 4 | cache: yarn 5 | script: 6 | - yarn 7 | - yarn test:ci 8 | notifications: 9 | email: false -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Anton 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/boot/actual.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | // generated by react-imported-component, DO NOT EDIT 5 | import {assignImportedComponents} from 'react-imported-component/macro'; 6 | 7 | const PreloadComponent = imported(() => import('./PreloadThis')); 8 | const PrefetchChunkComponent = imported(() => import('./ChunkThis')); 9 | 10 | const applicationImports = assignImportedComponents([ 11 | [() => import('./should-not-be-transformed'), '', './some-file', false], 12 | ]); 13 | 14 | -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/boot/expected.js: -------------------------------------------------------------------------------- 1 | var importedWrapper = require('react-imported-component/wrapper'); 2 | 3 | import { assignImportedComponents } from "react-imported-component/boot"; 4 | 5 | /* eslint-disable */ 6 | 7 | /* tslint:disable */ 8 | // generated by react-imported-component, DO NOT EDIT 9 | const PreloadComponent = imported(() => importedWrapper("imported_-hbct7n_component", import('./PreloadThis'))); 10 | const PrefetchChunkComponent = imported(() => importedWrapper("imported_fspdct_component", import( 11 | /* webpackChunkName: "chunked-this" */ 12 | './ChunkThis'))); 13 | const applicationImports = assignImportedComponents([[() => importedWrapper("imported_12i8gg9_component", import('./should-not-be-transformed')), '', './some-file', false]]); -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/node/actual.js: -------------------------------------------------------------------------------- 1 | import imported from 'react-imported-component'; 2 | 3 | const AsyncComponent0 = imported(() => import(/* webpackChunkName:namedChunk */'./MyComponent')); 4 | 5 | const AsyncComponent1 = imported(() => import('./MyComponent')); 6 | 7 | const AsyncComponent2 = imported(async () => await import('./MyComponent')); 8 | 9 | const AsyncComponent3 = imported(() => Promise.all([import('./MyComponent'), import('./MyComponent')])); 10 | 11 | const AsyncComponent4 = imported(async () => (await Promise.all([import('./MyComponent1'), import('./MyComponent2')]))[0]); 12 | 13 | export default AsyncComponent1; -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/node/expected.js: -------------------------------------------------------------------------------- 1 | var importedWrapper = require('react-imported-component/wrapper'); 2 | 3 | function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } } 4 | 5 | import imported from 'react-imported-component'; 6 | const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); 7 | const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); 8 | const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))); 9 | const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent')))), importedWrapper("imported_18g2v0c_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent'))))])); 10 | const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent1')))), importedWrapper("imported_9j5sqq_component", Promise.resolve().then(() => _interopRequireWildcard(require('./MyComponent2'))))]))[0]); 11 | export default AsyncComponent1; -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/webpack/actual.js: -------------------------------------------------------------------------------- 1 | import {lazy, useImported} from 'react-imported-component/macro'; 2 | import imported from 'react-imported-component'; 3 | 4 | const PreloadComponent = imported(() => import('./PreloadThis')); 5 | const PrefetchChunkComponent = imported(() => import('./ChunkThis')); 6 | const AsyncComponent0 = imported(() => import(/* webpackChunkName: "namedChunk" */'./MyComponent')); 7 | 8 | const AsyncComponent1 = imported(() => import('./MyComponent')); 9 | 10 | const AsyncComponent2 = imported(async () => await import('./MyComponent')); 11 | 12 | const AsyncComponent3 = imported(() => Promise.all([import('./MyComponent'), import('./MyComponent')])); 13 | 14 | const AsyncComponent4 = imported(async () => (await Promise.all([import('./MyComponent1'), import('./MyComponent2')]))[0]); 15 | 16 | export default AsyncComponent1; -------------------------------------------------------------------------------- /__tests__/__fixtures__/babel/webpack/expected.js: -------------------------------------------------------------------------------- 1 | var importedWrapper = require('react-imported-component/wrapper'); 2 | 3 | import { lazy, useImported } from "react-imported-component"; 4 | import imported from 'react-imported-component'; 5 | const PreloadComponent = imported(() => importedWrapper("imported_-hbct7n_component", import( 6 | /* webpackPreload: true */ 7 | './PreloadThis'))); 8 | const PrefetchChunkComponent = imported(() => importedWrapper("imported_fspdct_component", import( 9 | /* webpackChunkName: "chunked-this" */ 10 | 11 | /* webpackPrefetch: true */ 12 | './ChunkThis'))); 13 | const AsyncComponent0 = imported(() => importedWrapper("imported_18g2v0c_component", import( 14 | /* webpackChunkName: "namedChunk" */ 15 | './MyComponent'))); 16 | const AsyncComponent1 = imported(() => importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); 17 | const AsyncComponent2 = imported(async () => await importedWrapper("imported_18g2v0c_component", import('./MyComponent'))); 18 | const AsyncComponent3 = imported(() => Promise.all([importedWrapper("imported_18g2v0c_component", import('./MyComponent')), importedWrapper("imported_18g2v0c_component", import('./MyComponent'))])); 19 | const AsyncComponent4 = imported(async () => (await Promise.all([importedWrapper("imported_-1qs8n90_component", import('./MyComponent1')), importedWrapper("imported_9j5sqq_component", import('./MyComponent2'))]))[0]); 20 | export default AsyncComponent1; -------------------------------------------------------------------------------- /__tests__/__snapshots__/macro.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`babel macro macros boot: boot 1`] = ` 4 | " 5 | import {assignImportedComponents, lazy} from \\"../macro\\"; 6 | assignImportedComponents([() => import('./a')]); 7 | lazy(() => import('./a')); 8 | 9 | ↓ ↓ ↓ ↓ ↓ ↓ 10 | 11 | function _interopRequireWildcard(obj) { 12 | if (obj && obj.__esModule) { 13 | return obj; 14 | } else { 15 | var newObj = {}; 16 | if (obj != null) { 17 | for (var key in obj) { 18 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 19 | var desc = 20 | Object.defineProperty && Object.getOwnPropertyDescriptor 21 | ? Object.getOwnPropertyDescriptor(obj, key) 22 | : {}; 23 | if (desc.get || desc.set) { 24 | Object.defineProperty(newObj, key, desc); 25 | } else { 26 | newObj[key] = obj[key]; 27 | } 28 | } 29 | } 30 | } 31 | newObj.default = obj; 32 | return newObj; 33 | } 34 | } 35 | 36 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 37 | 38 | import { lazy } from \\"react-imported-component\\"; 39 | import { assignImportedComponents } from \\"react-imported-component/boot\\"; 40 | assignImportedComponents([ 41 | () => 42 | importedWrapper( 43 | \\"imported_-mg71kn_component\\", 44 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a\\"))) 45 | ), 46 | ]); 47 | lazy(() => 48 | importedWrapper( 49 | \\"imported_-mg71kn_component\\", 50 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a\\"))) 51 | ) 52 | ); 53 | 54 | " 55 | `; 56 | 57 | exports[`babel macro macros flat import: flat import 1`] = ` 58 | " 59 | import \\"../macro\\"; 60 | import('./a.js') 61 | 62 | ↓ ↓ ↓ ↓ ↓ ↓ 63 | 64 | function _interopRequireWildcard(obj) { 65 | if (obj && obj.__esModule) { 66 | return obj; 67 | } else { 68 | var newObj = {}; 69 | if (obj != null) { 70 | for (var key in obj) { 71 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 72 | var desc = 73 | Object.defineProperty && Object.getOwnPropertyDescriptor 74 | ? Object.getOwnPropertyDescriptor(obj, key) 75 | : {}; 76 | if (desc.get || desc.set) { 77 | Object.defineProperty(newObj, key, desc); 78 | } else { 79 | newObj[key] = obj[key]; 80 | } 81 | } 82 | } 83 | } 84 | newObj.default = obj; 85 | return newObj; 86 | } 87 | } 88 | 89 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 90 | 91 | importedWrapper( 92 | \\"imported_-1ko6oiq_component\\", 93 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a.js\\"))) 94 | ); 95 | 96 | " 97 | `; 98 | 99 | exports[`babel macro macros lazy: lazy 1`] = ` 100 | " 101 | import {lazy} from \\"../macro\\"; 102 | const v = lazy(() => import('./a')); 103 | 104 | ↓ ↓ ↓ ↓ ↓ ↓ 105 | 106 | function _interopRequireWildcard(obj) { 107 | if (obj && obj.__esModule) { 108 | return obj; 109 | } else { 110 | var newObj = {}; 111 | if (obj != null) { 112 | for (var key in obj) { 113 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 114 | var desc = 115 | Object.defineProperty && Object.getOwnPropertyDescriptor 116 | ? Object.getOwnPropertyDescriptor(obj, key) 117 | : {}; 118 | if (desc.get || desc.set) { 119 | Object.defineProperty(newObj, key, desc); 120 | } else { 121 | newObj[key] = obj[key]; 122 | } 123 | } 124 | } 125 | } 126 | newObj.default = obj; 127 | return newObj; 128 | } 129 | } 130 | 131 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 132 | 133 | import { lazy } from \\"react-imported-component\\"; 134 | const v = lazy(() => 135 | importedWrapper( 136 | \\"imported_-mg71kn_component\\", 137 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a\\"))) 138 | ) 139 | ); 140 | 141 | " 142 | `; 143 | 144 | exports[`babel macro macros many: many 1`] = ` 145 | " 146 | import {imported, useImported} from \\"../macro\\"; 147 | const v = imported(() => import('./a')); 148 | const x = () => useImported(() => import('./b')); 149 | 150 | ↓ ↓ ↓ ↓ ↓ ↓ 151 | 152 | function _interopRequireWildcard(obj) { 153 | if (obj && obj.__esModule) { 154 | return obj; 155 | } else { 156 | var newObj = {}; 157 | if (obj != null) { 158 | for (var key in obj) { 159 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 160 | var desc = 161 | Object.defineProperty && Object.getOwnPropertyDescriptor 162 | ? Object.getOwnPropertyDescriptor(obj, key) 163 | : {}; 164 | if (desc.get || desc.set) { 165 | Object.defineProperty(newObj, key, desc); 166 | } else { 167 | newObj[key] = obj[key]; 168 | } 169 | } 170 | } 171 | } 172 | newObj.default = obj; 173 | return newObj; 174 | } 175 | } 176 | 177 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 178 | 179 | import { imported, useImported } from \\"react-imported-component\\"; 180 | const v = imported(() => 181 | importedWrapper( 182 | \\"imported_-mg71kn_component\\", 183 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a\\"))) 184 | ) 185 | ); 186 | 187 | const x = () => 188 | useImported(() => 189 | importedWrapper( 190 | \\"imported_15vaa6j_component\\", 191 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./b\\"))) 192 | ) 193 | ); 194 | 195 | " 196 | `; 197 | 198 | exports[`babel macro macros no usage: no usage 1`] = ` 199 | " 200 | import {lazy} from \\"../macro\\"; 201 | 202 | ↓ ↓ ↓ ↓ ↓ ↓ 203 | 204 | import { lazy } from \\"react-imported-component\\"; 205 | 206 | " 207 | `; 208 | 209 | exports[`babel macro macros nothing: nothing 1`] = ` 210 | " 211 | const a = 42; 212 | 213 | ↓ ↓ ↓ ↓ ↓ ↓ 214 | 215 | const a = 42; 216 | 217 | " 218 | `; 219 | 220 | exports[`babel macro macros plugin combination: plugin combination 1`] = ` 221 | " 222 | import {imported, useImported} from \\"../macro\\"; 223 | const v = imported(() => import('./a')); 224 | const x = () => useImported(() => import('./b')); 225 | 226 | ↓ ↓ ↓ ↓ ↓ ↓ 227 | 228 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 229 | 230 | function _interopRequireWildcard(obj) { 231 | if (obj && obj.__esModule) { 232 | return obj; 233 | } else { 234 | var newObj = {}; 235 | if (obj != null) { 236 | for (var key in obj) { 237 | if (Object.prototype.hasOwnProperty.call(obj, key)) { 238 | var desc = 239 | Object.defineProperty && Object.getOwnPropertyDescriptor 240 | ? Object.getOwnPropertyDescriptor(obj, key) 241 | : {}; 242 | if (desc.get || desc.set) { 243 | Object.defineProperty(newObj, key, desc); 244 | } else { 245 | newObj[key] = obj[key]; 246 | } 247 | } 248 | } 249 | } 250 | newObj.default = obj; 251 | return newObj; 252 | } 253 | } 254 | 255 | var importedWrapper = require(\\"react-imported-component/wrapper\\"); 256 | 257 | import { imported, useImported } from \\"react-imported-component\\"; 258 | const v = imported(() => 259 | importedWrapper( 260 | \\"imported_-mg71kn_component\\", 261 | importedWrapper( 262 | \\"imported_-mg71kn_component\\", 263 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./a\\"))) 264 | ) 265 | ) 266 | ); 267 | 268 | const x = () => 269 | useImported(() => 270 | importedWrapper( 271 | \\"imported_15vaa6j_component\\", 272 | importedWrapper( 273 | \\"imported_15vaa6j_component\\", 274 | Promise.resolve().then(() => _interopRequireWildcard(require(\\"./b\\"))) 275 | ) 276 | ) 277 | ); 278 | 279 | " 280 | `; 281 | -------------------------------------------------------------------------------- /__tests__/babel.spec.ts: -------------------------------------------------------------------------------- 1 | import { transform } from '@babel/core'; 2 | import { readFileSync } from 'fs'; 3 | import { join } from 'path'; 4 | import { ImportedConfiguration } from '../src/configuration/configuration'; 5 | 6 | const FIXTURE_PATH = join(__dirname, '__fixtures__/babel'); 7 | 8 | const configuration: ImportedConfiguration = { 9 | shouldPreload: filename => filename.indexOf('PreloadThis') >= 0, 10 | shouldPrefetch: filename => filename.indexOf('ChunkThis') >= 0, 11 | chunkName: filename => (filename.indexOf('ChunkThis') >= 0 ? 'chunked-this' : undefined), 12 | }; 13 | 14 | const testPlugin = { 15 | node: (code: string) => { 16 | const result = transform(code, { 17 | presets: ['@babel/preset-react'], 18 | plugins: [require.resolve('../dist/es5/entrypoints/babel'), 'dynamic-import-node'], 19 | }); 20 | 21 | return result!.code; 22 | }, 23 | webpack: (code: string) => { 24 | const result = transform(code, { 25 | presets: ['@babel/preset-react'], 26 | plugins: [[require.resolve('../dist/es5/entrypoints/babel'), configuration]], 27 | }); 28 | 29 | return result!.code; 30 | }, 31 | boot: (code: string) => { 32 | const result = transform(code, { 33 | presets: ['@babel/preset-react'], 34 | plugins: [[require.resolve('../dist/es5/entrypoints/babel'), configuration]], 35 | }); 36 | 37 | return result!.code; 38 | }, 39 | }; 40 | 41 | describe('babel', () => { 42 | Object.keys(testPlugin).forEach(folderName => { 43 | const actual = readFileSync(join(FIXTURE_PATH, folderName, 'actual.js'), 'utf8'); 44 | const expected = readFileSync(join(FIXTURE_PATH, folderName, 'expected.js'), 'utf8'); 45 | 46 | it(`works with ${folderName}`, () => { 47 | const result = (testPlugin as any)[folderName](actual); 48 | expect(result.trim()).toBe(expected.trim()); 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /__tests__/componentLoader.spec.tsx: -------------------------------------------------------------------------------- 1 | import '@theuiteam/lib-builder/configs/setupEnzyme'; 2 | import { mount } from 'enzyme'; 3 | import * as React from 'react'; 4 | 5 | import { toLoadable } from '../src/loadable/toLoadable'; 6 | import { ImportedComponent } from '../src/ui/Component'; 7 | import loader from '../src/ui/HOC'; 8 | 9 | describe('Async Component', () => { 10 | describe('loader', () => { 11 | it('should load component', done => { 12 | const TargetComponent = ({ payload }: any) =>
{payload}
; 13 | const Component = loader(() => Promise.resolve(TargetComponent)); 14 | 15 | const wrapper = mount(); 16 | 17 | expect(wrapper.find(TargetComponent)).toHaveLength(0); 18 | setImmediate(() => { 19 | wrapper.update(); 20 | expect(wrapper.find(TargetComponent).html()).toContain('42'); 21 | wrapper.unmount(); 22 | done(); 23 | }); 24 | }); 25 | 26 | it('forwardRef', done => { 27 | const TargetComponent = React.forwardRef(({ payload }, fref) =>
{payload}
); 28 | const Component = loader(() => Promise.resolve(TargetComponent)); 29 | const ref = React.createRef(); 30 | mount(); 31 | 32 | setImmediate(() => { 33 | expect(ref.current).not.toBe(null); 34 | done(); 35 | }); 36 | }); 37 | }); 38 | 39 | describe('SSR', () => { 40 | it('should precache Components', async () => { 41 | const TargetComponent = ({ payload }: any) =>
{payload}
; 42 | const LoadingComponent = () =>
loading
; 43 | const loadable = toLoadable(() => Promise.resolve(TargetComponent)); 44 | await loadable.load(); 45 | 46 | const wrapper = mount( 47 | 48 | ); 49 | expect(wrapper.find(LoadingComponent)).toHaveLength(0); 50 | expect(wrapper.find(TargetComponent).html()).toContain('42'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /__tests__/loadable.spec.ts: -------------------------------------------------------------------------------- 1 | import { getLoadable } from '../src/loadable/loadable'; 2 | 3 | describe('getLoadable', () => { 4 | const importedWrapper = (_: any, b: any) => b; 5 | 6 | it('cache test - should use mark', () => { 7 | const l1 = getLoadable(() => importedWrapper('imported_mark1_component', Promise.resolve(42))); 8 | const l2 = getLoadable(() => importedWrapper('imported_mark1_component', Promise.resolve(42))); 9 | 10 | expect(l1).toEqual(l2); 11 | }); 12 | 13 | it('cache test - no mark present', () => { 14 | const l1 = getLoadable(() => Promise.resolve(42)); 15 | const l2 = getLoadable(() => Promise.resolve(42)); 16 | 17 | expect(l1).not.toEqual(l2); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /__tests__/macro.spec.ts: -------------------------------------------------------------------------------- 1 | const pluginTester = require('babel-plugin-tester'); 2 | const plugin = require('babel-plugin-macros'); 3 | const prettier = require('prettier'); 4 | 5 | describe('babel macro', () => { 6 | pluginTester({ 7 | plugin, 8 | snapshot: true, 9 | babelOptions: { 10 | filename: __filename, 11 | plugins: ['dynamic-import-node'], 12 | }, 13 | formatResult(result: string) { 14 | return prettier.format(result, { trailingComma: 'es5' }); 15 | }, 16 | tests: { 17 | nothing: 'const a = 42;', 18 | 'no usage': `import {lazy} from "../macro";`, 19 | 'flat import': `import "../macro"; 20 | import('./a.js') 21 | `, 22 | boot: ` 23 | import {assignImportedComponents, lazy} from "../macro"; 24 | assignImportedComponents([() => import('./a')]); 25 | lazy(() => import('./a')); 26 | `, 27 | lazy: ` 28 | import {lazy} from "../macro"; 29 | const v = lazy(() => import('./a')); 30 | `, 31 | many: ` 32 | import {imported, useImported} from "../macro"; 33 | const v = imported(() => import('./a')); 34 | const x = () => useImported(() => import('./b')); 35 | `, 36 | }, 37 | }); 38 | }); 39 | 40 | describe('babel macro', () => { 41 | pluginTester({ 42 | plugin, 43 | snapshot: true, 44 | babelOptions: { 45 | filename: __filename, 46 | plugins: [require.resolve('../babel'), 'dynamic-import-node'], 47 | }, 48 | formatResult(result: string) { 49 | return prettier.format(result, { trailingComma: 'es5' }); 50 | }, 51 | tests: { 52 | 'plugin combination': ` 53 | import {imported, useImported} from "../macro"; 54 | const v = imported(() => import('./a')); 55 | const x = () => useImported(() => import('./b')); 56 | `, 57 | }, 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /__tests__/module.spec.tsx: -------------------------------------------------------------------------------- 1 | import '@theuiteam/lib-builder/configs/setupEnzyme'; 2 | import { mount } from 'enzyme'; 3 | import * as React from 'react'; 4 | import { act } from 'react-dom/test-utils'; 5 | 6 | import { done } from '../src/loadable/pending'; 7 | import { importedModule, ImportedModule } from '../src/ui/Module'; 8 | 9 | describe('Module Component', () => { 10 | describe('hoc', () => { 11 | it('should load component', async () => { 12 | const Component = importedModule(() => Promise.resolve((x: number) => x + 42)); 13 | 14 | const wrapper = mount({fn => {fn(42)}}); 15 | 16 | expect(wrapper.update().html()).toContain('loading'); 17 | 18 | await act(async () => { 19 | await done(); 20 | }); 21 | 22 | expect(wrapper.update().html()).toContain('84'); 23 | }); 24 | }); 25 | 26 | describe('component', () => { 27 | it('should load component', async () => { 28 | const importer = () => Promise.resolve((x: number) => x + 42); 29 | 30 | const wrapper = mount( 31 | 32 | {fn => {fn(42)}} 33 | 34 | ); 35 | 36 | expect(wrapper.update().html()).toContain('loading'); 37 | 38 | await act(async () => { 39 | await done(); 40 | }); 41 | 42 | expect(wrapper.update().html()).toContain('84'); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/signatures.spec.ts: -------------------------------------------------------------------------------- 1 | import { getFunctionSignature, importMatch } from '../src/utils/signatures'; 2 | 3 | describe('signatures', () => { 4 | const a = (i: any) => i; 5 | const b = (i: any) => i; 6 | 7 | it('extract markers from function', () => { 8 | expect(importMatch(getFunctionSignature(() => a('imported_XXYY_component')))).toEqual(['XXYY']); 9 | }); 10 | 11 | it('work similar for similar functions', () => { 12 | expect(getFunctionSignature(() => a('imported_XXYY_component'))).toBe( 13 | getFunctionSignature(() => b('imported_XXYY_component')) 14 | ); 15 | }); 16 | }); 17 | 18 | describe('importMatch', () => { 19 | it('standard', () => { 20 | expect( 21 | importMatch( 22 | getFunctionSignature( 23 | `() => importedWrapper('imported_mark1_component', Promise.resolve(TargetComponent)), true)` 24 | ) 25 | ) 26 | ).toEqual(['mark1']); 27 | }); 28 | 29 | it('webpack', () => { 30 | expect( 31 | importMatch( 32 | getFunctionSignature( 33 | `() => importedWrapper("imported_mark1_component", __webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))` 34 | ) 35 | ) 36 | ).toEqual(['mark1']); 37 | }); 38 | 39 | it('webpack-prod', () => { 40 | expect( 41 | importMatch( 42 | getFunctionSignature( 43 | `() => importedWrapper('imported_mark1_component',__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./components/Another */ "./app/components/Another.tsx")))` 44 | ) 45 | ) 46 | ).toEqual(['mark1']); 47 | }); 48 | 49 | it('functional', () => { 50 | expect( 51 | importMatch( 52 | getFunctionSignature(`"function loadable() { 53 | return importedWrapper('imported_1ubbetg_component', __webpack_require__.e(/*! import() | namedChunk-1 */ "namedChunk-1").then(__webpack_require__.t.bind(null, /*! ./DeferredRender */ "./src/DeferredRender.js", 7))); 54 | }"`) 55 | ) 56 | ).toEqual(['1ubbetg']); 57 | }); 58 | 59 | it('parcel', () => { 60 | expect( 61 | importMatch( 62 | getFunctionSignature(`function _() { 63 | return importedWrapper('imported_mark1_component', require("_bundle_loader")(require.resolve('./HelloWorld3'))); 64 | }`) 65 | ) 66 | ).toEqual(['mark1']); 67 | }); 68 | 69 | it('ie11 uglify', () => { 70 | expect( 71 | importMatch( 72 | getFunctionSignature(`function _() { 73 | var t = 'imported_mark1_component'; 74 | }`) 75 | ) 76 | ).toEqual(['mark1']); 77 | }); 78 | 79 | it('multiple imports in one line', () => { 80 | expect( 81 | importMatch( 82 | getFunctionSignature(`function _() { 83 | "imported_1pn9k36_component", blablabla- importedWrapper("imported_-1556gns_component") 84 | }`) 85 | ) 86 | ).toEqual(['1pn9k36', '-1556gns']); 87 | }); 88 | 89 | it('maps function signatures', () => { 90 | expect(getFunctionSignature(`import('file')`)).toEqual(getFunctionSignature(`import(/* */'file')`)); 91 | 92 | expect(getFunctionSignature(`import('file')`)).toEqual('import(`file`)'); 93 | }); 94 | 95 | it('maps function signatures after terser pass', () => { 96 | expect(getFunctionSignature('()=>$(`imported_-f5674t_component`,n.e(3).then(n.bind(null,`xxx`,7)))')).toEqual( 97 | getFunctionSignature('()=>$(`imported_-f5674t_component`,x.e(3).then(x.bind(null,`xxx`,7)))') 98 | ); 99 | expect(getFunctionSignature('()=>$(`imported_-f5674t_component`,n.e(3).then(n.bind(null,`xxx`,7)))')).toEqual( 100 | '()=>$(`imported_-f5674t_component`,-we().-wbind(null,`xxx`,7)))' 101 | ); 102 | }); 103 | 104 | it('maps internal and external signatures', () => { 105 | // internal is with Promise.resolve 106 | // extenal is with webpack_require.e 107 | expect( 108 | getFunctionSignature( 109 | '() => importedWrapper("imported_-1135avo_component", __webpack_require__.e(/*! import() */ 12).then(__webpack_require__.bind(null, /*! universal/components/SERP */ "./universal/components/SERP/index.js")))' 110 | ) 111 | ).toBe( 112 | getFunctionSignature( 113 | '() => importedWrapper("imported_-1135avo_component", Promise.resolve(/*! import() */).then(__webpack_require__.bind(null, /*! universal/components/SERP */ "./universal/components/SERP/index.js")))' 114 | ) 115 | ); 116 | }); 117 | 118 | it('maps function with different wrappers', () => { 119 | expect(getFunctionSignature('()=>P("imported_-is59m_component",t.e(41).then(t.bind(null,"./Promo.jsx")))')).toEqual( 120 | getFunctionSignature('()=>s("imported_-is59m_component",n.e(41).then(n.bind(null,"./Promo.jsx")))') 121 | ); 122 | expect(getFunctionSignature('()=>P("imported_-is59m_component",t.e(41).then(t.bind(null,"./Promo.jsx")))')).toEqual( 123 | '()=>$(`imported_-is59m_component`,-we().-wbind(null,`./Promo.jsx`)))' 124 | ); 125 | }); 126 | 127 | it('fallback check: same signature, different function', () => { 128 | expect( 129 | getFunctionSignature('()=>P("imported_-one_component",t.e(41).then(t.bind(null,"./Promo.jsx")))') 130 | ).not.toEqual( 131 | getFunctionSignature('()=>s("imported_-another_component",n.e(41).then(n.bind(null,"./Promo.jsx")))') 132 | ); 133 | }); 134 | }); 135 | -------------------------------------------------------------------------------- /assets/imported-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-imported-component/359cb486a049a4151f64c2e5c7b34ad3851cd554/assets/imported-logo.png -------------------------------------------------------------------------------- /babel.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./dist/es5/entrypoints/babel').default; 2 | -------------------------------------------------------------------------------- /bin/imported-components: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../dist/es5/scanners/cli'); -------------------------------------------------------------------------------- /boot/boot.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | declare var sidecar: React.SFC; 4 | 5 | export default sidecar; 6 | -------------------------------------------------------------------------------- /boot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/boot.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/boot.js", 5 | "module": "../dist/es2015/entrypoints/boot.js", 6 | "types": "../dist/es2015/entrypoints/boot.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /doczrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | typescript: true, 3 | title: 'Docz', 4 | menu: ['Getting Started', 'Components'], 5 | }; 6 | -------------------------------------------------------------------------------- /example/app.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import imported, {lazy, LazyBoundary} from "../src"; 4 | 5 | const deferImport = (promise: Promise) => new Promise(resolve => setTimeout(() => resolve(promise), 2000)); 6 | 7 | const Lazy = () =>
I AM LAZY
; 8 | 9 | 10 | const ReactLazy = React.lazy(() => deferImport({default: Lazy})); 11 | const ImportedLazy = lazy(() => deferImport({default: Lazy})); 12 | const Imported = imported(() => deferImport({default: Lazy})); 13 | const ImportedLoading = imported(() => deferImport({default: Lazy}), { LoadingComponent: () => "imported is loading"}); 14 | 15 | function App() { 16 | return ( 17 |
18 |

Lazy Loading

19 |
    20 |
  • 21 | Loading (React)
}> 22 | 23 | 24 | 25 |
  • 26 | Loading (Imported)}> 27 | 28 | 29 |
  • 30 |
  • 31 | 32 |
  • 33 |
  • 34 | 35 |
  • 36 | 37 | 38 | ); 39 | } 40 | 41 | export default App; -------------------------------------------------------------------------------- /example/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-imported-component/359cb486a049a4151f64c2e5c7b34ad3851cd554/example/assets/.gitkeep -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Example 6 | 7 | 8 |
    9 | 10 | 11 | -------------------------------------------------------------------------------- /example/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | 5 | ReactDOM.render(, document.getElementById('app')); 6 | -------------------------------------------------------------------------------- /examples/CRA/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /examples/CRA/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | dist 5 | yarn.lock -------------------------------------------------------------------------------- /examples/CRA/async-imports.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /* tslint:disable */ 4 | 5 | // generated by react-imported-component, DO NOT EDIT 6 | import {assignImportedComponents} from 'react-imported-component/macro'; 7 | 8 | // all your imports are defined here 9 | // all, even the ones you tried to hide in comments (that's the cost of making a very fast parser) 10 | // to remove any import from here 11 | // 1) use magic comment `import(/* client-side */ './myFile')` - and it will disappear 12 | // 2) use file filter to ignore specific locations (refer to the README) 13 | 14 | const applicationImports = assignImportedComponents([ 15 | [() => import('./src/async'), '', './src/async', false], 16 | ]); 17 | 18 | export default applicationImports; 19 | 20 | // @ts-ignore 21 | if (module.hot) { 22 | // these imports would make this module a parent for the imported modules. 23 | // but this is just a helper - so ignore(and accept!) all updates 24 | 25 | // @ts-ignore 26 | module.hot.accept(() => null); 27 | } 28 | -------------------------------------------------------------------------------- /examples/CRA/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-react-app-example", 3 | "version": "1.0.0", 4 | "description": "", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "dependencies": { 8 | "react": "16.8.6", 9 | "react-dom": "16.8.6", 10 | "react-imported-component": "6.1.1", 11 | "react-scripts": "3.0.1" 12 | }, 13 | "devDependencies": { 14 | "babel-jest": "^24.8.0", 15 | "typescript": "3.3.3" 16 | }, 17 | "scripts": { 18 | "start": "imported-components src async-imports.js && react-scripts start", 19 | "build": "react-scripts build", 20 | "test": "react-scripts test --env=jsdom", 21 | "eject": "react-scripts eject" 22 | }, 23 | "browserslist": [ 24 | ">0.2%", 25 | "not dead", 26 | "not ie <= 11", 27 | "not op_mini all" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /examples/CRA/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
    31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /examples/CRA/src/async-imports.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* tslint:disable */ 3 | 4 | // generated by react-imported-component, DO NOT EDIT 5 | import { assignImportedComponents } from "react-imported-component/macro"; 6 | 7 | // all your imports are defined here 8 | // all, even the ones you tried to hide in comments (that's the cost of making a very fast parser) 9 | // to remove any import from here 10 | // 1) use magic comment `import(/* client-side */ './myFile')` - and it will disappear 11 | // 2) use file filter to ignore specific locations (refer to the README) 12 | 13 | const applicationImports = assignImportedComponents([ 14 | [() => import("./async"), "", "./async", false] 15 | ]); 16 | 17 | console.log("async imports called"); 18 | 19 | export default applicationImports; 20 | 21 | // @ts-ignore 22 | if (module.hot) { 23 | // these imports would make this module a parent for the imported modules. 24 | // but this is just a helper - so ignore(and accept!) all updates 25 | 26 | // @ts-ignore 27 | module.hot.accept(() => null); 28 | } 29 | -------------------------------------------------------------------------------- /examples/CRA/src/async.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>
    loaded async!
    -------------------------------------------------------------------------------- /examples/CRA/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | 4 | // this import should enable import transpilation 5 | import "react-imported-component/macro"; 6 | 7 | import './async-imports'; 8 | 9 | import "./styles.css"; 10 | import Load from "./load"; 11 | 12 | const Async = Load(() => import("./async")); 13 | 14 | function App() { 15 | return ( 16 |
    17 |

    Hello CodeSandbox

    18 |

    Start editing to see some magic happen!

    19 | 20 |
    21 | ); 22 | } 23 | 24 | const rootElement = document.getElementById("root"); 25 | ReactDOM.render(, rootElement); 26 | -------------------------------------------------------------------------------- /examples/CRA/src/load.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { imported, lazy } from "react-imported-component/macro"; 3 | 4 | const Load = component => { 5 | if (!component) return; 6 | return imported(component, { 7 | LoadingComponent: () =>

    Loading

    , 8 | ErrorComponent: () =>

    Error

    9 | }); 10 | }; 11 | 12 | 13 | export default Load; 14 | -------------------------------------------------------------------------------- /examples/CRA/src/styles.css: -------------------------------------------------------------------------------- 1 | .App { 2 | font-family: sans-serif; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": [ 4 | "react-imported-component/babel", 5 | "@babel/plugin-proposal-dynamic-import", 6 | "@babel/plugin-transform-modules-commonjs" 7 | ], 8 | "env": { 9 | "client": { 10 | "plugins": [ 11 | "react-hot-loader/babel" 12 | ] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | dist 5 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © Benoit Tremblay 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/README.md: -------------------------------------------------------------------------------- 1 | __COMPLEX__ streamed SSR example 2 | 3 | - `yarn start` - simple `renderToString` SSR + used style extraction 4 | - `yarn start:stream` - complex `renderToStream` example 5 | - if `stream-server/middleware` is used - all styles would be in HEAD 6 | - if `stream-server/interleaved-middleware` is used - all styles would be _interleaved_ in result HTML -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/App.jsx: -------------------------------------------------------------------------------- 1 | import {hot} from 'react-hot-loader'; 2 | import React from 'react'; 3 | import importedComponent from 'react-imported-component'; 4 | 5 | import { Helmet } from 'react-helmet'; 6 | import { Switch, Route, Redirect } from 'react-router-dom'; 7 | 8 | import HelloWorld from './HelloWorld'; 9 | import "./base.css"; 10 | 11 | const HelloWorld2 = importedComponent(() => import('./HelloWorld2')); 12 | 13 | export default hot(module)(function App() { 14 | return ( 15 |
    16 | 17 | 18 | 19 | App 20 | 21 | 22 | 23 | 24 | 25 |
    26 | ); 27 | }) 28 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/HelloWorld.jsx: -------------------------------------------------------------------------------- 1 | // Dead simple component for the hello world (hi mom!) 2 | 3 | import React from 'react'; 4 | import { Link } from 'react-router-dom'; 5 | import './HelloWorld.scss'; 6 | 7 | export default function HelloWorld() { 8 | return
    9 |

    Hello world

    10 |

    11 | This is an ordinary react component. 12 |
    13 | Click here to see a code-split component. 14 |

    15 |
    ; 16 | } 17 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/HelloWorld.scss: -------------------------------------------------------------------------------- 1 | // With Parcel, you have nothing to setup for SCSS, you just write! 2 | 3 | $primary-color: #0098da; 4 | 5 | body { 6 | margin: 0; 7 | } 8 | 9 | .hello-world { 10 | background: $primary-color; 11 | color: white; 12 | text-align: center; 13 | } 14 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/HelloWorld2.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import './codeSplitAssets/NyanCat.css'; 4 | import Go from './codeSplitAssets/NyanCat.js'; 5 | // import HelloWorld3 from './HelloWorld3'; 6 | 7 | // right now Parcel does not support `import inside import` - https://github.com/parcel-bundler/parcel/issues/2620 8 | 9 | import importedComponent from "react-imported-component"; 10 | 11 | const HelloWorld3 = importedComponent(() => import('./HelloWorld3')); 12 | 13 | export default class Nyan extends Component { 14 | 15 | componentDidMount() { 16 | Go(); 17 | } 18 | 19 | render() { 20 | return
    21 |

    Code-splitted CAT!

    22 |

    23 | This is a code-split component. 24 |
    25 | Click here to see an ordinary component. 26 |

    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 |
    34 | 35 |
    36 |
    37 |
    38 |
    39 |
    40 | 41 |
    42 |
    43 | and it has a nested code-splitted cat.... 44 | 45 |
    46 | } 47 | } 48 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/HelloWorld3.jsx: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react'; 2 | import {Link} from 'react-router-dom'; 3 | import './codeSplitAssets/NyanCat.css'; 4 | import Go from './codeSplitAssets/NyanCat.js'; 5 | import {say42} from "./codeSplitAssets/utils"; 6 | 7 | export default class Nyan extends Component { 8 | 9 | componentDidMount() { 10 | Go(); 11 | } 12 | 13 | render() { 14 | return
    15 | I am async {say42()} cat 16 |
    17 |
    18 | 19 |
    20 |
    21 |
    22 |
    23 | 24 |
    25 |
    26 |
    27 |
    28 |
    29 | 30 |
    31 |
    32 |
    33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/base.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/client.js: -------------------------------------------------------------------------------- 1 | // pre-import imported component 2 | import './imported-chunk'; 3 | 4 | // install stream helper 5 | import {injectLoadableTracker} from 'react-imported-component/boot'; 6 | 7 | injectLoadableTracker('exampleTracker'); 8 | 9 | // HINT! 10 | // ------------ 11 | // CASE 1 - let browser load chunks before going further 12 | // outcome - network and CPU are working together! 👍 13 | // ------------ 14 | if(1) { 15 | // load the rest after letting the browser kick off chunk loading 16 | Promise.resolve().then(() => 17 | Promise.resolve().then(() => { 18 | require('./main') 19 | }) 20 | ); 21 | } else { 22 | 23 | // ------------ 24 | // CASE 2 - dont do that 25 | // outcome - network is idle while CPU is busy 👎 26 | // ------------ 27 | 28 | // require('./main') 29 | } 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/codeSplitAssets/NyanCat.js: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const stars = document.querySelectorAll('.stars')[0]; 3 | for (let i = 0; i < 12; i += 1) { 4 | stars.innerHTML += '
    '; 5 | }; 6 | } -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/codeSplitAssets/utils.js: -------------------------------------------------------------------------------- 1 | export const say42 = () => 42; -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/imported-chunk.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /* tslint:disable */ 4 | 5 | // generated by react-imported-component, DO NOT EDIT 6 | import {assignImportedComponents} from 'react-imported-component/macro'; 7 | 8 | // all your imports are defined here 9 | // all, even the ones you tried to hide in comments (that's the cost of making a very fast parser) 10 | // to remove any import from here 11 | // 1) use magic comment `import(/* client-side */ './myFile')` - and it will disappear 12 | // 2) use file filter to ignore specific locations (refer to the README) 13 | 14 | const applicationImports = assignImportedComponents([ 15 | [() => import('./HelloWorld2'), '', './app/HelloWorld2', false], 16 | [() => import('./HelloWorld3'), '', './app/HelloWorld3', false], 17 | ]); 18 | 19 | export default applicationImports; 20 | 21 | // @ts-ignore 22 | if (module.hot) { 23 | // these imports would make this module a parent for the imported modules. 24 | // but this is just a helper - so ignore(and accept!) all updates 25 | 26 | // @ts-ignore 27 | module.hot.accept(() => null); 28 | } 29 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
    12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/app/main.js: -------------------------------------------------------------------------------- 1 | // Entry point for the browser 2 | // Start your React application and add the required containers 3 | // Here we have for react-router 4 | 5 | import 'react-hot-loader'; 6 | import {rehydrateMarks} from 'react-imported-component'; 7 | import React from 'react'; 8 | import ReactDOM from 'react-dom'; 9 | import {BrowserRouter} from 'react-router-dom'; 10 | import {moveStyles} from 'used-styles/moveStyles'; 11 | // import chunk definition 12 | import './imported-chunk'; 13 | 14 | import App from './App'; 15 | 16 | // move SSR-ed styles to the head 17 | moveStyles(); 18 | 19 | const element = document.getElementById('app'); 20 | const app = ( 21 | 22 | 23 | 24 | ); 25 | 26 | const TM = 1000; 27 | 28 | console.log('waiting'); 29 | setTimeout(function () { 30 | // rehydrate the bundle marks 31 | console.log('loading'); 32 | rehydrateMarks().then(() => { 33 | console.log('loaded...'); 34 | setTimeout(function () { 35 | console.log('rendering'); 36 | // In production, we want to hydrate instead of render 37 | // because of the server-rendering 38 | if (1 || process.env.NODE_ENV === 'production') { 39 | ReactDOM.hydrate(app, element); 40 | } else { 41 | ReactDOM.render(app, element); 42 | } 43 | }, TM); 44 | }); 45 | }, TM); 46 | 47 | // Hot reload is that easy with Parcel 48 | if (module.hot) { 49 | module.hot.accept(); 50 | } 51 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parcel-react-ssr", 3 | "version": "0.4.0", 4 | "description": "Example of SSR with React and ParcelJS", 5 | "main": "index.js", 6 | "keywords": [ 7 | "parcel", 8 | "react", 9 | "ssr" 10 | ], 11 | "author": "Benoit Tremblay ", 12 | "license": "MIT", 13 | "repository": "reactivestack/parcel-react-ssr", 14 | "scripts": { 15 | "dev": "parcel app/index.html", 16 | "build": "rimraf dist && npm run build-imported && npm run build-client && npm run build-server && npm run build-server-stream", 17 | "build-imported": "imported-components app app/imported-chunk.js", 18 | "build-client": "cross-env BABEL_ENV=client parcel build app/index.html -d dist/client --public-url /dist --no-minify --no-cache", 19 | "build-server": "cross-env BABEL_ENV=server parcel build server/index.js -d dist/server --public-url /dist --target=node --no-minify --no-cache", 20 | "build-server-stream": "cross-env BABEL_ENV=server parcel build stream-server/index.js -d dist/server-stream --public-url /dist --target=node --no-minify --no-cache", 21 | "start": "node dist/server", 22 | "start:stream": "node dist/server-stream" 23 | }, 24 | "alias": { 25 | "react": "./node_modules/react", 26 | "react-dom": "./node_modules/react-dom" 27 | }, 28 | "dependencies": { 29 | "cheerio": "^1.0.0-rc.3", 30 | "compression": "^1.7.4", 31 | "express": "^4.17.1", 32 | "multistream": "^4.0.0", 33 | "parcel-plugin-bundle-manifest": "^0.2.0", 34 | "react": "^16.12.0", 35 | "react-dom": "^16.12.0", 36 | "react-helmet": "^5.2.1", 37 | "react-imported-component": "^6.2.1", 38 | "react-router-dom": "^4.2.2", 39 | "used-styles": "^1.1.0" 40 | }, 41 | "devDependencies": { 42 | "@babel/cli": "^7.8.4", 43 | "@babel/core": "^7.8.4", 44 | "@babel/plugin-proposal-dynamic-import": "^7.8.3", 45 | "@babel/plugin-transform-modules-commonjs": "^7.8.3", 46 | "@babel/preset-env": "7.8.4", 47 | "@babel/preset-react": "7.8.3", 48 | "cross-env": "^7.0.0", 49 | "node-sass": "^4.13.1", 50 | "parcel-bundler": "^1.12.4", 51 | "react-hot-loader": "^4.12.19", 52 | "rimraf": "^3.0.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/server/generateHtml.js: -------------------------------------------------------------------------------- 1 | // Generate the HTML using index.html as a template 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import cheerio from 'cheerio'; 6 | import {Helmet} from 'react-helmet'; 7 | 8 | // The path is relative from server bundle to client bundle, not the source 9 | const templatePath = path.join(__dirname, '..', 'client', 'index.html'); 10 | const HTML_TEMPLATE = fs.readFileSync(templatePath).toString(); 11 | 12 | export default function generateHtml(markup, styles) { 13 | // Get the serer-rendering values for the 14 | const helmet = Helmet.renderStatic(); 15 | 16 | const $template = cheerio.load(HTML_TEMPLATE); 17 | $template('head').append( 18 | helmet.title.toString() + helmet.meta.toString() + helmet.link.toString() 19 | ); 20 | styles.forEach(style => { 21 | const link = `\n`; 22 | $template('head').append(link) 23 | }); 24 | $template('#app').html(markup); 25 | console.log(markup); 26 | 27 | return $template.html(); 28 | } 29 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/server/index.js: -------------------------------------------------------------------------------- 1 | // Setup express to handle Web requests 2 | 3 | import compression from 'compression'; 4 | import express from 'express'; 5 | import middleware from './middleware'; 6 | 7 | const app = express(); 8 | 9 | // Add gzip compression to responses 10 | app.use(compression()); 11 | 12 | // Expose the public directory as /dist and point to the browser version 13 | app.use('/dist', express.static(__dirname + '/../client')); 14 | 15 | // Anything unresolved is serving the application and let 16 | // react-router do the routing! 17 | app.get('/*', middleware); 18 | 19 | // Check for PORT environment variable, otherwise fallback on Parcel default port 20 | const port = process.env.PORT || 1234; 21 | app.listen(port, () => { 22 | console.log(`Listening on port ${port}...`); 23 | }); 24 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/server/middleware.js: -------------------------------------------------------------------------------- 1 | // Middleware for the server-rendering 2 | 3 | import {printDrainHydrateMarks} from 'react-imported-component'; 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom/server'; 6 | import {StaticRouter} from 'react-router-dom'; 7 | 8 | import App from '../app/App'; 9 | import generateHtml from './generateHtml'; 10 | import {getProjectStyles, getUsedStyles} from 'used-styles'; 11 | 12 | let projectStyles; 13 | getProjectStyles(__dirname+'/../client').then(x => { 14 | projectStyles = x; 15 | console.log(x); 16 | }); 17 | 18 | export default function middleware(req, res) { 19 | // Generate the server-rendered HTML using the appropriate router 20 | const context = {}; 21 | const markup = ReactDOM.renderToString( 22 | 23 | 24 | 25 | ) + printDrainHydrateMarks(); 26 | 27 | // If react-router is redirecting, do it on the server side 28 | if (context.url) { 29 | return res.redirect(301, context.url); 30 | } 31 | 32 | const usedStyles = getUsedStyles(markup, projectStyles); 33 | console.log('used styles', usedStyles); 34 | // Format the HTML using the template and send the result 35 | const html = generateHtml('JS will start in ~2s
    ' + markup, usedStyles); 36 | res.send(html); 37 | } 38 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/stream-server/index.js: -------------------------------------------------------------------------------- 1 | // Setup express to handle Web requests 2 | 3 | import compression from 'compression'; 4 | import express from 'express'; 5 | 6 | // Pick one of these 7 | 8 | import middleware from './middleware'; 9 | // import middleware from './interleaved-middleware'; 10 | 11 | const app = express(); 12 | 13 | // Add gzip compression to responses 14 | app.use(compression()); 15 | 16 | // Expose the public directory as /dist and point to the browser version 17 | app.use('/dist', express.static(__dirname + '/../client')); 18 | 19 | // Anything unresolved is serving the application and let 20 | // react-router do the routing! 21 | app.get('/*', middleware); 22 | 23 | // Check for PORT environment variable, otherwise fallback on Parcel default port 24 | const port = process.env.PORT || 1234; 25 | app.listen(port, () => { 26 | console.log(`Listening on port ${port}...`); 27 | }); 28 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/stream-server/interleaved-middleware.js: -------------------------------------------------------------------------------- 1 | // Middleware for the server-rendering 2 | import {Readable} from 'stream'; 3 | import fs from 'fs'; 4 | import MultiStream from 'multistream'; 5 | import {getProjectStyles, createLink} from 'used-styles'; 6 | import {createStyleStream} from 'used-styles/react'; 7 | import {printDrainHydrateMarks, ImportedStream} from 'react-imported-component'; 8 | import {createLoadableStream} from 'react-imported-component/server'; 9 | import React from 'react'; 10 | import ReactDOM from 'react-dom/server'; 11 | import {StaticRouter} from 'react-router-dom'; 12 | 13 | import App from '../app/App'; 14 | 15 | const readable = () => { 16 | const s = new Readable(); 17 | s._read = () => true; 18 | return s; 19 | }; 20 | 21 | const readableString = string => { 22 | const s = new Readable(); 23 | s.push(string); 24 | s.push(null); 25 | s._read = () => true; 26 | return s; 27 | }; 28 | 29 | let projectStyles = {}; 30 | getProjectStyles(__dirname + '/../client').then(x => { 31 | projectStyles = x; 32 | console.log(x); 33 | }); 34 | 35 | const manifect = JSON.parse(fs.readFileSync(__dirname + '/../client/parcel-manifest.json')); 36 | 37 | export default function middleware(req, res) { 38 | // Generate the server-rendered HTML using the appropriate router 39 | const context = {}; 40 | 41 | let streamUID = createLoadableStream(); 42 | const htmlStream = ReactDOM.renderToNodeStream( 43 | 44 | 45 | 46 | 47 | 48 | ); 49 | 50 | // If react-router is redirecting, do it on the server side 51 | if (context.url) { 52 | return res.redirect(301, context.url); 53 | } 54 | 55 | // create a style steam 56 | const styledStream = createStyleStream(projectStyles, (style) => ( 57 | // just return it 58 | createLink(`dist/${style}`) 59 | )); 60 | 61 | // allow client to start loading js bundle 62 | res.write(`
    `); 63 | 64 | const endStream = readableString(''); 65 | 66 | const streams = [ 67 | styledStream, 68 | endStream, 69 | ]; 70 | 71 | MultiStream(streams).pipe(res); 72 | 73 | // start by piping react and styled transform stream 74 | htmlStream.pipe(styledStream); 75 | styledStream.on('end', () => { 76 | res.write('
    '); 77 | // push loaded chunks information 78 | res.write(printDrainHydrateMarks(streamUID)); 79 | res.write(''); 80 | res.end(); 81 | }); 82 | } 83 | -------------------------------------------------------------------------------- /examples/SSR/parcel-react-ssr/stream-server/middleware.js: -------------------------------------------------------------------------------- 1 | // Middleware for the server-rendering 2 | import {Readable} from 'stream'; 3 | import fs from 'fs'; 4 | import MultiStream from 'multistream'; 5 | import {getProjectStyles, createStyleStream} from 'used-styles'; 6 | import {printDrainHydrateMarks, ImportedStream} from 'react-imported-component'; 7 | import {createLoadableStream, createLoadableTransformer, getLoadableTrackerCallback} from 'react-imported-component/server'; 8 | import React from 'react'; 9 | import ReactDOM from 'react-dom/server'; 10 | import {StaticRouter} from 'react-router-dom'; 11 | 12 | import App from '../app/App'; 13 | 14 | const readable = () => { 15 | const s = new Readable(); 16 | s._read = () => true; 17 | return s; 18 | }; 19 | 20 | const readableString = string => { 21 | const s = new Readable(); 22 | s.push(string); 23 | s.push(null); 24 | s._read = () => true; 25 | return s; 26 | }; 27 | 28 | let projectStyles = {}; 29 | getProjectStyles(__dirname + '/../client').then(x => { 30 | projectStyles = x; 31 | console.log(x); 32 | }); 33 | 34 | const manifect = JSON.parse(fs.readFileSync(__dirname + '/../client/parcel-manifest.json')); 35 | 36 | export default function middleware(req, res) { 37 | // Generate the server-rendered HTML using the appropriate router 38 | const context = {}; 39 | 40 | let streamUID = createLoadableStream(); 41 | const htmlStream = ReactDOM.renderToNodeStream( 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | 49 | // If react-router is redirecting, do it on the server side 50 | if (context.url) { 51 | return res.redirect(301, context.url); 52 | } 53 | 54 | // create a "header" stream 55 | const headerStream = readable(); 56 | 57 | // create a style steam 58 | const styledStream = createStyleStream(projectStyles, (style) => { 59 | // emit a line to header Stream 60 | headerStream.push(`\n/*used style*/\n`); 61 | }); 62 | 63 | // allow client to start loading js bundle 64 | res.write([ 65 | '' 66 | ``, 67 | // HINT! 68 | // uncomment this line to convert this example in a "classical one", when all scripts are present in the HTML code 69 | // ``, 70 | ].join('\n')); 71 | 72 | /* 73 | 74 | res 75 | <- headerStream 76 | <- middleStream 77 | <- styledStream 78 | <- importedHelper 79 | <- htmlStream (React) 80 | <- htmlStream "end" 81 | */ 82 | 83 | 84 | const middleStream = readableString('
    '); 85 | const endStream = readableString('
    '); 86 | 87 | const streams = [ 88 | headerStream, 89 | middleStream, 90 | styledStream, 91 | endStream, 92 | ]; 93 | 94 | const importedHelper = createLoadableTransformer(streamUID, getLoadableTrackerCallback("exampleTracker")); 95 | 96 | MultiStream(streams).pipe(res); 97 | 98 | // start by piping react and styled transform stream 99 | htmlStream 100 | .pipe(importedHelper) 101 | .pipe(styledStream, {end: false}); 102 | 103 | htmlStream.on('end', () => { 104 | // push loaded chunks information 105 | headerStream.push(printDrainHydrateMarks(streamUID)); 106 | // kill header stream on the main stream end 107 | headerStream.push(null); 108 | styledStream.end(); 109 | }); 110 | } 111 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-imported-component/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: debian:stretch 6 | steps: 7 | - checkout 8 | - run: 9 | name: Greeting 10 | command: echo Hello, world. 11 | - run: 12 | name: Print the Current Time 13 | command: date -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | dist 5 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/README.md: -------------------------------------------------------------------------------- 1 | Example using `runtime chunk` or `manifest` to speed up chunk loading 2 | 3 | 4 | -- not complete! -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #EEE; 3 | } -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Home from "./components/Home"; 3 | import importedComponent, {ComponentLoader, loadableResource} from "react-imported-component"; 4 | 5 | const Another = importedComponent(() => import(/* webpackChunkName: namedChunk-0 */"./components/Another"), { 6 | LoadingComponent: () =>
    loading
    7 | }); 8 | const Other1 = importedComponent(() => import(/* webpackChunkName: "namedChunk-1" */"./components/Other")); 9 | const Other2 = importedComponent(() => import(/* webpackChunkName: "namedChunk-1" */"./components/OtherTween")); 10 | 11 | const AnotherWrapped = importedComponent( 12 | () => import(/* webpackChunkName: namedChunk-0 */"./components/Another"), { 13 | render(Component, state, props: { prop: number }) { 14 | if (state.loading) { 15 | return 16 | } 17 | return
    18 | } 19 | }); 20 | //import Another from "./components/Another"; 21 | 22 | // @ts-ignore 23 | // const importCss = () => im port("./App.css"); 24 | 25 | export default function App() { 26 | return ( 27 |
    28 | [not-trackable] 29 | import("./components/Another")} 31 | /> 32 | import("./components/Another"))} 34 | /> 35 | [/not-trackable] 36 | [home][/home] 37 | 38 | 39 | 40 | 41 | 42 | {0 && } 43 | 44 |
    45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-imported-component/359cb486a049a4151f64c2e5c7b34ad3851cd554/examples/SSR/runtime-manifest/app/assets/favicon.ico -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/client.tsx: -------------------------------------------------------------------------------- 1 | import { rehydrateMarks } from "react-imported-component"; 2 | import * as React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Provider } from "react-redux"; 6 | import { configureStore } from "./store"; 7 | import "./imported"; // eslint-disable-line 8 | 9 | import App from "./App"; 10 | 11 | const preloadedState = window.__PRELOADED_STATE__; 12 | delete window.__PRELOADED_STATE__; 13 | 14 | const store = configureStore(preloadedState); 15 | 16 | const element = document.getElementById("app"); 17 | 18 | const app = ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | rehydrateMarks().then(() => { 27 | console.log(element.innerHTML); 28 | ReactDOM.hydrate(app, element); 29 | console.log(element.innerHTML); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/Another.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Another:React.SFC<{test: number, p2: number}> = () => Another!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Helmet} from "react-helmet"; 3 | import {Link} from "react-router-dom"; 4 | import imported, {lazy, LazyBoundary, remapImports} from "react-imported-component"; 5 | 6 | const LZ = lazy(() => import("./LazyTarget")); 7 | 8 | const ND = imported( 9 | () => remapImports( 10 | import('./NotDefault'), 11 | ({NotDefault}) => NotDefault 12 | ) 13 | ); 14 | 15 | const Home = () => ( 16 |
    17 | 18 | Homepage! 19 | 20 | Link to Another 21 |
    Hello!
    22 | 23 | 24 | 25 | 26 |
    27 | ); 28 | export default Home; 29 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/LazyTarget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const LazyTarget: React.SFC = () => I am not lazy Lazy; 4 | export default LazyTarget; 5 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/NotDefault.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {useImported} from "react-imported-component" 3 | 4 | export const NotDefault: React.FC<{ p1: number }> = ({p1}) => { 5 | const { 6 | imported = x => `!${x}!`, 7 | } = useImported(() => import("./library"), (lib) => lib.magicFunction); 8 | 9 | return ( 10 | 11 | I am not not Default Export({p1} prop == {imported(p1)}) 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/Other.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Another:React.SFC<{test: number}> = () => Other!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/OtherTween.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Another: React.SFC = () => Other Twin!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/components/library.tsx: -------------------------------------------------------------------------------- 1 | console.log('loading library function'); 2 | 3 | export const magicFunction = (x: number) => `** ${x} **`; -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/imported.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /* tslint:disable */ 4 | 5 | import {assignImportedComponents} from 'react-imported-component/boot'; 6 | 7 | const applicationImports = [ 8 | () => import('./components/Another'), 9 | () => import('./components/LazyTarget'), 10 | () => import('./components/NotDefault'), 11 | () => import('./components/library'), 12 | () => import(/* webpackChunkName: "namedChunk-1" */'./components/Other'), 13 | () => import(/* webpackChunkName: "namedChunk-1" */'./components/OtherTween'), 14 | ]; 15 | 16 | assignImportedComponents(applicationImports); 17 | export default applicationImports; -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import rootReducer from "./reducers"; 4 | 5 | export function configureStore(initialState = {}) { 6 | const store = createStore(rootReducer, initialState, composeWithDevTools()); 7 | 8 | if (module.hot) { 9 | module.hot.accept("./reducers", () => { 10 | console.log("Replacing reducers..."); 11 | store.replaceReducer(require("./reducers").default); 12 | }); 13 | } 14 | 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/app/store/reducers.ts: -------------------------------------------------------------------------------- 1 | export default function rootReducer(state) { 2 | return state; 3 | } 4 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate-imported-component": "imported-components app app/imported.ts", 4 | "build": "yarn webpack", 5 | "start": "node dist/server/index.js", 6 | "cp:1": "cp -R ../../../dist ./node_modules/react-imported-component/", 7 | "cp:2": "cp ../../../package.json ./node_modules/react-imported-component", 8 | "cp:3": "cp ../../../boot ./node_modules/react-imported-component", 9 | "cp": "yarn cp:1 && yarn cp:2 && yarn cp:3" 10 | }, 11 | "dependencies": { 12 | "@types/cheerio": "^0.22.7", 13 | "@types/react": "^16.9.2", 14 | "@types/redux-devtools-extension": "^2.13.2", 15 | "@types/webpack-env": "^1.13.6", 16 | "awesome-typescript-loader": "^5.0.0", 17 | "babel-plugin-dynamic-import-node": "^1.2.0", 18 | "cheerio": "^1.0.0-rc.2", 19 | "compression": "^1.7.2", 20 | "express": "^4.16.3", 21 | "file-loader": "^1.1.11", 22 | "html-webpack-plugin": "^3.2.0", 23 | "react": "^16.9.0", 24 | "react-dom": "^16.9.0", 25 | "react-helmet": "^5.2.0", 26 | "react-imported-component": "^6.0.0-beta.3", 27 | "react-redux": "^5.0.7", 28 | "react-router-dom": "^4.2.2", 29 | "redux": "^4.0.0", 30 | "redux-devtools-extension": "^2.13.2", 31 | "ts-loader": "^4.2.0", 32 | "typescript": "^2.8.3", 33 | "webpack-serve": "^1.0.2" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.21.0", 37 | "babel-loader": "^7.1.4", 38 | "babel-preset-es2015": "^6.18.0", 39 | "css-loader": "^2.1.0", 40 | "style-loader": "^0.23.1", 41 | "webpack": "^4.29.0", 42 | "webpack-cli": "^3.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/server/generateHtml.ts: -------------------------------------------------------------------------------- 1 | // Generate the HTML using index.html as a template 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import cheerio from "cheerio"; 6 | import { Helmet } from "react-helmet"; 7 | import { printDrainHydrateMarks } from "react-imported-component"; 8 | 9 | // The path is relative from server bundle to client bundle, not the source 10 | const templatePath = path.join(__dirname, "..", "client", "index.html"); 11 | const HTML_TEMPLATE = fs.readFileSync(templatePath).toString(); 12 | 13 | export default function generateHtml(markup, state, getStream) { 14 | // Get the serer-rendering values for the 15 | const helmet = Helmet.renderStatic(); 16 | 17 | const $template = cheerio.load(HTML_TEMPLATE); 18 | $template("head").append( 19 | helmet.title.toString() + helmet.meta.toString() + helmet.link.toString() 20 | ); 21 | $template("head").append( 22 | `` 25 | ); 26 | $template("head").append(printDrainHydrateMarks(getStream())); 27 | $template("#app").html(markup); 28 | 29 | return $template.html(); 30 | } 31 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/server/index.ts: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import middleware from "./middleware"; 4 | 5 | const app = express(); 6 | 7 | // Add gzip compression to responses 8 | app.use(compression()); 9 | 10 | // Expose the public directory as /dist and point to the browser version 11 | app.use(express.static(`${__dirname}/../client`, { index: false })); 12 | 13 | // Anything unresolved is serving the application and let 14 | // react-router do the routing! 15 | app.get("/*", middleware); 16 | 17 | // Check for PORT environment variable, otherwise fallback on Parcel default port 18 | const port = process.env.PORT || 1234; 19 | app.listen(port, () => { 20 | console.log(`Listening on port ${port}...`); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/server/middleware.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/server"; 3 | import {StaticRouter} from "react-router-dom"; 4 | import {Provider} from "react-redux"; 5 | 6 | import "../app/imported"; 7 | import App from "../app/App"; 8 | import {configureStore} from "../app/store"; 9 | 10 | import generateHtml from "./generateHtml"; 11 | import {ImportedStream} from "react-imported-component"; 12 | import {createLoadableStream} from "react-imported-component/server"; 13 | 14 | const store = configureStore(); 15 | 16 | export default (req, res) => { 17 | // Generate the server-rendered HTML using the appropriate router 18 | const context: { url?: string } = {}; 19 | let streamId = createLoadableStream(); 20 | const router = ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | const markup = ReactDOM.renderToString(router); 30 | 31 | // If react-router is redirecting, do it on the server side 32 | if (context.url) { 33 | res.redirect(301, context.url); 34 | } else { 35 | // Format the HTML using the template and send the result 36 | 37 | // stream is not defined unless we call the render 38 | const html = generateHtml(markup, store.getState(), () => streamId); 39 | res.send(html); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["dom", "es2015", "es2015.promise", "es2016"], 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "baseUrl": "." 18 | }, 19 | "files": ["types.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint-config-airbnb", "tslint-react", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "variable-name": false, 7 | "function-name": false, 8 | "jsx-no-lambda": false, 9 | "import-name": false 10 | }, 11 | "rulesDirectory": [] 12 | } 13 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.ico"; 2 | 3 | interface Window { 4 | ___REACT_DEFERRED_COMPONENT_MARKS: any; 5 | __PRELOADED_STATE__: any; 6 | } 7 | 8 | declare var window: Window; 9 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/webpack-client.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { 4 | CheckerPlugin 5 | } = require("awesome-typescript-loader"); 6 | const sharedConfig = require("./webpack-shared.config.js"); 7 | 8 | module.exports = { 9 | ...sharedConfig(false), 10 | entry: path.join(__dirname, "app", "client.tsx"), 11 | output: { 12 | // publicPath: "/dist", 13 | filename: "client.js", 14 | path: path.resolve(__dirname, "dist", "client"), 15 | chunkFilename: '[name].client.js', 16 | }, 17 | optimization: { 18 | runtimeChunk: { 19 | name: 'manifest', 20 | }, 21 | }, 22 | plugins: [ 23 | new HtmlWebpackPlugin({ 24 | hash: true, 25 | filename: path.resolve(__dirname, "dist", "client", "index.html"), 26 | template: path.resolve(__dirname, "app", "index.html") 27 | }), 28 | new CheckerPlugin() 29 | ], 30 | devtool: "source-map", 31 | devServer: { 32 | // open: true, 33 | content: path.join("/dist/client/") 34 | // dev: { publicPath: "/dist" } 35 | } 36 | }; -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/webpack-server.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const { 5 | CheckerPlugin 6 | } = require("awesome-typescript-loader"); 7 | const sharedConfig = require("./webpack-shared.config.js"); 8 | 9 | module.exports = { 10 | ...sharedConfig(true), 11 | target: "node", 12 | node: { 13 | __dirname: false, 14 | __filename: false, 15 | fs: "empty", 16 | net: "empty" 17 | }, 18 | entry: path.join(__dirname, "server", "index.ts"), 19 | output: { 20 | filename: "index.js", 21 | path: path.resolve(__dirname, "dist", "server") 22 | }, 23 | plugins: [new CheckerPlugin()], 24 | devtool: "source-map" 25 | }; -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/webpack-shared.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function (node) { 4 | return { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js?$/, 9 | use: { 10 | loader: "babel-loader", 11 | }, 12 | exclude: /node_modules/ 13 | }, 14 | { 15 | test: /\.css$/, 16 | use: ['css-loader'], 17 | exclude: /node_modules/ 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | use: { 22 | loader: "awesome-typescript-loader", 23 | options: { 24 | silent: process.argv.indexOf("--json") !== -1, 25 | useBabel: true, 26 | babelOptions: { 27 | plugins: node 28 | ? [ 29 | "react-imported-component/babel", 30 | "babel-plugin-dynamic-import-node" 31 | ] 32 | : ["react-imported-component/babel"] 33 | } 34 | } 35 | }, 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(jpg|jpeg|gif|png|ico)$/, 40 | exclude: /node_modules/, 41 | loader: "file-loader?name=img/[path][name].[ext]&context=./app/images" 42 | } 43 | ] 44 | }, 45 | resolve: { 46 | extensions: [".ts", ".tsx", ".js", ".css"], 47 | alias: { 48 | react: path.resolve(path.join(__dirname, './node_modules/react')), 49 | 'babel-core': path.resolve( 50 | path.join(__dirname, './node_modules/@babel/core'), 51 | ), 52 | }, 53 | }, 54 | mode: "development" 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/SSR/runtime-manifest/webpack.config.js: -------------------------------------------------------------------------------- 1 | const clientConfig = require("./webpack-client.config.js"); 2 | const serverConfig = require("./webpack-server.config.js"); 3 | 4 | module.exports = [clientConfig, serverConfig]; 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["react-imported-component/babel"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: debian:stretch 6 | steps: 7 | - checkout 8 | - run: 9 | name: Greeting 10 | command: echo Hello, world. 11 | - run: 12 | name: Print the Current Time 13 | command: date -------------------------------------------------------------------------------- /examples/SSR/typescript-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | dist 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/README.md: -------------------------------------------------------------------------------- 1 | Simple example with TS, React + imported-components using `renderToString` and `printDrainHydrateMarks` -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #EEE; 3 | } -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Home from "./components/Home"; 3 | import importedComponent, {ComponentLoader, loadableResource} from "react-imported-component"; 4 | 5 | const Another = importedComponent(() => import(/* webpackChunkName: namedChunk-0 */"./components/Another"), { 6 | LoadingComponent: () =>
    loading
    7 | }); 8 | const Other1 = importedComponent(() => import(/* webpackChunkName: "namedChunk-1" */"./components/Other")); 9 | const Other2 = importedComponent(() => import(/* webpackChunkName: "namedChunk-1" */"./components/OtherTween")); 10 | 11 | const AnotherWrapped = importedComponent( 12 | () => import(/* webpackChunkName: namedChunk-0 */"./components/Another"), { 13 | render(Component, state, props: { prop: number }) { 14 | if (state.loading) { 15 | return 16 | } 17 | return
    18 | } 19 | }); 20 | //import Another from "./components/Another"; 21 | 22 | // @ts-ignore 23 | // const importCss = () => im port("./App.css"); 24 | 25 | export default function App() { 26 | return ( 27 |
    28 | [not-trackable] 29 | import("./components/Another")} 31 | /> 32 | import("./components/Another"))} 34 | /> 35 | [/not-trackable] 36 | [home][/home] 37 | 38 | 39 | 40 | 41 | 42 | {0 && } 43 | 44 |
    45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theKashey/react-imported-component/359cb486a049a4151f64c2e5c7b34ad3851cd554/examples/SSR/typescript-react/app/assets/favicon.ico -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/client.tsx: -------------------------------------------------------------------------------- 1 | import { rehydrateMarks } from "react-imported-component"; 2 | import * as React from "react"; 3 | import ReactDOM from "react-dom"; 4 | import { BrowserRouter } from "react-router-dom"; 5 | import { Provider } from "react-redux"; 6 | import { configureStore } from "./store"; 7 | import "./imported"; // eslint-disable-line 8 | 9 | import App from "./App"; 10 | 11 | const preloadedState = window.__PRELOADED_STATE__; 12 | delete window.__PRELOADED_STATE__; 13 | 14 | const store = configureStore(preloadedState); 15 | 16 | const element = document.getElementById("app"); 17 | 18 | const app = ( 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | 26 | rehydrateMarks().then(() => { 27 | console.log(element.innerHTML); 28 | ReactDOM.hydrate(app, element); 29 | console.log(element.innerHTML); 30 | }); 31 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/Another.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const Another:React.SFC<{test: number, p2: number}> = () => Another!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {Helmet} from "react-helmet"; 3 | import {Link} from "react-router-dom"; 4 | import imported, {lazy, LazyBoundary, remapImports} from "react-imported-component"; 5 | 6 | const LZ = lazy(() => import("./LazyTarget")); 7 | 8 | const ND = imported( 9 | () => remapImports( 10 | import('./NotDefault'), 11 | ({NotDefault}) => NotDefault 12 | ) 13 | ); 14 | 15 | const Home = () => ( 16 |
    17 | 18 | Homepage! 19 | 20 | Link to Another 21 |
    Hello!
    22 | 23 | 24 | 25 | 26 |
    27 | ); 28 | export default Home; 29 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/LazyTarget.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const LazyTarget: React.SFC = () => I am not lazy Lazy; 4 | export default LazyTarget; 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/NotDefault.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import {useImported} from "react-imported-component" 3 | 4 | export const NotDefault: React.FC<{ p1: number }> = ({p1}) => { 5 | const { 6 | imported = x => `!${x}!`, 7 | } = useImported(() => import("./library"), (lib) => lib.magicFunction); 8 | 9 | return ( 10 | 11 | I am not not Default Export({p1} prop == {imported(p1)}) 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/Other.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Another:React.SFC<{test: number}> = () => Other!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/OtherTween.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const Another: React.SFC = () => Other Twin!; 4 | export default Another; 5 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/components/library.tsx: -------------------------------------------------------------------------------- 1 | console.log('loading library function'); 2 | 3 | export const magicFunction = (x: number) => `** ${x} **`; -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/imported.ts: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /* tslint:disable */ 4 | 5 | import {assignImportedComponents} from 'react-imported-component/boot'; 6 | 7 | const applicationImports = [ 8 | () => import('./components/Another'), 9 | () => import('./components/LazyTarget'), 10 | () => import('./components/NotDefault'), 11 | () => import('./components/library'), 12 | () => import(/* webpackChunkName: "namedChunk-1" */'./components/Other'), 13 | () => import(/* webpackChunkName: "namedChunk-1" */'./components/OtherTween'), 14 | ]; 15 | 16 | assignImportedComponents(applicationImports); 17 | export default applicationImports; -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { createStore } from "redux"; 2 | import { composeWithDevTools } from "redux-devtools-extension"; 3 | import rootReducer from "./reducers"; 4 | 5 | export function configureStore(initialState = {}) { 6 | const store = createStore(rootReducer, initialState, composeWithDevTools()); 7 | 8 | if (module.hot) { 9 | module.hot.accept("./reducers", () => { 10 | console.log("Replacing reducers..."); 11 | store.replaceReducer(require("./reducers").default); 12 | }); 13 | } 14 | 15 | return store; 16 | } 17 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/app/store/reducers.ts: -------------------------------------------------------------------------------- 1 | export default function rootReducer(state) { 2 | return state; 3 | } 4 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "generate-imported-component": "imported-components app app/imported.ts", 4 | "build": "yarn webpack", 5 | "start": "node dist/server/index.js", 6 | "cp:1": "cp -R ../../../dist ./node_modules/react-imported-component/", 7 | "cp:2": "cp ../../../package.json ./node_modules/react-imported-component", 8 | "cp:3": "cp ../../../boot ./node_modules/react-imported-component", 9 | "cp": "yarn cp:1 && yarn cp:2 && yarn cp:3" 10 | }, 11 | "dependencies": { 12 | "@types/cheerio": "^0.22.7", 13 | "@types/react": "^16.9.2", 14 | "@types/redux-devtools-extension": "^2.13.2", 15 | "@types/webpack-env": "^1.13.6", 16 | "awesome-typescript-loader": "^5.0.0", 17 | "babel-plugin-dynamic-import-node": "^1.2.0", 18 | "cheerio": "^1.0.0-rc.2", 19 | "compression": "^1.7.2", 20 | "express": "^4.16.3", 21 | "file-loader": "^1.1.11", 22 | "html-webpack-plugin": "^3.2.0", 23 | "react": "^16.9.0", 24 | "react-dom": "^16.9.0", 25 | "react-helmet": "^5.2.0", 26 | "react-imported-component": "^6.1.1", 27 | "react-redux": "^5.0.7", 28 | "react-router-dom": "^4.2.2", 29 | "redux": "^4.0.0", 30 | "redux-devtools-extension": "^2.13.2", 31 | "ts-loader": "^4.2.0", 32 | "typescript": "^2.8.3", 33 | "webpack-serve": "^1.0.2" 34 | }, 35 | "devDependencies": { 36 | "babel-core": "^6.21.0", 37 | "babel-loader": "^8.0.5", 38 | "babel-preset-es2015": "^6.18.0", 39 | "css-loader": "^2.1.0", 40 | "style-loader": "^1.0.0", 41 | "webpack": "^4.29.0", 42 | "webpack-cli": "^3.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/server/generateHtml.ts: -------------------------------------------------------------------------------- 1 | // Generate the HTML using index.html as a template 2 | 3 | import fs from "fs"; 4 | import path from "path"; 5 | import cheerio from "cheerio"; 6 | import { Helmet } from "react-helmet"; 7 | import { printDrainHydrateMarks } from "react-imported-component"; 8 | 9 | // The path is relative from server bundle to client bundle, not the source 10 | const templatePath = path.join(__dirname, "..", "client", "index.html"); 11 | const HTML_TEMPLATE = fs.readFileSync(templatePath).toString(); 12 | 13 | export default function generateHtml(markup, state, getStream) { 14 | // Get the serer-rendering values for the 15 | const helmet = Helmet.renderStatic(); 16 | 17 | const $template = cheerio.load(HTML_TEMPLATE); 18 | $template("head").append( 19 | helmet.title.toString() + helmet.meta.toString() + helmet.link.toString() 20 | ); 21 | $template("head").append( 22 | `` 25 | ); 26 | $template("head").append(printDrainHydrateMarks(getStream())); 27 | $template("#app").html(markup); 28 | 29 | return $template.html(); 30 | } 31 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/server/index.ts: -------------------------------------------------------------------------------- 1 | import compression from "compression"; 2 | import express from "express"; 3 | import middleware from "./middleware"; 4 | 5 | const app = express(); 6 | 7 | // Add gzip compression to responses 8 | app.use(compression()); 9 | 10 | // Expose the public directory as /dist and point to the browser version 11 | app.use(express.static(`${__dirname}/../client`, { index: false })); 12 | 13 | // Anything unresolved is serving the application and let 14 | // react-router do the routing! 15 | app.get("/*", middleware); 16 | 17 | // Check for PORT environment variable, otherwise fallback on Parcel default port 18 | const port = process.env.PORT || 1234; 19 | app.listen(port, () => { 20 | console.log(`Listening on port ${port}...`); 21 | }); 22 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/server/middleware.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/server"; 3 | import {StaticRouter} from "react-router-dom"; 4 | import {Provider} from "react-redux"; 5 | 6 | import "../app/imported"; 7 | import App from "../app/App"; 8 | import {configureStore} from "../app/store"; 9 | 10 | import generateHtml from "./generateHtml"; 11 | import {ImportedStream} from "react-imported-component"; 12 | import {createLoadableStream} from "react-imported-component/server"; 13 | 14 | const store = configureStore(); 15 | 16 | export default (req, res) => { 17 | // Generate the server-rendered HTML using the appropriate router 18 | const context: { url?: string } = {}; 19 | let streamId = createLoadableStream(); 20 | const router = ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ); 29 | const markup = ReactDOM.renderToString(router); 30 | 31 | // If react-router is redirecting, do it on the server side 32 | if (context.url) { 33 | res.redirect(301, context.url); 34 | } else { 35 | // Format the HTML using the template and send the result 36 | 37 | // stream is not defined unless we call the render 38 | const html = generateHtml(markup, store.getState(), () => streamId); 39 | res.send(html); 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "module": "esnext", 6 | "lib": ["dom", "es2015", "es2015.promise", "es2016"], 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "removeComments": false, 14 | "preserveConstEnums": true, 15 | "sourceMap": true, 16 | "skipLibCheck": true, 17 | "baseUrl": "." 18 | }, 19 | "files": ["types.d.ts"] 20 | } 21 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": ["tslint-config-airbnb", "tslint-react", "tslint-config-prettier"], 4 | "jsRules": {}, 5 | "rules": { 6 | "variable-name": false, 7 | "function-name": false, 8 | "jsx-no-lambda": false, 9 | "import-name": false 10 | }, 11 | "rulesDirectory": [] 12 | } 13 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.ico"; 2 | 3 | interface Window { 4 | ___REACT_DEFERRED_COMPONENT_MARKS: any; 5 | __PRELOADED_STATE__: any; 6 | } 7 | 8 | declare var window: Window; 9 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/webpack-client.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 3 | const { 4 | CheckerPlugin 5 | } = require("awesome-typescript-loader"); 6 | const sharedConfig = require("./webpack-shared.config.js"); 7 | 8 | module.exports = { 9 | ...sharedConfig(false), 10 | entry: path.join(__dirname, "app", "client.tsx"), 11 | output: { 12 | // publicPath: "/dist", 13 | filename: "client.js", 14 | path: path.resolve(__dirname, "dist", "client"), 15 | chunkFilename: '[name].client.js', 16 | }, 17 | plugins: [ 18 | new HtmlWebpackPlugin({ 19 | hash: true, 20 | filename: path.resolve(__dirname, "dist", "client", "index.html"), 21 | template: path.resolve(__dirname, "app", "index.html") 22 | }), 23 | new CheckerPlugin() 24 | ], 25 | devtool: "source-map", 26 | devServer: { 27 | // open: true, 28 | content: path.join("/dist/client/") 29 | // dev: { publicPath: "/dist" } 30 | } 31 | }; -------------------------------------------------------------------------------- /examples/SSR/typescript-react/webpack-server.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const webpack = require("webpack"); 3 | const HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | const { 5 | CheckerPlugin 6 | } = require("awesome-typescript-loader"); 7 | const sharedConfig = require("./webpack-shared.config.js"); 8 | 9 | module.exports = { 10 | ...sharedConfig(true), 11 | target: "node", 12 | node: { 13 | __dirname: false, 14 | __filename: false, 15 | fs: "empty", 16 | net: "empty" 17 | }, 18 | entry: path.join(__dirname, "server", "index.ts"), 19 | output: { 20 | filename: "index.js", 21 | path: path.resolve(__dirname, "dist", "server") 22 | }, 23 | plugins: [new CheckerPlugin()], 24 | devtool: "source-map" 25 | }; -------------------------------------------------------------------------------- /examples/SSR/typescript-react/webpack-shared.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = function (node) { 4 | return { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.js?$/, 9 | use: { 10 | loader: "babel-loader", 11 | }, 12 | exclude: /node_modules/ 13 | }, 14 | { 15 | test: /\.css$/, 16 | use: ['css-loader'], 17 | exclude: /node_modules/ 18 | }, 19 | { 20 | test: /\.tsx?$/, 21 | use: { 22 | loader: "awesome-typescript-loader", 23 | options: { 24 | silent: process.argv.indexOf("--json") !== -1, 25 | useBabel: true, 26 | babelOptions: { 27 | plugins: node 28 | ? [ 29 | "react-imported-component/babel", 30 | "babel-plugin-dynamic-import-node" 31 | ] 32 | : ["react-imported-component/babel"] 33 | } 34 | } 35 | }, 36 | exclude: /node_modules/ 37 | }, 38 | { 39 | test: /\.(jpg|jpeg|gif|png|ico)$/, 40 | exclude: /node_modules/, 41 | loader: "file-loader?name=img/[path][name].[ext]&context=./app/images" 42 | } 43 | ] 44 | }, 45 | resolve: { 46 | extensions: [".ts", ".tsx", ".js", ".css"], 47 | alias: { 48 | react: path.resolve(path.join(__dirname, './node_modules/react')), 49 | 'babel-core': path.resolve( 50 | path.join(__dirname, './node_modules/@babel/core'), 51 | ), 52 | }, 53 | }, 54 | mode: "development" 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /examples/SSR/typescript-react/webpack.config.js: -------------------------------------------------------------------------------- 1 | const clientConfig = require("./webpack-client.config.js"); 2 | const serverConfig = require("./webpack-server.config.js"); 3 | 4 | module.exports = [clientConfig, serverConfig]; 5 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ], 6 | "plugins": ["react-imported-component/babel"] 7 | } -------------------------------------------------------------------------------- /examples/hybrid/react-snap/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .cache 4 | dist 5 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/README.md: -------------------------------------------------------------------------------- 1 | react-snap-example 2 | ==== 3 | 4 | Using `react-imported-component` with `react-snap` without a flash of unloaded content. 5 | 6 | working out of the box 7 | ```js 8 | import imported, {rehydrateMarks, drainHydrateMarks} from 'react-imported-component'; 9 | // import chunk definition 10 | import './imported-chunk'; 11 | 12 | const Component1 = imported(() => import('./splitted-1')); 13 | 14 | // wait for all the used marks to load 15 | rehydrateMarks(window.__IMPORTED_COMPONENTS__).then(() => { 16 | ReactDOM.render(, document.getElementById('app')); 17 | }); 18 | 19 | // save used marks 20 | window.snapSaveState = () => ({ 21 | "__IMPORTED_COMPONENTS__": drainHydrateMarks(), 22 | }); 23 | ``` -------------------------------------------------------------------------------- /examples/hybrid/react-snap/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-snap", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "parcel src/index.html", 8 | "build-imported": "imported-components src src/imported-chunk.js", 9 | "build": "rm -rf dist && npm run build-imported && npm run build-client", 10 | "build-client": "parcel build src/index.html -d dist/ --public-url ./ --no-minify --no-cache", 11 | "postbuild": "react-snap" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "dependencies": { 16 | "react": "^16.8.4", 17 | "react-dom": "^16.8.4", 18 | "react-imported-component": "^5.5.3", 19 | "react-snap": "^1.23.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.3.4", 23 | "parcel-bundler": "^1.12.2" 24 | }, 25 | "reactSnap": { 26 | "source": "dist" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/app.js: -------------------------------------------------------------------------------- 1 | import imported, {rehydrateMarks, drainHydrateMarks} from 'react-imported-component'; 2 | import React from 'react'; 3 | import ReactDOM from 'react-dom'; 4 | // import chunk definition 5 | import './imported-chunk'; 6 | 7 | const Component1 = imported(() => import('./splitted-1')); 8 | const Component2 = imported(() => import('./splitted-2')); 9 | 10 | function hereTime(prefix) { 11 | var perfData = window.performance.timing; 12 | var pageLoadTime = Date.now() - perfData.navigationStart; 13 | console.log(prefix, pageLoadTime); 14 | } 15 | 16 | hereTime('enter'); 17 | // wait for all the used marks to load 18 | rehydrateMarks(window.__IMPORTED_COMPONENTS__).then(() => { 19 | hereTime('start'); 20 | ReactDOM.render( 21 |
    22 | APP 23 |

    24 | first component -> 25 |

    26 |

    27 | second component -> 28 |

    29 |
    30 | , document.getElementById('app')); 31 | }); 32 | 33 | // save used marks 34 | window.snapSaveState = () => ({ 35 | "__IMPORTED_COMPONENTS__": drainHydrateMarks(), 36 | }); 37 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/imported-chunk.js: -------------------------------------------------------------------------------- 1 | 2 | /* eslint-disable */ 3 | /* tslint:disable */ 4 | 5 | import {assignImportedComponents} from 'react-imported-component'; 6 | 7 | const applicationImports = [ 8 | () => import('./splitted-1'), 9 | () => import('./splitted-2'), 10 | () => import('./splitted-3'), 11 | ]; 12 | 13 | assignImportedComponents(applicationImports); 14 | export default applicationImports; -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
    11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/main.js: -------------------------------------------------------------------------------- 1 | import {rehydrateMarks} from 'react-imported-component/dist/es2015/marks'; 2 | import './imported-chunk'; 3 | import './app'; 4 | 5 | function hereTime(prefix) { 6 | var perfData = window.performance.timing; 7 | var pageLoadTime = Date.now() - perfData.navigationStart; 8 | console.log(prefix, pageLoadTime); 9 | } 10 | 11 | hereTime('root'); 12 | 13 | rehydrateMarks(window.__IMPORTED_COMPONENTS__).then(() => hereTime('chunks loaded')); 14 | 15 | -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/splitted-1.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => This is code - splitted component1 -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/splitted-2.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {lazy} from "react-imported-component"; 3 | 4 | const Component3 = lazy(() => import('./splitted-3')); 5 | 6 | export default () => ( 7 | 8 | This is code - splitted component 2, with 9 | - 10 | 11 | - 12 | inside Suspense 13 | 14 | ) -------------------------------------------------------------------------------- /examples/hybrid/react-snap/src/splitted-3.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => Component3 -------------------------------------------------------------------------------- /examples/react-hot-loader/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "env", 4 | "react" 5 | ], 6 | "plugins": [ 7 | "react-hot-loader/babel", 8 | "transform-class-properties", 9 | "syntax-dynamic-import", 10 | "react-imported-component/babel" 11 | // "dynamic-import-node" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-hot-loader/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [lints] 8 | 9 | [options] 10 | include_warnings=true 11 | 12 | [strict] 13 | -------------------------------------------------------------------------------- /examples/react-hot-loader/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /examples/react-hot-loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot-async-portals", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "start": "NODE_ENV=development webpack-dev-server --hot" 7 | }, 8 | "devDependencies": { 9 | "babel-core": "^6.26.0", 10 | "babel-loader": "^8.0.5", 11 | "babel-plugin-transform-class-properties": "^6.24.1", 12 | "babel-preset-env": "^1.6.1", 13 | "babel-preset-react": "^6.24.1", 14 | "flow-bin": "^0.110.0", 15 | "html-webpack-plugin": "^3.2.0", 16 | "webpack": "^4.21.0", 17 | "webpack-cli": "^3.1.2", 18 | "webpack-dev-server": "^3.1.9" 19 | }, 20 | "dependencies": { 21 | "react": "^16.9.0", 22 | "react-dom": "^16.9.0", 23 | "react-hot-loader": "^4.12.12", 24 | "react-imported-component": "^6.1.1", 25 | "react-portal": "^4.1.2" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/App.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | import {hot, setConfig} from 'react-hot-loader' 3 | import * as React from 'react' 4 | import Counter from './Counter' 5 | import imported, {lazy, ComponentLoader, printDrainHydrateMarks} from 'react-imported-component' 6 | import Portal from './Portal' 7 | import Indirect from './indirectUsage'; 8 | 9 | imported(() => import(/* webpackChunkName: "namedChunk-1" */'./DeferredRender'), { 10 | async: true 11 | }); 12 | 13 | const Async = imported(() => import(/* webpackChunkName: "namedChunk-1" */'./DeferredRender')); 14 | const Async2 = lazy(() => { 15 | console.log('loading lazy'); 16 | return import(/* webpackChunkName: "namedChunk-2" */'./Lazy') 17 | }); 18 | const ShouldNotBeImported = imported(() => import(/* webpackChunkName: "namedChunk-2" */'./NotImported')); 19 | 20 | const App = () => ( 21 |

    22 |

    Component loaded

    23 | import(/* webpackChunkName: "namedChunk-1" */'./DeferredRender')} 25 | /> 26 |

    test!

    27 |
    28 |

    C:

    29 |
    30 |

    Imported:

    31 |
    32 | 33 |

    Lazy:

    34 |
    35 |
    36 |

    Portal:

    37 |
    38 | 39 | {Date.now() < 0 && } 40 |

    41 | ) 42 | 43 | setConfig({logLevel: 'debug'}) 44 | 45 | setTimeout(() => console.log('marks', printDrainHydrateMarks()), 1000); 46 | 47 | export default hot(module)(App) 48 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/Counter.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | // const RAND = Math.round(Math.random() * 1000) 4 | 5 | class Counter extends React.Component { 6 | state = { count: Math.round(Math.random() * 1000) }; 7 | gen = 0; 8 | 9 | render() { 10 | // gen should change. count - no. 11 | return ( 12 | 13 | state({this.state.count}):rerender({this.gen++}) 14 | 15 | ) 16 | } 17 | } 18 | 19 | export default Counter 20 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/DeferredRender.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import imported, {remapImports} from 'react-imported-component'; 3 | import {Portal} from 'react-portal' 4 | import hidden from './HiddenComponent' 5 | 6 | const V = 2; 7 | console.log('run ', V); 8 | const SubAsync = imported(() => { 9 | console.log('import ', V); 10 | return remapImports(import('./SubAsync'), x => x.SubAsync) 11 | }); 12 | 13 | const Hidden = hidden(); 14 | 15 | const APortal = () => ( 16 | 17 | This is a async portal 18 | 19 | 20 | ); 21 | 22 | export default () => ( 23 |
    24 | ASYNC 77 25 | 26 | and 27 | + 28 |
    29 | ); 30 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/HiddenComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Counter from './Counter' 3 | 4 | const hidden = function() { 5 | return { 6 | counter: () => ( 7 |
    8 | this is hidden counter() 9 |
    10 | ), 11 | } 12 | } 13 | 14 | export default hidden 15 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/Lazy.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import hidden from './HiddenComponent' 3 | 4 | const Hidden = hidden(); 5 | 6 | const N = 240; 7 | console.log('execute lazy', N); 8 | 9 | export default () => ( 10 |
    11 | Lazy {N} 12 | 13 |
    14 | ) 15 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/NotImported.js: -------------------------------------------------------------------------------- 1 | alert('this file should not be imported!'); 2 | debugger; -------------------------------------------------------------------------------- /examples/react-hot-loader/src/Portal.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Portal } from 'react-portal' 3 | import hidden from './HiddenComponent' 4 | 5 | const Hidden = hidden() 6 | 7 | const InPortal = ({ children }) =>
    + {children}
    8 | 9 | export default () => ( 10 | 11 | 12 | This is a first portal 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/SubAsync.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | console.log('sub async 4'); 4 | 5 | export const SubAsync = () => ( 6 |
    7 | Async In Async! 8 |
    9 | ); 10 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | import App from './App' 4 | 5 | const root = document.createElement('div') 6 | document.body.appendChild(root) 7 | 8 | render(, root) 9 | -------------------------------------------------------------------------------- /examples/react-hot-loader/src/indirect.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import importedComponent from 'react-imported-component' 3 | import Counter from './Counter' 4 | 5 | const DefaultLoading = () => ; 6 | 7 | export const makeSplitPoint = fn => importedComponent(fn, {LoadingComponent: DefaultLoading}) -------------------------------------------------------------------------------- /examples/react-hot-loader/src/indirectTarget.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () =>
    Indirect Target
    -------------------------------------------------------------------------------- /examples/react-hot-loader/src/indirectUsage.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {makeSplitPoint} from './indirect' 3 | 4 | const AsyncComponent = makeSplitPoint(() => import('./indirectTarget')) 5 | 6 | export default () => ; -------------------------------------------------------------------------------- /examples/react-hot-loader/webpack.config.babel.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path') 3 | const webpack = require('webpack') 4 | const HtmlWebpackPlugin = require('html-webpack-plugin') 5 | 6 | module.exports = { 7 | entry: ['./src/index'], 8 | mode: "development", 9 | output: { 10 | path: path.join(__dirname, 'dist'), 11 | filename: 'bundle.js', 12 | chunkFilename: '[name].client.js', 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.js$/, 18 | include: /node_modules\/react-dom/, 19 | use: ['react-hot-loader/webpack'], 20 | }, 21 | { 22 | exclude: /node_modules|packages/, // should work without exclude 23 | test: /\.js$/, 24 | use: 'babel-loader', 25 | }, 26 | ], 27 | }, 28 | resolve: { 29 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 30 | alias: { 31 | react: path.resolve(path.join(__dirname, './node_modules/react')), 32 | 'babel-core': path.resolve( 33 | path.join(__dirname, './node_modules/@babel/core'), 34 | ), 35 | }, 36 | }, 37 | plugins: [new HtmlWebpackPlugin(), new webpack.NamedModulesPlugin()], 38 | } 39 | -------------------------------------------------------------------------------- /greenkeeper.json: -------------------------------------------------------------------------------- 1 | { 2 | "groups": { 3 | "default": { 4 | "packages": [ 5 | "examples/SSR/parcel-react-ssr/package.json", 6 | "examples/SSR/typescript-react/package.json", 7 | "examples/react-hot-loader/package.json", 8 | "package.json" 9 | ] 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testMatch: ['**/__tests__/*.spec.(ts|tsx)'], 4 | }; 5 | -------------------------------------------------------------------------------- /macro/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/macro.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/macro.js", 5 | "module": "../dist/es2015/entrypoints/macro.js", 6 | "types": "../dist/es2015/entrypoints/macro.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-imported-component", 3 | "version": "6.5.4", 4 | "description": "I will import your component, and help to handle it", 5 | "main": "dist/es5/entrypoints/index.js", 6 | "jsnext:main": "dist/es2015/entrypoints/index.js", 7 | "module": "dist/es2015/entrypoints/index.js", 8 | "module:es2019": "dist/es2019/entrypoints/index.js", 9 | "types": "dist/es5/entrypoints/index.d.ts", 10 | "sideEffects": false, 11 | "scripts": { 12 | "build:ci": "lib-builder build && yarn size", 13 | "build": "lib-builder build && yarn size && yarn size:report", 14 | "test": "jest", 15 | "prepublish": "yarn build && yarn changelog", 16 | "lint": "lib-builder lint", 17 | "dev": "lib-builder dev", 18 | "test:ci": "jest --runInBand --coverage", 19 | "release": "yarn build && yarn test", 20 | "format": "lib-builder format", 21 | "size": "npx size-limit", 22 | "size:report": "npx size-limit --json > .size.json", 23 | "update": "lib-builder update", 24 | "docz:dev": "docz dev", 25 | "docz:build": "docz build", 26 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s", 27 | "changelog:rewrite": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/theKashey/react-hot-component-loader.git" 32 | }, 33 | "bin": { 34 | "imported-components": "./bin/imported-components" 35 | }, 36 | "keywords": [ 37 | "react-hot-loader", 38 | "loader", 39 | "import", 40 | "async", 41 | "ssr", 42 | "code splitting", 43 | "hmr" 44 | ], 45 | "peerDependencies": { 46 | "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", 47 | "react": "^16.9.0 || ^17.0.0 || ^18.0.0" 48 | }, 49 | "peerDependenciesMeta": { 50 | "@types/react": { 51 | "optional": true 52 | } 53 | }, 54 | "author": "theKashey ", 55 | "license": "MIT", 56 | "bugs": { 57 | "url": "https://github.com/theKashey/react-hot-component-loader/issues" 58 | }, 59 | "homepage": "https://github.com/theKashey/react-hot-component-loader#readme", 60 | "devDependencies": { 61 | "@theuiteam/lib-builder": "^0.1.4", 62 | "@size-limit/preset-small-lib": "^4.5.1", 63 | "@babel/core": "^7.5.5", 64 | "@babel/preset-react": "^7.0.0", 65 | "@babel/runtime": "^7.3.1", 66 | "@babel/plugin-syntax-dynamic-import": "^7.5.5", 67 | "babel-plugin-dynamic-import-node": "^2.3.0", 68 | "babel-plugin-tester": "^7.0.1" 69 | }, 70 | "dependencies": { 71 | "babel-plugin-macros": "^2.6.1", 72 | "crc-32": "^1.2.0", 73 | "detect-node-es": "^1.0.0", 74 | "scan-directory": "^2.0.0", 75 | "tslib": "^2.0.0" 76 | }, 77 | "engines": { 78 | "node": ">=10" 79 | }, 80 | "files": [ 81 | "bin", 82 | "dist", 83 | "boot", 84 | "server", 85 | "macro", 86 | "babel.js", 87 | "wrapper.js" 88 | ], 89 | "husky": { 90 | "hooks": { 91 | "pre-commit": "lint-staged" 92 | } 93 | }, 94 | "lint-staged": { 95 | "*.{ts,tsx}": [ 96 | "prettier --write", 97 | "eslint --fix", 98 | "git add" 99 | ], 100 | "*.{js,css,json,md}": [ 101 | "prettier --write", 102 | "git add" 103 | ] 104 | }, 105 | "prettier": { 106 | "printWidth": 120, 107 | "trailingComma": "es5", 108 | "tabWidth": 2, 109 | "semi": true, 110 | "singleQuote": true 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "main": "../dist/es5/entrypoints/server.js", 4 | "jsnext:main": "../dist/es2015/entrypoints/server.js", 5 | "module": "../dist/es2015/entrypoints/server.js", 6 | "types": "../dist/es2015/entrypoints/server.d.ts" 7 | } 8 | -------------------------------------------------------------------------------- /src/babel/babel.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { existsSync } from 'fs'; 3 | import { dirname, join, relative, resolve } from 'path'; 4 | 5 | import * as crc32 from 'crc-32'; 6 | 7 | import { ImportedConfiguration } from '../configuration/configuration'; 8 | import { processComment } from './magic-comments'; 9 | 10 | export const encipherImport = (str: string) => { 11 | return crc32.str(str).toString(32); 12 | }; 13 | 14 | // Babel v7 compat 15 | let syntax: any; 16 | 17 | try { 18 | syntax = require('babel-plugin-syntax-dynamic-import'); 19 | } catch (err) { 20 | try { 21 | syntax = require('@babel/plugin-syntax-dynamic-import'); 22 | } catch (e) { 23 | throw new Error( 24 | 'react-imported-component babel plugin is requiring `babel-plugin-syntax-dynamic-import` or `@babel/plugin-syntax-dynamic-import` to work. Please add this dependency.' 25 | ); 26 | } 27 | } 28 | 29 | syntax = syntax.default || syntax; 30 | 31 | const resolveImport = (importName: string, file = '') => { 32 | if (importName.charAt(0) === '.') { 33 | return relative(process.cwd(), resolve(dirname(file), importName)); 34 | } 35 | 36 | return importName; 37 | }; 38 | 39 | const templateOptions = { 40 | placeholderPattern: /^([A-Z0-9]+)([A-Z0-9_]+)$/, 41 | }; 42 | 43 | function getImportArg(callPath: any) { 44 | return callPath.get('arguments.0'); 45 | } 46 | 47 | function getComments(callPath: any) { 48 | return callPath.has('leadingComments') ? callPath.get('leadingComments') : []; 49 | } 50 | 51 | // load configuration 52 | const configurationFile = join(process.cwd(), '.imported.js'); 53 | const defaultConfiguration: ImportedConfiguration = ( 54 | existsSync(configurationFile) ? require(configurationFile) : {} 55 | ) as ImportedConfiguration; 56 | 57 | export const createTransformer = ( 58 | { types: t, template }: any, 59 | excludeMacro = false, 60 | configuration = defaultConfiguration 61 | ) => { 62 | const headerTemplate = template( 63 | `var importedWrapper = require('react-imported-component/wrapper');`, 64 | templateOptions 65 | ); 66 | 67 | const importRegistration = template('importedWrapper(MARK, IMPORT)', templateOptions); 68 | 69 | const hasImports = new Set(); 70 | const visitedNodes = new Map(); 71 | 72 | return { 73 | traverse(programPath: any, fileName: string) { 74 | let isBootstrapFile = false; 75 | 76 | programPath.traverse({ 77 | ImportDeclaration(path: any) { 78 | if (excludeMacro) { 79 | return; 80 | } 81 | 82 | const source = path.node.source.value; 83 | 84 | if (source === 'react-imported-component/macro') { 85 | const { specifiers } = path.node; 86 | path.remove(); 87 | 88 | const assignName = 'assignImportedComponents'; 89 | 90 | if (specifiers.length === 1 && specifiers[0].imported.name === assignName) { 91 | isBootstrapFile = true; 92 | 93 | programPath.node.body.unshift( 94 | t.importDeclaration( 95 | [t.importSpecifier(t.identifier(assignName), t.identifier(assignName))], 96 | t.stringLiteral('react-imported-component/boot') 97 | ) 98 | ); 99 | } else { 100 | programPath.node.body.unshift( 101 | t.importDeclaration( 102 | specifiers.map((spec: any) => 103 | t.importSpecifier(t.identifier(spec.imported.name), t.identifier(spec.imported.name)) 104 | ), 105 | t.stringLiteral('react-imported-component') 106 | ) 107 | ); 108 | } 109 | } 110 | }, 111 | Import({ parentPath }: any) { 112 | if (visitedNodes.has(parentPath.node)) { 113 | return; 114 | } 115 | 116 | const newImport = parentPath.node; 117 | const rawImport = getImportArg(parentPath); 118 | const importName = rawImport.node.value; 119 | const rawComments = getComments(rawImport); 120 | const comments = rawComments.map((parent: any) => parent.node.value); 121 | 122 | const newComments = processComment(configuration, comments, importName, fileName, { 123 | isBootstrapFile, 124 | }); 125 | 126 | if (newComments !== comments) { 127 | rawComments.forEach((comment: any) => comment.remove()); 128 | 129 | newComments.forEach((comment: string) => { 130 | rawImport.addComment('leading', ` ${comment} `); 131 | }); 132 | } 133 | 134 | if (!importName) { 135 | return; 136 | } 137 | 138 | const requiredFileHash = encipherImport(resolveImport(importName, fileName)); 139 | 140 | let replace = null; 141 | 142 | replace = importRegistration({ 143 | MARK: t.stringLiteral(`imported_${requiredFileHash}_component`), 144 | IMPORT: newImport, 145 | }); 146 | 147 | hasImports.add(fileName); 148 | visitedNodes.set(newImport, true); 149 | 150 | parentPath.replaceWith(replace); 151 | }, 152 | }); 153 | }, 154 | 155 | finish(node: any, filename: string) { 156 | if (!hasImports.has(filename)) { 157 | return; 158 | } 159 | 160 | node.body.unshift(headerTemplate()); 161 | }, 162 | 163 | hasImports, 164 | }; 165 | }; 166 | 167 | export const babelPlugin = (babel: any, options: ImportedConfiguration = {}) => { 168 | const transformer = createTransformer(babel, false, { 169 | ...defaultConfiguration, 170 | ...options, 171 | }); 172 | 173 | return { 174 | inherits: syntax, 175 | 176 | visitor: { 177 | Program: { 178 | enter(programPath: any, { file }: any) { 179 | transformer.traverse(programPath, file.opts.filename); 180 | }, 181 | 182 | exit({ node }: any, { file }: any) { 183 | transformer.finish(node, file.opts.filename); 184 | }, 185 | }, 186 | }, 187 | }; 188 | }; 189 | -------------------------------------------------------------------------------- /src/babel/magic-comments.ts: -------------------------------------------------------------------------------- 1 | import { ImportedConfiguration, KnownImportOptions } from '../configuration/configuration'; 2 | import { commentsToConfiguration } from './utils'; 3 | 4 | const preservePrefetch = (_: any, __: any, options: KnownImportOptions) => !!options.webpackPrefetch; 5 | const preservePreload = (_: any, __: any, options: KnownImportOptions) => !!options.webpackPreload; 6 | const preserveChunkName = (_: any, __: any, options: KnownImportOptions) => 7 | options.webpackChunkName || options.chunkName; 8 | 9 | const chunkComment = (chunk: string) => `webpackChunkName: "${chunk}"`; 10 | const preloadComment = () => `webpackPreload: true`; 11 | const prefetchComment = () => `webpackPrefetch: true`; 12 | 13 | const knownMagics = ['webpackChunkName', 'webpackPrefetch', 'webpackPreload']; 14 | 15 | const toComments = >(conf: T): string[] => 16 | (Object.keys(conf) as Array) 17 | .filter((key) => !knownMagics.includes(key as any)) 18 | .reduce((acc, key) => { 19 | acc.concat(`${key}:${JSON.stringify(conf[key])}`); 20 | 21 | return acc; 22 | }, [] as string[]); 23 | 24 | const nullish = (a: T, b: T): T => { 25 | if (a === undefined) { 26 | return b; 27 | } 28 | 29 | return a; 30 | }; 31 | 32 | export const processComment = ( 33 | configuration: ImportedConfiguration, 34 | comments: string[], 35 | importName: string, 36 | fileName: string, 37 | options: { 38 | isBootstrapFile: boolean; 39 | } 40 | ): string[] => { 41 | const { 42 | shouldPrefetch = preservePrefetch, 43 | shouldPreload = preservePreload, 44 | chunkName = preserveChunkName, 45 | } = configuration; 46 | 47 | const importConfiguration = commentsToConfiguration(comments); 48 | 49 | const newChunkName = nullish( 50 | chunkName(importName, fileName, importConfiguration), 51 | preserveChunkName(importName, fileName, importConfiguration) 52 | ); 53 | const { isBootstrapFile } = options; 54 | 55 | return [ 56 | ...toComments(importConfiguration), 57 | !isBootstrapFile && shouldPrefetch(importName, fileName, importConfiguration) ? prefetchComment() : '', 58 | !isBootstrapFile && shouldPreload(importName, fileName, importConfiguration) ? preloadComment() : '', 59 | newChunkName ? chunkComment(newChunkName) : '', 60 | ].filter((x) => !!x); 61 | }; 62 | -------------------------------------------------------------------------------- /src/babel/utils.ts: -------------------------------------------------------------------------------- 1 | import vm from 'vm'; 2 | 3 | import { CLIENT_SIDE_ONLY } from '../configuration/constants'; 4 | 5 | const parseMagicComments = (str: string): Record => { 6 | if (str.trim() === CLIENT_SIDE_ONLY) { 7 | return {}; 8 | } 9 | 10 | try { 11 | const values = vm.runInNewContext(`(function(){return {${str}};})()`); 12 | 13 | return values; 14 | } catch (e) { 15 | return {}; 16 | } 17 | }; 18 | 19 | export const commentsToConfiguration = (comments: any[]): Record => 20 | comments.reduce( 21 | (acc, comment) => ({ 22 | ...acc, 23 | ...parseMagicComments(comment), 24 | }), 25 | {} as any 26 | ); 27 | -------------------------------------------------------------------------------- /src/configuration/config.ts: -------------------------------------------------------------------------------- 1 | import { isBackend } from '../utils/detectBackend'; 2 | 3 | const rejectNetwork = (url: string): boolean => url.indexOf('http') !== 0; 4 | 5 | /** 6 | * client-side only imported settings 7 | */ 8 | export interface ImportedClientSettings { 9 | /** 10 | * enabled hot module replacement 11 | * @autoconfig enabled if HMR is detected 12 | */ 13 | hot: boolean; 14 | /** 15 | * Sets SSR mode 16 | * @autoconfig autodetects environment 17 | */ 18 | SSR: boolean; 19 | /** 20 | * rethrows errors from loading 21 | * @autoconfig enabled in development 22 | */ 23 | rethrowErrors: boolean; 24 | /** 25 | * Controls which imports should be controlled via imported 26 | * @default - everything non http 27 | */ 28 | fileFilter: (url: string) => boolean; 29 | /** 30 | * Controls import signature matching 31 | * - true(default): checks signatures 32 | * - false: uses "marks"(file names) only 33 | */ 34 | checkSignatures: boolean; 35 | } 36 | 37 | const localSettings: ImportedClientSettings = { 38 | hot: typeof module !== 'undefined' && (!!module as any).hot, 39 | SSR: isBackend, 40 | rethrowErrors: process.env.NODE_ENV !== 'production', 41 | fileFilter: rejectNetwork, 42 | checkSignatures: true, 43 | }; 44 | 45 | export const settings: ImportedClientSettings = { 46 | get hot() { 47 | return localSettings.hot; 48 | }, 49 | get SSR() { 50 | return localSettings.SSR; 51 | }, 52 | get rethrowErrors() { 53 | return localSettings.rethrowErrors; 54 | }, 55 | get fileFilter() { 56 | return localSettings.fileFilter; 57 | }, 58 | get checkSignatures() { 59 | return localSettings.checkSignatures; 60 | }, 61 | }; 62 | 63 | /** 64 | * allows fine tune imported logic 65 | * client side only! 66 | * @internal 67 | * @see configuration via imported.json {@link https://github.com/theKashey/react-imported-component#importedjs} 68 | */ 69 | export const setConfiguration = (config: Partial): void => { 70 | Object.assign(localSettings, config); 71 | }; 72 | -------------------------------------------------------------------------------- /src/configuration/configuration.ts: -------------------------------------------------------------------------------- 1 | import { ImportedClientSettings } from './config'; 2 | 3 | export interface KnownImportOptions { 4 | chunkName?: string; 5 | webpackChunkName?: string; 6 | 7 | webpackPreload?: boolean; 8 | webpackPrefetch?: boolean; 9 | } 10 | 11 | type ImportOptions = KnownImportOptions | Record; 12 | 13 | /** 14 | * @name ImportedConfiguration 15 | * react-imported-component configuration 16 | * __TO BE USED AT `imported.js`__ 17 | * @see {@link https://github.com/theKashey/react-imported-component#-imported-js} 18 | */ 19 | export interface ImportedConfiguration { 20 | /** 21 | * tests folder during scanning process. Can be used to optimize scanning process. 22 | * @default ignores `node_modules` and `.*` directories 23 | * @returns boolean flag 24 | * - true, dive in 25 | * - false, stop here 26 | */ 27 | testFolder?: (targetName: string) => boolean; 28 | /** 29 | * tests if this file should scanned by `imported-component`. 30 | * Keep in mind that you might consider removing (unit)test files from the scan 31 | * @param fileName - source file name 32 | * @returns {Boolean} true - if should, false - is should not 33 | * @example 34 | * // hides node modules 35 | * testFile(filename) { return !filename.test(/node_modules/); } 36 | */ 37 | testFile?: (fileName: string) => boolean; 38 | /** 39 | * tests if a given import should be visible to a `imported-component` 40 | * This method is equivalent to `client-side` magic comment 41 | * @param {String} targetFileName - import target 42 | * @param {String} sourceFileName - source filename 43 | * @returns {Boolean} false if import should be ignored by the `imported-components` 44 | * @see {@link https://github.com/theKashey/react-imported-component/#server-side-auto-import} 45 | * @example 46 | * //a.js -> source 47 | * import('./b.js) -> target 48 | * @example 49 | * testImport(filename, source, config) { 50 | * return !( 51 | * // no mjs please 52 | * filename.indexOf('.mjs')===-1 53 | * // no webpack-ignore please (don't do it) 54 | * config.webpackIgnore 55 | * ) 56 | * } 57 | */ 58 | testImport?: (targetFileName: string, sourceFileName: string) => boolean; 59 | /** 60 | * marks import with prefetch comment (if possible) 61 | * @param {String} targetFile 62 | * @param {String} sourceFile 63 | * @param sourceConfiguration 64 | */ 65 | shouldPrefetch?: (targetFile: string, sourceFile: string, sourceConfiguration: ImportOptions) => boolean; 66 | /** 67 | * marks import with preload comment (if possible) 68 | * @param {String} targetFile 69 | * @param {String} sourceFile 70 | * @param sourceConfiguration 71 | */ 72 | shouldPreload?: (targetFile: string, sourceFile: string, sourceConfiguration: ImportOptions) => boolean; 73 | /** 74 | * adds custom chunkname to a import (if possible) 75 | * @param {String} targetFile 76 | * @param {String} sourceFile 77 | * @param {String|undefined} givenChunkName 78 | * @returns 79 | * {string} - a new chunk name 80 | * {undefined} - keep as is 81 | * {null} - keep as is (will remove in the future) 82 | */ 83 | chunkName?: (targetFile: string, sourceFile: string, importOptions: ImportOptions) => string | null | undefined; 84 | 85 | /** 86 | * clientside configuration properties to be passed into `setConfiguration` 87 | */ 88 | configuration?: Partial; 89 | } 90 | 91 | /** 92 | * provides react-imported-component configuration 93 | * @param {ImportedConfiguration} config 94 | */ 95 | export const configure = (config: ImportedConfiguration): ImportedConfiguration => config; 96 | -------------------------------------------------------------------------------- /src/configuration/constants.ts: -------------------------------------------------------------------------------- 1 | export const CLIENT_SIDE_ONLY = 'client-side'; 2 | -------------------------------------------------------------------------------- /src/entrypoints/babel.ts: -------------------------------------------------------------------------------- 1 | import { babelPlugin } from '../babel/babel'; 2 | 3 | export default babelPlugin; 4 | -------------------------------------------------------------------------------- /src/entrypoints/boot.ts: -------------------------------------------------------------------------------- 1 | import { setConfiguration } from '../configuration/config'; 2 | import { assignImportedComponents } from '../loadable/assignImportedComponents'; 3 | import { loadByChunkname } from '../loadable/loadByChunkName'; 4 | import { rehydrateMarks } from '../loadable/marks'; 5 | import { done as whenComponentsReady } from '../loadable/pending'; 6 | import { addPreloader } from '../loadable/preloaders'; 7 | import { injectLoadableTracker } from '../trackers/globalTracker'; 8 | 9 | export { 10 | rehydrateMarks, 11 | whenComponentsReady, 12 | assignImportedComponents, 13 | loadByChunkname, 14 | setConfiguration, 15 | injectLoadableTracker, 16 | addPreloader, 17 | }; 18 | -------------------------------------------------------------------------------- /src/entrypoints/index.ts: -------------------------------------------------------------------------------- 1 | import { setConfiguration } from '../configuration/config'; 2 | import { configure, ImportedConfiguration } from '../configuration/configuration'; 3 | import { assignImportedComponents } from '../loadable/assignImportedComponents'; 4 | import { loadByChunkname } from '../loadable/loadByChunkName'; 5 | import { clearImportedCache, dryRender, getLoadable as loadableResource } from '../loadable/loadable'; 6 | import { getMarkedChunks, getMarkedFileNames } from '../loadable/markerMapper'; 7 | import { drainHydrateMarks, printDrainHydrateMarks, rehydrateMarks, waitForMarks } from '../loadable/marks'; 8 | import { done as whenComponentsReady } from '../loadable/pending'; 9 | import { addPreloader } from '../loadable/preloaders'; 10 | import { DefaultImport } from '../types'; 11 | import { ImportedComponent } from '../ui/Component'; 12 | import { ImportedComponent as ComponentLoader } from '../ui/Component'; 13 | import imported, { lazy } from '../ui/HOC'; 14 | import { ImportedController } from '../ui/ImportedController'; 15 | import { LazyBoundary } from '../ui/LazyBoundary'; 16 | import { ImportedModule, importedModule } from '../ui/Module'; 17 | import { ImportedStream } from '../ui/context'; 18 | import { useImported, useLazy, useLoadable } from '../ui/useImported'; 19 | import { remapImports } from '../utils/helpers'; 20 | import { useIsClientPhase } from '../utils/useClientPhase'; 21 | 22 | export { 23 | printDrainHydrateMarks, 24 | drainHydrateMarks, 25 | rehydrateMarks, 26 | waitForMarks, 27 | whenComponentsReady, 28 | dryRender, 29 | assignImportedComponents, 30 | loadByChunkname, 31 | ComponentLoader, 32 | ImportedComponent, 33 | ImportedModule, 34 | loadableResource, 35 | ImportedStream, 36 | setConfiguration, 37 | imported, 38 | importedModule, 39 | lazy, 40 | LazyBoundary, 41 | ImportedController, 42 | useIsClientPhase, 43 | remapImports, 44 | useLoadable, 45 | useImported, 46 | useLazy, 47 | addPreloader, 48 | getMarkedChunks, 49 | getMarkedFileNames, 50 | clearImportedCache, 51 | ImportedConfiguration, 52 | configure, 53 | DefaultImport, 54 | }; 55 | export default imported; 56 | -------------------------------------------------------------------------------- /src/entrypoints/macro.ts: -------------------------------------------------------------------------------- 1 | // @ts-ignore 2 | import { createMacro } from 'babel-plugin-macros'; 3 | 4 | import { createTransformer } from '../babel/babel'; 5 | import { assignImportedComponents } from '../loadable/assignImportedComponents'; 6 | 7 | function getMacroType(tagName: string) { 8 | switch (tagName) { 9 | case 'importedModule': 10 | case 'imported': 11 | case 'lazy': 12 | case 'useImported': 13 | 14 | case 'ImportedModule': 15 | case 'ImportedComponent': 16 | return 'react-imported-component'; 17 | 18 | case 'assignImportedComponents': 19 | return 'react-imported-component/boot'; 20 | 21 | default: 22 | return false; 23 | } 24 | } 25 | 26 | function macro({ references, state, babel }: any) { 27 | const { types: t } = babel; 28 | const fileName = state.file.opts.filename; 29 | 30 | const imports: Record = {}; 31 | const transformer = createTransformer(babel, true); 32 | 33 | Object.keys(references).forEach((tagName: string) => { 34 | const origin = getMacroType(tagName); 35 | 36 | if (origin) { 37 | imports[origin] = imports[origin] || []; 38 | imports[origin].push(tagName); 39 | 40 | const tags = references[tagName]; 41 | 42 | tags.forEach((tag: any) => { 43 | const expression = tag.parentPath; 44 | 45 | if (t.isCallExpression(expression)) { 46 | transformer.traverse(expression, fileName); 47 | } 48 | }); 49 | } 50 | }); 51 | 52 | addReactImports(babel, state, imports); 53 | transformer.traverse(state.file.path, fileName); 54 | transformer.finish(state.file.path.node, fileName); 55 | } 56 | 57 | function addReactImports(babel: any, state: any, importsGroups: Record) { 58 | const { types: t } = babel; 59 | 60 | return Object.keys(importsGroups) 61 | .map((origin) => { 62 | const imports = importsGroups[origin]; 63 | 64 | if (!imports.length) { 65 | return false; 66 | } 67 | 68 | const importedImport = state.file.path.node.body.find( 69 | (importNode: any) => t.isImportDeclaration(importNode) && importNode.source.value === origin 70 | ); 71 | 72 | // Handle adding the import or altering the existing import 73 | if (importedImport) { 74 | imports.forEach((name) => { 75 | if ( 76 | !importedImport.specifiers.find((specifier: any) => specifier.imported && specifier.imported.name === name) 77 | ) { 78 | importedImport.specifiers.push(t.importSpecifier(t.identifier(name), t.identifier(name))); 79 | } 80 | }); 81 | } else { 82 | state.file.path.node.body.unshift( 83 | t.importDeclaration( 84 | imports.map((name) => t.importSpecifier(t.identifier(name), t.identifier(name))), 85 | t.stringLiteral(origin) 86 | ) 87 | ); 88 | } 89 | 90 | return true; 91 | }) 92 | .some(Boolean); 93 | } 94 | 95 | const neverCallMe: any = () => { 96 | throw new Error( 97 | 'you have used `react-imported-component/macro` without `babel-plugin-macro` or `react-hot-loader/babel` installed' 98 | ); 99 | }; 100 | 101 | const lazy: typeof import('../ui/HOC').lazy = neverCallMe; 102 | const imported: typeof import('../ui/HOC').default = neverCallMe; 103 | const importedModule: typeof import('../ui/Module').importedModule = neverCallMe; 104 | const useImported: typeof import('../ui/useImported').useImported = neverCallMe; 105 | 106 | const ImportedModule: typeof import('../ui/Module').ImportedModule = neverCallMe; 107 | const ImportedComponent: typeof import('../ui/Component').ImportedComponent = neverCallMe; 108 | 109 | export { lazy, imported, importedModule, ImportedModule, ImportedComponent, useImported, assignImportedComponents }; 110 | 111 | export default createMacro(macro); 112 | -------------------------------------------------------------------------------- /src/entrypoints/server.ts: -------------------------------------------------------------------------------- 1 | import { setConfiguration } from '../configuration/config'; 2 | import { getMarkedChunks, getMarkedFileNames } from '../loadable/markerMapper'; 3 | import { drainHydrateMarks, printDrainHydrateMarks } from '../loadable/marks'; 4 | import { createLoadableStream } from '../loadable/stream'; 5 | import { getLoadableTrackerCallback } from '../trackers/globalTracker'; 6 | import { createLoadableTransformer } from '../transformers/loadableTransformer'; 7 | import { Stream as ImportedStreamTracker } from '../types'; 8 | import { ImportedStream } from '../ui/context'; 9 | 10 | export { 11 | printDrainHydrateMarks, 12 | drainHydrateMarks, 13 | ImportedStream, 14 | setConfiguration, 15 | createLoadableStream, 16 | createLoadableTransformer, 17 | getLoadableTrackerCallback, 18 | getMarkedChunks, 19 | getMarkedFileNames, 20 | ImportedStreamTracker, 21 | }; 22 | -------------------------------------------------------------------------------- /src/loadable/assignImportedComponents.ts: -------------------------------------------------------------------------------- 1 | import { settings } from '../configuration/config'; 2 | import { Promised } from '../types'; 3 | import { assignMetaData } from './metadata'; 4 | import { done } from './pending'; 5 | import { LOADABLE_SIGNATURE } from './registry'; 6 | import { toLoadable } from './toLoadable'; 7 | 8 | type ImportedDefinition = [Promised, string, string, boolean]; 9 | 10 | /** 11 | * to be used __only via CLI tools__ 12 | */ 13 | export const assignImportedComponents = (set: ImportedDefinition[]) => { 14 | const countBefore = LOADABLE_SIGNATURE.size; 15 | 16 | set.forEach((imported) => { 17 | const allowAutoLoad = !(imported[3] || !settings.fileFilter(imported[2])); 18 | const loadable = toLoadable(imported[0], allowAutoLoad); 19 | assignMetaData(loadable.mark, loadable, imported[1], imported[2]); 20 | }); 21 | 22 | if (set.length === 0) { 23 | // tslint:disable-next-line:no-console 24 | console.error('react-imported-component: no import-marks found, please check babel plugin'); 25 | } 26 | 27 | if (countBefore === LOADABLE_SIGNATURE.size) { 28 | // tslint:disable-next-line:no-console 29 | console.error('react-imported-component: no new imports found'); 30 | } 31 | 32 | done(); 33 | 34 | return set; 35 | }; 36 | -------------------------------------------------------------------------------- /src/loadable/loadByChunkName.ts: -------------------------------------------------------------------------------- 1 | import { markMeta } from './metadata'; 2 | 3 | /** 4 | * loads chunk by a known chunkname 5 | * @param {String} chunkName 6 | */ 7 | export const loadByChunkname = (chunkName: string): Promise => 8 | Promise.all(markMeta.filter((meta) => meta.chunkName === chunkName).map((meta) => meta.loadable.load())); 9 | -------------------------------------------------------------------------------- /src/loadable/loadable.ts: -------------------------------------------------------------------------------- 1 | import { DefaultImport, Loadable } from '../types'; 2 | import { getFunctionSignature, importMatch } from '../utils/signatures'; 3 | import { done } from './pending'; 4 | import { LOADABLE_SIGNATURE, LOADABLE_WEAK_SIGNATURE } from './registry'; 5 | import { toLoadable } from './toLoadable'; 6 | import { toKnownSignature } from './utils'; 7 | 8 | /** 9 | * try to perform a render and loads all chunks required for it 10 | * @deprecated 11 | */ 12 | export const dryRender = (renderFunction: () => void): Promise => { 13 | renderFunction(); 14 | 15 | return Promise.resolve().then(done); 16 | }; 17 | 18 | export function executeLoadable(importFunction: DefaultImport | Loadable) { 19 | if ('resolution' in importFunction) { 20 | return importFunction.reload(); 21 | } else { 22 | return importFunction(); 23 | } 24 | } 25 | 26 | /** 27 | * wraps an `import` function with a tracker 28 | * @internal 29 | * @param importFunction 30 | */ 31 | export function getLoadable(importFunction: DefaultImport | Loadable): Loadable { 32 | if ('resolution' in importFunction) { 33 | return importFunction; 34 | } 35 | 36 | if (LOADABLE_WEAK_SIGNATURE.has(importFunction)) { 37 | return LOADABLE_WEAK_SIGNATURE.get(importFunction) as any; 38 | } 39 | 40 | const rawSignature = getFunctionSignature(importFunction); 41 | const ownMark = importMatch(String(rawSignature)); 42 | // read cache signature 43 | const functionSignature = toKnownSignature(rawSignature, ownMark); 44 | 45 | if (LOADABLE_SIGNATURE.has(functionSignature)) { 46 | // tslint:disable-next-line:no-shadowed-variable 47 | const loadable = LOADABLE_SIGNATURE.get(functionSignature)!; 48 | loadable.replaceImportFunction(importFunction); 49 | 50 | return loadable as any; 51 | } 52 | 53 | if (ownMark) { 54 | if (process.env.NODE_ENV !== 'production') { 55 | if (ownMark.length > 1) { 56 | // tslint:disable-next-line:no-console 57 | console.warn('A function exposes multiple imports. Imported-Components cannot know which one will be used.', { 58 | importFunction, 59 | }); 60 | } 61 | } 62 | 63 | LOADABLE_SIGNATURE.forEach(({ mark, importer }) => { 64 | if (mark[0] === ownMark[1] && mark.join('|') === ownMark.join('|')) { 65 | // tslint:disable-next-line:no-console 66 | console.warn( 67 | "Another loadable found for an existing mark. That's possible, but signatures must match (https://github.com/theKashey/react-imported-component/issues/192):", 68 | { 69 | mark, 70 | knownImporter: importer, 71 | currentImported: importFunction, 72 | currentSignature: String(importFunction), 73 | knownSignature: String(importer), 74 | } 75 | ); 76 | } 77 | }); 78 | } 79 | 80 | const loadable = toLoadable(importFunction as any); 81 | LOADABLE_WEAK_SIGNATURE.set(importFunction, loadable); 82 | 83 | return loadable as any; 84 | } 85 | 86 | /** 87 | * Reset `importers` weak cache 88 | * @internal 89 | */ 90 | export const clearImportedCache = (): void => LOADABLE_SIGNATURE.clear(); 91 | -------------------------------------------------------------------------------- /src/loadable/markerMapper.ts: -------------------------------------------------------------------------------- 1 | import { Mark, MarkMeta } from '../types'; 2 | import { markMeta } from './metadata'; 3 | import { markerOverlap } from './utils'; 4 | 5 | const getMarkedMeta = (marks: Mark, mapping: (meta: MarkMeta) => string) => { 6 | if (markMeta.length === 0) { 7 | throw new Error('react-imported-component: no import meta-information found. Have you imported async-requires?'); 8 | } 9 | 10 | return Array.from( 11 | new Set( 12 | markMeta 13 | .filter(meta => markerOverlap(meta.mark, marks)) 14 | .map(mapping) 15 | .filter(Boolean) 16 | ).values() 17 | ); 18 | }; 19 | 20 | export const getMarkedChunks = (marks: Mark) => getMarkedMeta(marks, meta => meta.chunkName); 21 | export const getMarkedFileNames = (marks: Mark) => getMarkedMeta(marks, meta => meta.fileName); 22 | -------------------------------------------------------------------------------- /src/loadable/marks.ts: -------------------------------------------------------------------------------- 1 | import { Loadable, Mark, Stream } from '../types'; 2 | import { checkStream, clearStream, defaultStream } from './stream'; 3 | import { markerOverlap } from './utils'; 4 | 5 | interface MarkPair { 6 | mark: Mark; 7 | loadable: Loadable; 8 | } 9 | 10 | const LOADABLE_MARKS = new Map(); 11 | 12 | export const consumeMark = (stream: Stream = defaultStream, marks: string[]): void => { 13 | checkStream(stream); 14 | 15 | if (marks && marks.length) { 16 | marks.forEach((a) => (stream.marks[a] = true)); 17 | } 18 | }; 19 | 20 | export const assignLoadableMark = (mark: Mark, loadable: Loadable): void => { 21 | LOADABLE_MARKS.set(JSON.stringify(mark), { mark, loadable }); 22 | }; 23 | 24 | /** 25 | * returns marks used in the stream 26 | * @param stream 27 | */ 28 | export const getUsedMarks = (stream: Stream = defaultStream): string[] => (stream ? Object.keys(stream.marks) : []); 29 | 30 | /** 31 | * SSR 32 | * @returns list or marks used 33 | */ 34 | export const drainHydrateMarks = (stream: Stream = defaultStream): string[] => { 35 | checkStream(stream); 36 | 37 | const marks = getUsedMarks(stream); 38 | clearStream(stream); 39 | 40 | return marks; 41 | }; 42 | 43 | /** 44 | * Loads a given marks/chunks 45 | * @see returns a promise for a given marks only. In order to await all requests currently in flight use {@link waitForMarks} 46 | * @param marks 47 | * @returns a resolution promise 48 | */ 49 | export const rehydrateMarks = (marks?: string[]): Promise => { 50 | const rehydratedMarks: string[] = marks || (global as any).___REACT_DEFERRED_COMPONENT_MARKS || []; 51 | const tasks: Array> = []; 52 | 53 | const usedMarks = new Set(); 54 | 55 | LOADABLE_MARKS.forEach(({ mark, loadable }) => { 56 | if (markerOverlap(mark, rehydratedMarks)) { 57 | mark.forEach((m) => usedMarks.add(m)); 58 | tasks.push(loadable.load()); 59 | } 60 | }); 61 | 62 | rehydratedMarks.forEach((m) => { 63 | if (!usedMarks.has(m)) { 64 | throw new Error( 65 | `react-imported-component: unknown mark(${m}) has been used. Client and Server should have the same babel configuration.` 66 | ); 67 | } 68 | }); 69 | 70 | return Promise.all(tasks); 71 | }; 72 | 73 | /** 74 | * waits for the given marks to load 75 | * @param marks 76 | */ 77 | export const waitForMarks = (marks: string[]): Promise => { 78 | const tasks: Array> = []; 79 | 80 | LOADABLE_MARKS.forEach(({ mark, loadable }) => { 81 | if (markerOverlap(mark, marks)) { 82 | tasks.push(loadable.resolution); 83 | } 84 | }); 85 | 86 | return Promise.all(tasks); 87 | }; 88 | 89 | /** 90 | * @returns a `; 96 | }; 97 | -------------------------------------------------------------------------------- /src/loadable/metadata.ts: -------------------------------------------------------------------------------- 1 | import { Loadable, Mark, MarkMeta } from '../types'; 2 | 3 | export const markMeta: MarkMeta[] = []; 4 | 5 | export const assignMetaData = (mark: Mark, loadable: Loadable, chunkName: string, fileName: string): void => { 6 | markMeta.push({ mark, loadable, chunkName, fileName }); 7 | }; 8 | -------------------------------------------------------------------------------- /src/loadable/pending.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * pending indicates any ongoing procceses 3 | */ 4 | let pending: Array> = []; 5 | 6 | export const addPending = (promise: Promise): void => { 7 | pending.push(promise); 8 | }; 9 | 10 | export const removeFromPending = (promise: Promise): void => { 11 | pending = pending.filter((a) => a !== promise); 12 | }; 13 | 14 | /** 15 | * is it really ready? 16 | */ 17 | let readyFlag = false; 18 | 19 | export const isItReady = (): boolean => readyFlag; 20 | 21 | /** 22 | * waits for all necessary imports to be fulfilled 23 | */ 24 | export const done = (): Promise => { 25 | if (pending.length) { 26 | readyFlag = false; 27 | 28 | return Promise.all(pending) 29 | .then((a) => a[1]) 30 | .then(done); 31 | } 32 | 33 | readyFlag = true; 34 | 35 | return Promise.resolve(); 36 | }; 37 | -------------------------------------------------------------------------------- /src/loadable/preloaders.ts: -------------------------------------------------------------------------------- 1 | export type Preloader = () => any; 2 | 3 | let preloaders: Preloader[] = []; 4 | type Callback = () => void; 5 | 6 | /** 7 | * adds a precondition before resolving any imported object 8 | */ 9 | export const addPreloader = (preloader: Preloader): Callback => { 10 | preloaders.push(preloader); 11 | 12 | return () => { 13 | preloaders = preloaders.filter((p) => p !== preloader); 14 | }; 15 | }; 16 | 17 | export const getPreloaders = (): any[] => preloaders.map((preloader) => preloader()); 18 | -------------------------------------------------------------------------------- /src/loadable/registry.ts: -------------------------------------------------------------------------------- 1 | import { Loadable } from '../types'; 2 | 3 | export const LOADABLE_WEAK_SIGNATURE = new WeakMap>(); 4 | export const LOADABLE_SIGNATURE = new Map>(); 5 | -------------------------------------------------------------------------------- /src/loadable/stream.ts: -------------------------------------------------------------------------------- 1 | import { Stream } from '../types'; 2 | 3 | export const createLoadableStream = (): Stream => ({ marks: {} }); 4 | 5 | export const clearStream = (stream?: Stream): void => { 6 | if (stream) { 7 | stream.marks = {}; 8 | } 9 | }; 10 | 11 | export const checkStream = (stream: Stream | number | string | undefined): void => { 12 | if (process.env.NODE_ENV !== 'production') { 13 | if (!stream) { 14 | return; 15 | } 16 | 17 | if (typeof stream !== 'object' || !stream.marks) { 18 | throw new Error( 19 | 'react-imported-component: version 6 requires `stream` to be an object. Refer to the migration guide' 20 | ); 21 | } 22 | } 23 | }; 24 | 25 | export const defaultStream = createLoadableStream(); 26 | -------------------------------------------------------------------------------- /src/loadable/toLoadable.ts: -------------------------------------------------------------------------------- 1 | import { AnyFunction, Loadable, Promised } from '../types'; 2 | import { isBackend } from '../utils/detectBackend'; 3 | import { getFunctionSignature, importMatch } from '../utils/signatures'; 4 | import { assignLoadableMark } from './marks'; 5 | import { addPending, removeFromPending } from './pending'; 6 | import { getPreloaders } from './preloaders'; 7 | import { LOADABLE_SIGNATURE } from './registry'; 8 | import { toKnownSignature } from './utils'; 9 | 10 | export interface InnerLoadable extends Loadable { 11 | ok: boolean; 12 | promise: Promise | undefined; 13 | 14 | _probeChanges(): Promise; 15 | } 16 | 17 | export function toLoadable(firstImportFunction: Promised, autoImport = true): Loadable { 18 | let importFunction = firstImportFunction; 19 | const loadImportedComponent = (): Promise => 20 | Promise.all([importFunction(), ...getPreloaders()]).then(([result]) => result); 21 | const functionSignature = getFunctionSignature(importFunction); 22 | const mark = importMatch(functionSignature); 23 | 24 | let resolveResolution: AnyFunction; 25 | const resolution = new Promise((r) => { 26 | resolveResolution = r; 27 | }); 28 | 29 | const loadable: InnerLoadable = { 30 | // importFunction, 31 | mark, 32 | resolution, 33 | done: false, 34 | ok: false, 35 | error: null, 36 | payload: undefined, 37 | promise: undefined, 38 | 39 | isLoading() { 40 | return !!this.promise && !this.done; 41 | }, 42 | 43 | reset() { 44 | this.done = false; 45 | this.ok = true; 46 | this.payload = undefined; 47 | this.promise = undefined; 48 | }, 49 | 50 | replaceImportFunction(newImportFunction) { 51 | importFunction = newImportFunction; 52 | }, 53 | 54 | get importer() { 55 | return importFunction; 56 | }, 57 | 58 | then(cb, err) { 59 | if (this.promise) { 60 | return this.promise.then(cb, err); 61 | } 62 | 63 | if (err) { 64 | err(); 65 | } 66 | 67 | return Promise.reject(); 68 | }, 69 | 70 | loadIfNeeded() { 71 | if (this.error) { 72 | this.reset(); 73 | } 74 | 75 | if (!this.promise) { 76 | this.load(); 77 | } 78 | 79 | return this.promise!; 80 | }, 81 | 82 | tryResolveSync(then) { 83 | if (this.done) { 84 | const result = then(this.payload as any); 85 | 86 | return { 87 | then(cb: any) { 88 | // synchronous thenable - https://github.com/facebook/react/pull/14626 89 | cb(result); 90 | 91 | return Promise.resolve(result); 92 | }, 93 | } as any; 94 | } 95 | 96 | return this.loadIfNeeded().then(then); 97 | }, 98 | 99 | reload() { 100 | if (this.promise) { 101 | this.promise = undefined; 102 | 103 | return this.load() as any; 104 | } 105 | 106 | return Promise.resolve(); 107 | }, 108 | 109 | _probeChanges() { 110 | return Promise.resolve(importFunction()) 111 | .then((payload) => payload !== this.payload) 112 | .catch((err) => { 113 | throw err; 114 | }); 115 | }, 116 | 117 | load() { 118 | if (!this.promise) { 119 | const promise = (this.promise = loadImportedComponent().then( 120 | (payload) => { 121 | this.done = true; 122 | this.ok = true; 123 | this.payload = payload; 124 | this.error = null; 125 | removeFromPending(promise); 126 | resolveResolution(payload); 127 | 128 | return payload; 129 | }, 130 | (err) => { 131 | this.done = true; 132 | this.ok = false; 133 | this.error = err; 134 | removeFromPending(promise); 135 | throw err; 136 | } 137 | )); 138 | addPending(promise); 139 | } 140 | 141 | return this.promise; 142 | }, 143 | }; 144 | 145 | if (mark && mark.length) { 146 | LOADABLE_SIGNATURE.set(toKnownSignature(functionSignature, mark), loadable); 147 | assignLoadableMark(mark, loadable); 148 | } else { 149 | if (process.env.NODE_ENV !== 'development') { 150 | // tslint:disable-next-line:no-console 151 | console.warn( 152 | 'react-imported-component: no mark found at', 153 | importFunction, 154 | "Please check babel plugin or macro setup, as well as imported-component's limitations. See https://github.com/theKashey/react-imported-component/issues/147" 155 | ); 156 | } 157 | } 158 | 159 | // trigger preload on the server side 160 | if (isBackend && autoImport) { 161 | loadable.load(); 162 | } 163 | 164 | return loadable; 165 | } 166 | -------------------------------------------------------------------------------- /src/loadable/utils.ts: -------------------------------------------------------------------------------- 1 | import { settings } from '../configuration/config'; 2 | 3 | export const toKnownSignature = (signature: string, marks: string[]): string => 4 | (!settings.checkSignatures && marks.join('|')) || signature; 5 | 6 | export const markerOverlap = (a1: string[], a2: string[]): boolean => 7 | a1.filter((mark) => a2.indexOf(mark) >= 0).length === a1.length; 8 | -------------------------------------------------------------------------------- /src/scanners/__tests__/parser.spec.ts: -------------------------------------------------------------------------------- 1 | import { dirname } from 'path'; 2 | 3 | import { remapImports } from '../scanForImports'; 4 | import { getRelative } from '../shared'; 5 | 6 | describe('scanForImports', () => { 7 | const rel = dirname(__dirname); 8 | const root = '.'; 9 | const rootRel = '.' + process.cwd(); 10 | const sourceFile = `${rel}/a`; 11 | 12 | it('should map simple import', () => { 13 | const imports = {}; 14 | 15 | remapImports( 16 | [{ file: `${rel}/a`, content: 'blabla;import("./b.js"); blabla;' }], 17 | rel, 18 | rel, 19 | getRelative, 20 | imports, 21 | () => true 22 | ); 23 | 24 | expect(Object.values(imports)).toEqual([`[() => import('./b.js'), '', './b.js', false] /* from ./a */`]); 25 | }); 26 | 27 | it('handles imports in jsdoc', () => { 28 | const imports = {}; 29 | 30 | remapImports( 31 | [ 32 | { 33 | file: sourceFile, 34 | content: ` 35 | /** 36 | * @type {import('wrong-import')} 37 | */ 38 | import(/* comment:valuable */ "./a.js"); 39 | import("./b.js"); 40 | // import('another-wrong-import');// FIXME: temporary removed 41 | `, 42 | }, 43 | ], 44 | rel, 45 | rel, 46 | getRelative, 47 | imports, 48 | () => true 49 | ); 50 | 51 | expect(Object.values(imports)).toEqual([ 52 | `[() => import(/* comment:valuable */'./a.js'), '', './a.js', false] /* from ./a */`, 53 | `[() => import('./b.js'), '', './b.js', false] /* from ./a */`, 54 | ]); 55 | }); 56 | 57 | it('should map client-side import', () => { 58 | const imports = {}; 59 | 60 | remapImports( 61 | [{ file: sourceFile, content: 'blabla;import(/* client-side */"./a.js"); blabla;' }], 62 | rel, 63 | rel, 64 | getRelative, 65 | imports, 66 | () => true 67 | ); 68 | 69 | expect(Object.values(imports)).toEqual([ 70 | `[() => import(/* client-side */'./a.js'), '', './a.js', true] /* from ./a */`, 71 | ]); 72 | }); 73 | 74 | it('should map simple import with a comment', () => { 75 | const imports = {}; 76 | 77 | remapImports( 78 | [{ file: sourceFile, content: 'blabla;import(/* comment:42 */"./a.js"); blabla;' }], 79 | rel, 80 | rel, 81 | getRelative, 82 | imports, 83 | () => true 84 | ); 85 | 86 | expect(Object.values(imports)).toEqual([ 87 | `[() => import(/* comment:42 */'./a.js'), '', './a.js', false] /* from ./a */`, 88 | ]); 89 | }); 90 | 91 | it('should map complex import', () => { 92 | const imports = {}; 93 | 94 | remapImports( 95 | [ 96 | { 97 | file: sourceFile, 98 | content: 'blabla;import(/* webpack: "123" */"./a.js"); blabla; import(/* webpack: 123 */ \'./b.js\');', 99 | }, 100 | ], 101 | rel, 102 | rel, 103 | getRelative, 104 | imports, 105 | () => true 106 | ); 107 | 108 | expect(Object.values(imports)).toEqual([ 109 | `[() => import(/* webpack: \"123\" */'./a.js'), '', './a.js', false] /* from ./a */`, 110 | `[() => import(/* webpack: 123 */'./b.js'), '', './b.js', false] /* from ./a */`, 111 | ]); 112 | }); 113 | 114 | it('should match chunk name', () => { 115 | const imports = {}; 116 | 117 | remapImports( 118 | [ 119 | { 120 | file: 'a', 121 | content: 122 | 'blabla;import(/* webpackChunkName: "chunk-a" */"./a.js"); blabla; import(/* webpack: 123 */ \'./b.js\');', 123 | }, 124 | ], 125 | root, 126 | root, 127 | (a, b) => a + b, 128 | imports, 129 | () => true 130 | ); 131 | 132 | expect(Object.values(imports)).toEqual([ 133 | `[() => import(/* webpackChunkName: "chunk-a" */'${rootRel}/a.js'), 'chunk-a', '${rootRel}/a.js', false] /* from .a */`, 134 | `[() => import(/* webpack: 123 */'${rootRel}/b.js'), '', '${rootRel}/b.js', false] /* from .a */`, 135 | ]); 136 | }); 137 | 138 | it('should override chunk name', () => { 139 | const imports = {}; 140 | 141 | remapImports( 142 | [ 143 | { 144 | file: 'a', 145 | content: 146 | 'blabla;import(/* webpackChunkName: "chunk-a" */"./a.js"); blabla; import(/* webpackChunkName: "chunk-b" */"./b.js"); import(/* webpackChunkName: "chunk-c" */"./c.js");', 147 | }, 148 | ], 149 | root, 150 | root, 151 | (a, b) => a + b, 152 | imports, 153 | (imported) => imported.indexOf('c.js') < 0, 154 | (imported, _, options) => (imported.indexOf('a.js') > 0 ? `test-${options.chunkName}-test` : 'bundle-b') 155 | ); 156 | 157 | expect(Object.values(imports)).toEqual([ 158 | `[() => import(/* webpackChunkName: \"chunk-a\" */'${rootRel}/a.js'), 'test-chunk-a-test', '${rootRel}/a.js', false] /* from .a */`, 159 | `[() => import(/* webpackChunkName: \"chunk-b\" */'${rootRel}/b.js'), 'bundle-b', '${rootRel}/b.js', false] /* from .a */`, 160 | ]); 161 | }); 162 | 163 | it('should match support multiline imports', () => { 164 | const imports = {}; 165 | 166 | remapImports( 167 | [ 168 | { 169 | file: 'a', 170 | content: ` 171 | blabla;import( 172 | /* webpackChunkName: "chunk-a" */ 173 | "./a.js" 174 | ); 175 | something else 176 | import( 177 | // ts-ignore 178 | "./b.js" 179 | ); 180 | `, 181 | }, 182 | ], 183 | root, 184 | root, 185 | (a, b) => a + b, 186 | imports, 187 | () => true 188 | ); 189 | 190 | expect(Object.values(imports)).toEqual([ 191 | `[() => import(/* webpackChunkName: \"chunk-a\" */'${rootRel}/a.js'), 'chunk-a', '${rootRel}/a.js', false] /* from .a */`, 192 | `[() => import('${rootRel}/b.js'), '', '${rootRel}/b.js', false] /* from .a */`, 193 | ]); 194 | }); 195 | 196 | it('should remove webpackPrefetch and webpackPreload', () => { 197 | const imports = {}; 198 | 199 | remapImports( 200 | [ 201 | { 202 | file: 'a', 203 | content: 204 | 'blabla;import(/* webpackPrefetch: true *//* webpack: "123" */"./a.js"); blabla; import(/* webpackPreload: true */ \'./b.js\');', 205 | }, 206 | ], 207 | root, 208 | root, 209 | (a, b) => a + b, 210 | imports, 211 | () => true 212 | ); 213 | 214 | expect(Object.values(imports)).toEqual([ 215 | `[() => import(/* *//* webpack: \"123\" */'${rootRel}/a.js'), '', '${rootRel}/a.js', false] /* from .a */`, 216 | `[() => import(/* */'${rootRel}/b.js'), '', '${rootRel}/b.js', false] /* from .a */`, 217 | ]); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /src/scanners/cli.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-console */ 2 | 3 | import { scanTop } from './scanForImports'; 4 | 5 | if (!process.argv[3]) { 6 | console.log('usage: imported-components sourceRoot targetFile'); 7 | console.log('example: imported-components src src/importedComponents.js'); 8 | } else { 9 | scanTop(process.cwd(), process.argv[2], process.argv[3]); 10 | } 11 | -------------------------------------------------------------------------------- /src/scanners/scanForImports.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable no-console */ 2 | 3 | import { existsSync, Stats } from 'fs'; 4 | import { dirname, extname, resolve } from 'path'; 5 | 6 | // @ts-ignore 7 | import scanDirectory from 'scan-directory'; 8 | 9 | import { ImportedConfiguration } from '../configuration/configuration'; 10 | import { CLIENT_SIDE_ONLY } from '../configuration/constants'; 11 | import { getFileContent, getMatchString, getRelative, normalizePath, pWriteFile } from './shared'; 12 | 13 | const RESOLVE_EXTENSIONS = ['.js', '.jsx', '.ts', '.tsx', '.mjs']; 14 | 15 | const trimImport = (str: string) => str.replace(/['"]/g, ''); 16 | 17 | const getImportMatch = getMatchString(`(['"]?[\\w-/.@]+['"]?)\\)`, 1); 18 | const getImports = (str: string) => 19 | getImportMatch( 20 | str 21 | // remove comments 22 | .replace(/\/\*([^\*]*)\*\//gi, '') 23 | .replace(/\/\/(.*)/gi, '') 24 | // remove new lines 25 | .replace(/\n/gi, '') 26 | // remove spaces? 27 | .replace(/[\s]+\)/i, ')') 28 | ); 29 | 30 | const getComment = getMatchString(/\/\*.*\*\// as any, 0); 31 | 32 | const getChunkName = getMatchString('webpackChunkName: "([^"]*)"', 1); 33 | 34 | const clientSideOnly = (comment: string) => comment.indexOf(CLIENT_SIDE_ONLY) >= 0; 35 | 36 | const clearComment = (str: string) => str.replace('webpackPrefetch: true', '').replace('webpackPreload: true', ''); 37 | 38 | interface MappedImport { 39 | name: string; 40 | comment: string; 41 | } 42 | 43 | interface FileContent { 44 | file: string; 45 | content: string; 46 | } 47 | 48 | const getImportString = 49 | (pattern: string, selected: number) => 50 | (str: string): MappedImport[] => 51 | getMatchString( 52 | pattern, 53 | selected 54 | )(str).map((statement) => { 55 | return { 56 | name: trimImport(getImports(statement + ')')[0] || ''), 57 | comment: clearComment(getComment(statement)[0] || ''), 58 | }; 59 | }); 60 | 61 | export const getDynamicImports = getImportString(`import[\\s]?\\((([^)])+['"]?)\\)`, 1); 62 | 63 | export const cleanFileContent = (content: string): string => { 64 | const mapping: string[] = []; 65 | // wrap 66 | const wrapped = content.replace(new RegExp(`import[\\s]?\\((([^)])+['"]?)\\)`, 'g'), (match) => { 67 | const placement = mapping.push(match) - 1; 68 | 69 | return `imported_${placement}_replacement`; 70 | }); 71 | 72 | const cleaned = wrapped.replace(new RegExp('//.*', 'g'), '').replace(new RegExp('\\/\\*[\\s\\S]*?\\*\\/', 'gm'), ''); 73 | 74 | const unwrapped = cleaned.replace(new RegExp('imported_([\\d]*)_replacement', 'g'), (_, b: string) => { 75 | return mapping[+b]; 76 | }); 77 | 78 | return unwrapped; 79 | }; 80 | 81 | const mapImports = (file: string, imports: MappedImport[]) => 82 | imports.map((dep) => { 83 | const { name } = dep; 84 | 85 | if (name && name.charAt(0) === '.') { 86 | return { 87 | ...dep, 88 | file, 89 | name: resolve(dirname(file), name), 90 | doNotTransform: false, 91 | }; 92 | } 93 | 94 | return { 95 | ...dep, 96 | file, 97 | doNotTransform: true, 98 | }; 99 | }); 100 | 101 | const rejectSystemFiles = (test: Required['testFolder']) => (file: string, stats: Stats) => { 102 | if (stats.isDirectory()) { 103 | return !test(file); 104 | } 105 | 106 | return false; 107 | }; 108 | 109 | const rejectNodeModulesAndDotFolders = (file: string) => !(file.match(/node_modules/) || file.match(/(\/\.\w+)/)); 110 | 111 | export const remapImports = ( 112 | data: FileContent[], 113 | root: string, 114 | targetDir: string, 115 | getRelativeName: (a: string, b: string) => string, 116 | imports: Record, 117 | testImport: NonNullable, 118 | chunkName?: ImportedConfiguration['chunkName'] 119 | ): void => 120 | data 121 | .map(({ file, content }) => mapImports(file, getDynamicImports(cleanFileContent(content)))) 122 | .forEach((importBlock) => 123 | importBlock.forEach(({ name, comment, doNotTransform, file }) => { 124 | const rootName = doNotTransform ? name : getRelativeName(root, name); 125 | const fileName = doNotTransform ? name : getRelativeName(targetDir, name); 126 | const sourceName = getRelativeName(root, file); 127 | 128 | if (testImport(rootName, sourceName)) { 129 | const isClientSideOnly = clientSideOnly(comment); 130 | const givenChunkName = getChunkName(comment)[0] || ''; 131 | const def = `[() => import(${comment}'${fileName}'), '${ 132 | (chunkName && chunkName(rootName, sourceName, { chunkName: givenChunkName })) || givenChunkName 133 | }', '${rootName}', ${isClientSideOnly}] /* from ${sourceName} */`; 134 | const slot = getRelativeName(root, name); 135 | 136 | // keep the maximal definition 137 | imports[slot] = !imports[slot] ? def : imports[slot].length > def.length ? imports[slot] : def; 138 | } 139 | }) 140 | ); 141 | 142 | export function scanTop(root: string, start: string, target: string): Promise { 143 | async function scan() { 144 | const sourceDir = resolve(root, start); 145 | console.log('scanning', sourceDir, 'for imports...'); 146 | 147 | // try load configuration 148 | const configurationFile = resolve(root, '.imported.js'); 149 | const { 150 | testFolder = rejectNodeModulesAndDotFolders, 151 | testFile = () => true, 152 | testImport = () => true, 153 | chunkName, 154 | configuration, 155 | }: ImportedConfiguration = existsSync(configurationFile) ? require(configurationFile) : {}; 156 | 157 | const files = ((await scanDirectory(sourceDir, undefined, rejectSystemFiles(testFolder))) as string[]) 158 | .filter((name) => normalizePath(name).indexOf(target) === -1) 159 | .filter((name) => RESOLVE_EXTENSIONS.indexOf(extname(name)) >= 0) 160 | .filter((name) => testFile(name)) 161 | .sort(); 162 | 163 | const data: FileContent[] = await Promise.all( 164 | files.map(async (file) => { 165 | const content = await getFileContent(file); 166 | 167 | return { 168 | file, 169 | content, 170 | }; 171 | }) 172 | ); 173 | 174 | const imports: Record = {}; 175 | const targetDir = resolve(root, dirname(target)); 176 | remapImports(data, root, targetDir, getRelative, imports, testImport, chunkName); 177 | 178 | console.log(`${Object.keys(imports).length} imports found, saving to ${target}`); 179 | 180 | pWriteFile( 181 | target, 182 | ` 183 | /* eslint-disable */ 184 | /* tslint:disable */ 185 | 186 | // generated by react-imported-component, DO NOT EDIT 187 | import {assignImportedComponents} from 'react-imported-component/macro'; 188 | ${ 189 | configuration && 190 | `import {setConfiguration} from 'react-imported-component/boot'; 191 | // as configured in .imported.js 192 | setConfiguration(${JSON.stringify(configuration, null, 2)}); 193 | ` 194 | } 195 | 196 | // all your imports are defined here 197 | // all, even the ones you tried to hide in comments (that's the cost of making a very fast parser) 198 | // to remove any import from here 199 | // 1) use magic comment \`import(/* client-side */ './myFile')\` - and it will disappear 200 | // 2) use file filter to ignore specific locations (refer to the README - https://github.com/theKashey/react-imported-component/#server-side-auto-import) 201 | // 3) use .imported.js to control this table generation (refer to the README - https://github.com/theKashey/react-imported-component/#-imported-js) 202 | 203 | const applicationImports = assignImportedComponents([ 204 | ${Object.keys(imports) 205 | .map((key) => ` ${imports[key]},`) 206 | .sort() 207 | .join('\n')} 208 | ]); 209 | 210 | export default applicationImports; 211 | 212 | // @ts-ignore 213 | if (module.hot) { 214 | // these imports would make this module a parent for the imported modules. 215 | // but this is just a helper - so ignore(and accept!) all updates 216 | 217 | // @ts-ignore 218 | module.hot.accept(() => null); 219 | } 220 | ` 221 | ); 222 | } 223 | 224 | return scan(); 225 | } 226 | -------------------------------------------------------------------------------- /src/scanners/shared.ts: -------------------------------------------------------------------------------- 1 | import { readFile, writeFile } from 'fs'; 2 | import { relative, sep } from 'path'; 3 | import { promisify } from 'util'; 4 | 5 | export const normalizePath = (path: string): string => path.split(sep).join('/'); 6 | 7 | export const getRelative = (from: string, to: string): string => { 8 | // force one unit paths 9 | const rel = normalizePath(relative(from, to)); 10 | 11 | return rel[0] !== '.' ? './' + rel : rel; 12 | }; 13 | 14 | export const getMatchString = 15 | (pattern: string, selected: number) => 16 | (str: string): string[] => 17 | (str.match(new RegExp(pattern, 'g')) || []).map( 18 | (statement) => (statement.match(new RegExp(pattern, 'i')) || [])[selected] 19 | ); 20 | 21 | export const pReadFile = promisify(readFile); 22 | export const pWriteFile = promisify(writeFile); 23 | export const getFileContent = (file: string): Promise => pReadFile(file, 'utf8'); 24 | -------------------------------------------------------------------------------- /src/trackers/globalTracker.ts: -------------------------------------------------------------------------------- 1 | import { rehydrateMarks } from '../loadable/marks'; 2 | 3 | /** 4 | * injects a loadable tracker on a given global variable name. 5 | * 6 | * WARNING: mutates the origin variable! 7 | * 8 | * @param name = 'importedComponents` 9 | * @example 10 | * ```js 11 | * window.importedMarks = window.importedMarks || []; 12 | * importedMarks.push(hydratedMarks[0]); 13 | * /// 14 | * injectLoadableTracker(); 15 | */ 16 | export const injectLoadableTracker = (name = 'importedComponents'): void => { 17 | const value = (global as any)[name] as string[][]; 18 | 19 | if (value) { 20 | if (!value.push || (Boolean(value.push) && !Boolean(value.forEach))) { 21 | // tslint:disable-next-line:no-console 22 | console.error('given: ', value); 23 | throw new Error(`injectLoadableTracker(${name}) expected to be expected on Array-like variable, and only once.`); 24 | } 25 | 26 | value.forEach((mark) => rehydrateMarks(mark)); 27 | } 28 | 29 | (global as any)[name] = { 30 | push: rehydrateMarks, 31 | }; 32 | }; 33 | 34 | export const getLoadableTrackerCallback = 35 | (name = 'importedComponents') => 36 | (marks: string[]) => 37 | ``; 38 | -------------------------------------------------------------------------------- /src/transformers/loadableTransformer.ts: -------------------------------------------------------------------------------- 1 | import { Transform } from 'stream'; 2 | 3 | import { getUsedMarks } from '../loadable/marks'; 4 | import { Stream } from '../types'; 5 | 6 | type LoadableStreamCallback = (marks: string[]) => string; 7 | 8 | export const createLoadableTransformer = (stream: Stream, callback: LoadableStreamCallback): Transform => { 9 | const usedMarks = new Set(); 10 | 11 | return new Transform({ 12 | // transform() is called with each chunk of data 13 | // tslint:disable-next-line:variable-name 14 | transform(chunk, _, _callback) { 15 | const marks = getUsedMarks(stream); 16 | const newMarks: string[] = []; 17 | 18 | marks.forEach((mark) => { 19 | if (!usedMarks.has(mark)) { 20 | newMarks.push(mark); 21 | usedMarks.add(mark); 22 | } 23 | }); 24 | 25 | const chunkData = Buffer.from(chunk, 'utf-8'); 26 | 27 | _callback(undefined, callback(newMarks) + chunkData); 28 | }, 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType, ForwardRefExoticComponent, ReactElement, ReactNode, Ref } from 'react'; 2 | 3 | export interface DefaultImportedComponent

    { 4 | default: ComponentType

    ; 5 | } 6 | 7 | export type AnyFunction = (x: any) => any; 8 | 9 | export interface Default { 10 | default: T; 11 | } 12 | 13 | export interface Stream { 14 | marks: Record; 15 | } 16 | 17 | export type Mark = string[]; 18 | 19 | export type Promised = () => Promise; 20 | 21 | export type DefaultComponent

    = ComponentType

    | DefaultImportedComponent

    ; 22 | export type DefaultComponentImport = () => Promise>; 23 | 24 | export type Defaultable

    = P | Default

    ; 25 | /** 26 | * standard "importer" accepted by the package. 27 | * Could be {default:T} or T 28 | */ 29 | export type DefaultImport = () => Promise>; 30 | 31 | export interface MarkMeta { 32 | loadable: Loadable; 33 | mark: Mark; 34 | chunkName: string; 35 | fileName: string; 36 | } 37 | 38 | export type LazyImport = () => Promise>; 39 | 40 | export interface LoadableComponentState { 41 | loading?: boolean; 42 | error?: any; 43 | } 44 | 45 | export interface Loadable { 46 | done: boolean; 47 | error: any; 48 | payload: T | undefined; 49 | 50 | mark: Mark; 51 | 52 | resolution: Promise; 53 | importer: any; 54 | 55 | isLoading(): boolean; 56 | 57 | reset(): void; 58 | replaceImportFunction(newImportFunction: Promised): void; 59 | 60 | loadIfNeeded(): Promise; 61 | 62 | tryResolveSync(then: (x: T) => Y): Promise; 63 | 64 | load(): Promise; 65 | reload(): Promise; 66 | 67 | then(callback: (x: T) => void, err: () => void): Promise; 68 | } 69 | 70 | export interface ComponentOptions { 71 | loadable: DefaultComponentImport

    | Loadable>; 72 | 73 | LoadingComponent?: ComponentType; 74 | ErrorComponent?: ComponentType; 75 | 76 | onError?: (a: any) => void; 77 | 78 | async?: boolean; 79 | 80 | render?: (Component: ComponentType

    | undefined, State: LoadableComponentState, props?: K) => ReactElement | null; 81 | 82 | forwardRef?: Ref; 83 | forwardProps?: K; 84 | } 85 | 86 | export interface HOCOptions { 87 | noAutoImport?: boolean; 88 | } 89 | 90 | export interface AdditionalHOC { 91 | done: Promise; 92 | preload(): Promise; 93 | } 94 | 95 | export type HOCType = ForwardRefExoticComponent> }> & 96 | AdditionalHOC; 97 | 98 | export interface ImportModuleHOCProps { 99 | fallback: NonNullable | null; 100 | } 101 | 102 | export interface ImportModuleProps extends ImportModuleHOCProps { 103 | children: (arg: T) => ReactElement | null; 104 | } 105 | 106 | export interface FullImportModuleProps extends ImportModuleProps { 107 | import: DefaultImport | Loadable; 108 | } 109 | 110 | export type ModuleFC = (props: ImportModuleProps) => ReactElement | null; 111 | 112 | export type HOCModuleType = ModuleFC & AdditionalHOC; 113 | 114 | export type HOC = ( 115 | loader: DefaultComponentImport

    , 116 | options?: Partial> & HOCOptions 117 | ) => HOCType; 118 | 119 | export interface ImportedComponents { 120 | [index: number]: () => Promise>; 121 | } 122 | -------------------------------------------------------------------------------- /src/ui/Component.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { ReactElement } from 'react'; 3 | 4 | import { settings } from '../configuration/config'; 5 | import { ComponentOptions } from '../types'; 6 | import { useImported } from './useImported'; 7 | 8 | /** 9 | * @deprecated use {@link useImported} instead 10 | */ 11 | function ImportedComponent(props: ComponentOptions): ReactElement | null { 12 | const { loading, error, loadable, imported: Component, retry } = useImported(props.loadable); 13 | 14 | if (loading && props.async) { 15 | throw loadable.resolution; 16 | } 17 | 18 | if ('render' in props && props.render) { 19 | return props.render(Component, { loading, error }, props.forwardProps); 20 | } 21 | 22 | if (Component) { 23 | // importedUUID for cache busting 24 | // @ts-ignore 25 | return ; 26 | } 27 | 28 | if (loading) { 29 | if (props.async) { 30 | throw loadable.resolution; 31 | } 32 | 33 | return props.LoadingComponent ? : null; 34 | } 35 | 36 | if (error) { 37 | // always report errors 38 | // tslint:disable-next-line:no-console 39 | console.error('react-imported-component', error); 40 | 41 | // let's rethrow the error after react leaves this function 42 | // this might be very crucial for the "safe" dev mode 43 | if (settings.rethrowErrors) { 44 | setTimeout(() => { 45 | throw error; 46 | }); 47 | } 48 | 49 | if (props.ErrorComponent) { 50 | return ; 51 | } 52 | 53 | throw error; 54 | } 55 | 56 | return null; 57 | } 58 | 59 | export { ImportedComponent }; 60 | -------------------------------------------------------------------------------- /src/ui/HOC.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { useMemo } from 'react'; 3 | 4 | import { getLoadable } from '../loadable/loadable'; 5 | import { ComponentOptions, DefaultComponentImport, HOCOptions, HOCType, LazyImport } from '../types'; 6 | import { isBackend } from '../utils/detectBackend'; 7 | import { asDefault } from '../utils/utils'; 8 | import { ImportedComponent } from './Component'; 9 | import { useLoadable } from './useImported'; 10 | 11 | /** 12 | * creates a "lazy" component, like `React.lazy` 13 | * @see {@link useImported} or {@link useLazy} 14 | * @param {Function} loaderFunction - () => import('a'), or () => require('b') 15 | * @param {Object} [options] 16 | * @param {React.Component} [options.LoadingComponent] 17 | * @param {React.Component} [options.ErrorComponent] 18 | * @param {Function} [options.onError] - error handler. Will consume the real error. 19 | * @param {Function} [options.async = false] - enable React 16+ suspense. 20 | * 21 | * @example 22 | * const PageA = imported('./pageA', { async: true }); 23 | */ 24 | function loader( 25 | loaderFunction: DefaultComponentImport

    , 26 | baseOptions: Partial> & HOCOptions = {} 27 | ): HOCType { 28 | let loadable = getLoadable(loaderFunction); 29 | 30 | const Imported = React.forwardRef(function ImportedComponentHOC({ importedProps = {}, ...props }, ref) { 31 | const options = { ...baseOptions, ...importedProps }; 32 | // re-get loadable in order to have fresh reference 33 | loadable = getLoadable(loaderFunction); 34 | 35 | return ( 36 | 46 | ); 47 | }) as HOCType; 48 | 49 | Imported.preload = () => { 50 | loadable.load().catch(() => ({})); 51 | 52 | return loadable.resolution; 53 | }; 54 | 55 | Object.defineProperty(Imported, 'done', { 56 | get() { 57 | return loadable.resolution; 58 | }, 59 | }); 60 | 61 | return Imported; 62 | } 63 | 64 | const ReactLazy = React.lazy; 65 | 66 | /** 67 | * React.lazy "as-is" replacement 68 | */ 69 | export function lazy(importer: LazyImport): React.FC { 70 | if (isBackend) { 71 | return loader(importer); 72 | } 73 | 74 | if (process.env.NODE_ENV !== 'production') { 75 | // lazy is not hot-reloadable 76 | if ((module as any).hot) { 77 | return loader(importer, { async: true }); 78 | } 79 | } 80 | 81 | const topLoadable = getLoadable(importer); 82 | 83 | return function ImportedLazy(props: T) { 84 | const { loadable } = useLoadable(topLoadable); 85 | 86 | const Lazy = useMemo(() => ReactLazy(() => loadable.tryResolveSync(asDefault as any) as any), [loadable]); 87 | 88 | return ; 89 | }; 90 | } 91 | 92 | export default loader; 93 | -------------------------------------------------------------------------------- /src/ui/ImportedController.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useEffect, useState } from 'react'; 2 | 3 | interface ImportedState { 4 | usesHydration: boolean; 5 | pastHydration: boolean; 6 | } 7 | 8 | export const importedState = createContext(undefined); 9 | 10 | export const HydrationState: React.FC<{ state: ImportedState }> = ({ state, children }) => ( 11 | {children} 12 | ); 13 | 14 | /** 15 | * @see [LazyBoundary]{@link LazyBoundary} - HydrationController is required for LazyBoundary to properly work with React>16.10 16 | * Established a control over LazyBoundary suppressing fallback during the initial hydration 17 | * @param props 18 | * @param [props.usesHydration=true] determines of Application is rendered using hydrate 19 | */ 20 | export const ImportedController: React.FC<{ 21 | /** 22 | * determines of Application is rendered using hydrate 23 | */ 24 | usesHydration?: boolean; 25 | }> = ({ children, usesHydration = true }) => { 26 | const [state, setState] = useState({ 27 | usesHydration, 28 | pastHydration: false, 29 | }); 30 | 31 | useEffect(() => { 32 | setState((oldState) => ({ ...oldState, pastHydration: true })); 33 | }, []); 34 | 35 | return {children}; 36 | }; 37 | -------------------------------------------------------------------------------- /src/ui/LazyBoundary.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { isBackend } from '../utils/detectBackend'; 4 | import { useIsClientPhase } from '../utils/useClientPhase'; 5 | 6 | const LazyServerBoundary: React.FC<{ 7 | fallback: NonNullable | null; 8 | }> = ({ children }) => {children}; 9 | 10 | const LazyClientBoundary: React.FC<{ 11 | fallback: NonNullable | null; 12 | }> = ({ children, fallback }) => ( 13 | 18 | {children} 19 | 20 | ); 21 | 22 | /** 23 | * React.Suspense "as-is" replacement. Automatically "removed" during SSR and "patched" to work accordingly on the clientside 24 | * 25 | * @see {@link HydrationController} has to wrap entire application in order to provide required information 26 | */ 27 | export const LazyBoundary = isBackend ? LazyServerBoundary : LazyClientBoundary; 28 | -------------------------------------------------------------------------------- /src/ui/Module.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { getLoadable } from '../loadable/loadable'; 4 | import { DefaultImport, FullImportModuleProps, HOCModuleType, ImportModuleProps } from '../types'; 5 | import { useImported } from './useImported'; 6 | 7 | /** 8 | * @deprecated use {@link useImported} instead 9 | */ 10 | export function ImportedModule(props: FullImportModuleProps): React.ReactElement | null { 11 | const { error, loadable, imported: module } = useImported(props.import); 12 | 13 | if (error) { 14 | throw error; 15 | } 16 | 17 | if (module) { 18 | return props.children(module); 19 | } 20 | 21 | if (!props.fallback) { 22 | throw loadable.resolution; 23 | } 24 | 25 | return props.fallback as any; 26 | } 27 | 28 | /** 29 | * @deprecated use {@link useImported} instead 30 | */ 31 | export function importedModule(loaderFunction: DefaultImport): HOCModuleType { 32 | const loadable = getLoadable(loaderFunction); 33 | 34 | const Module = ((props: ImportModuleProps) => ( 35 | 36 | )) as HOCModuleType; 37 | 38 | Module.preload = () => { 39 | loadable.load().catch(() => ({})); 40 | 41 | return loadable.resolution; 42 | }; 43 | 44 | Module.done = loadable.resolution; 45 | 46 | return Module; 47 | } 48 | -------------------------------------------------------------------------------- /src/ui/context.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { defaultStream } from '../loadable/stream'; 4 | import { Stream } from '../types'; 5 | 6 | export const streamContext = React.createContext(defaultStream); 7 | 8 | /** 9 | * SSR. Tracker for used marks 10 | */ 11 | export const ImportedStream: React.FC<{ 12 | stream: Stream; 13 | }> = ({ stream, children, ...props }) => { 14 | if (process.env.NODE_ENV !== 'development') { 15 | if ('takeUID' in props) { 16 | throw new Error('react-imported-component: `takeUID` was replaced by `stream`.'); 17 | } 18 | } 19 | 20 | return {children}; 21 | }; 22 | 23 | export const UIDConsumer = streamContext.Consumer; 24 | -------------------------------------------------------------------------------- /src/ui/useImported.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType, lazy, LazyExoticComponent, useCallback, useContext, useEffect, useMemo, useState } from 'react'; 2 | 3 | import { getLoadable } from '../loadable/loadable'; 4 | import { consumeMark } from '../loadable/marks'; 5 | import { isItReady } from '../loadable/pending'; 6 | import { InnerLoadable } from '../loadable/toLoadable'; 7 | import { DefaultComponentImport, DefaultImport, DefaultImportedComponent, Loadable } from '../types'; 8 | import { isBackend } from '../utils/detectBackend'; 9 | import { es6import } from '../utils/utils'; 10 | import { streamContext } from './context'; 11 | 12 | function loadLoadable(loadable: Loadable, callback: (l: any) => void) { 13 | const upd = () => callback({}); 14 | 15 | loadable.loadIfNeeded().then(upd, upd); 16 | } 17 | 18 | function updateLoadable(loadable: Loadable, callback: (l: any) => void) { 19 | // support HMR 20 | if (process.env.NODE_ENV === 'development') { 21 | const upd = () => callback({}); 22 | 23 | (loadable as InnerLoadable)._probeChanges().then((changed) => changed && upd(), upd); 24 | } 25 | } 26 | 27 | interface ImportedShape { 28 | imported?: T; 29 | error?: any; 30 | loading?: boolean; 31 | 32 | loadable: Loadable; 33 | 34 | retry(): void; 35 | } 36 | 37 | interface HookOptions { 38 | import?: boolean; 39 | track?: boolean; 40 | } 41 | 42 | /** 43 | * react hook to wrap `import` with a tracker 44 | * used by {@link useImported} 45 | * @internal 46 | */ 47 | export function useLoadable(loadable: Loadable, options: HookOptions = {}) { 48 | const UID = useContext(streamContext); 49 | 50 | const wasDone = loadable.done; 51 | 52 | const [, forceUpdate] = useState({}); 53 | 54 | useMemo(() => { 55 | if (options.import !== false) { 56 | if (options.track !== false) { 57 | consumeMark(UID, loadable.mark); 58 | } 59 | 60 | if (!wasDone) { 61 | loadLoadable(loadable, forceUpdate); 62 | } else { 63 | updateLoadable(loadable, forceUpdate); 64 | } 65 | } 66 | 67 | return true; 68 | }, [loadable, options.import, options.track]); 69 | 70 | if (isBackend && !isItReady() && loadable.isLoading()) { 71 | /* tslint:disable:next-line no-console */ 72 | console.error( 73 | 'react-imported-component: trying to render a component which is not ready. You should `await whenComponentsReady()`?' 74 | ); 75 | } 76 | 77 | // use mark 78 | // retry 79 | const retry = useCallback(() => { 80 | if (!loadable) { 81 | return; 82 | } 83 | 84 | loadable.reset(); 85 | forceUpdate({}); 86 | updateLoadable(loadable, forceUpdate); 87 | }, [loadable]); 88 | 89 | if (process.env.NODE_ENV !== 'production') { 90 | if (isBackend) { 91 | if (!loadable.done) { 92 | /* tslint:disable:next-line no-console */ 93 | console.error( 94 | 'react-imported-component: using not resolved loadable. You should `await whenComponentsReady()`.' 95 | ); 96 | } 97 | } 98 | } 99 | 100 | return useMemo( 101 | () => ({ 102 | loadable, 103 | retry, 104 | update: forceUpdate, 105 | }), 106 | [loadable, retry, forceUpdate] 107 | ); 108 | } 109 | 110 | /** 111 | * short version of {@link useImported} 112 | * @param {Function} importer - an function with `import` inside it 113 | * 114 | * @return {Object} 115 | * - imported: if non empty - the data is loaded 116 | * - error: if non empty - there is an error 117 | * - loading: if true - then it's still loading 118 | * - loadable: the under laying reference 119 | * - retry: retry if case of failure 120 | * 121 | * @example 122 | * const { imported: Imported, loadable } = useImported(importer); 123 | * if (Imported) { 124 | * // yep, it's imported 125 | * return ; 126 | * } 127 | * // else throw resolution 128 | * throw loadable.resolution; 129 | */ 130 | export function useImported(importer: DefaultImport | Loadable): ImportedShape; 131 | /** 132 | * The code splitting hook, the full version 133 | * @param {Function} importer - an function with `import` inside it 134 | * @param {Function} [exportPicker] - a "picker" of the export inside 135 | * @param {HookOptions} options 136 | * @param {Boolean} [options.import=true] - should the component be imported. Allow to defer execution. 137 | * @param {Boolean} [options.track=true] - allows disabling tracking of components, isolating them to SSR 138 | * 139 | * @return {Object} 140 | * - imported: if non empty - the data is loaded 141 | * - error: if non empty - there is an error 142 | * - loading: if true - then it's still loading 143 | * - loadable: the under laying reference 144 | * - retry: retry if case of failure 145 | * 146 | * @see if you dont need precise control consider(and loading Components) {@link useLazy} 147 | * 148 | * @example 149 | * const { imported: Imported, loadable } = useImported(importer, ({namedExport} => namedExport); 150 | * @example 151 | * const { imported: Imported, loadable } = useImported(importer); 152 | * if (Imported) { 153 | * // yep, it's imported 154 | * return ; 155 | * } 156 | * // else throw resolution 157 | * throw loadable.resolution; 158 | */ 159 | export function useImported( 160 | importer: DefaultImport | Loadable, 161 | exportPicker: undefined | ((x: T) => K), 162 | options?: HookOptions 163 | ): ImportedShape; 164 | 165 | export function useImported( 166 | importer: DefaultImport | Loadable, 167 | exportPicker: undefined | ((x: T) => K) = es6import, 168 | options: HookOptions = {} 169 | ): ImportedShape { 170 | const topLoadable = getLoadable(importer); 171 | const { loadable, retry } = useLoadable(topLoadable, options); 172 | 173 | const { error, done, payload } = loadable; 174 | const loading = loadable.isLoading(); 175 | 176 | return useMemo(() => { 177 | if (error) { 178 | return { 179 | error, 180 | loadable, 181 | retry, 182 | }; 183 | } 184 | 185 | if (done) { 186 | return { 187 | imported: exportPicker(payload as T), 188 | loadable, 189 | retry, 190 | }; 191 | } 192 | 193 | return { 194 | loading, 195 | loadable, 196 | retry, 197 | }; 198 | }, [error, loading, payload, loadable]); 199 | } 200 | 201 | /** 202 | * A mix of React.lazy and useImported - uses React.lazy for Component and `useImported` to track the promise 203 | * not "retry"-able 204 | * @see if you need precise control consider {@link useImported} 205 | * @example 206 | * const Component = useLazy(() => import('./MyComponent'); 207 | * return // throws to SuspenseBoundary if not ready 208 | */ 209 | export function useLazy(importer: DefaultComponentImport): LazyExoticComponent> { 210 | const [{ resolve, reject, lazyComponent }] = useState(() => { 211 | /* tslint:disable no-shadowed-variable */ 212 | let resolve: any; 213 | let reject: any; 214 | const promise = new Promise>((rs, rej) => { 215 | resolve = rs; 216 | reject = rej; 217 | }); 218 | 219 | return { 220 | resolve, 221 | reject, 222 | lazyComponent: lazy(() => promise), 223 | }; 224 | /* tslint:enable */ 225 | }); 226 | 227 | const { error, imported } = useImported(importer); 228 | 229 | useEffect(() => { 230 | if (error) { 231 | reject!(error); 232 | } 233 | 234 | if (imported) { 235 | resolve!(imported); 236 | } 237 | }, [error, imported]); 238 | 239 | return lazyComponent; 240 | } 241 | -------------------------------------------------------------------------------- /src/utils/detectBackend.ts: -------------------------------------------------------------------------------- 1 | import { isNode } from 'detect-node-es'; 2 | 3 | export const isBackend = isNode || typeof window === 'undefined'; 4 | -------------------------------------------------------------------------------- /src/utils/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * helper function to remap imports 3 | * @param x 4 | * @param map 5 | */ 6 | export function remapImports(x: Promise, map: (a: T) => Y): Promise { 7 | return x.then(map); 8 | } 9 | -------------------------------------------------------------------------------- /src/utils/signatures.ts: -------------------------------------------------------------------------------- 1 | import { AnyFunction, Mark } from '../types'; 2 | 3 | const trimImport = (str: string) => str.replace(/['"]/g, ''); 4 | 5 | export const importMatch = (functionString: string): Mark => { 6 | const markMatches = functionString.match(/`imported_(.*?)_component`/g) || []; 7 | 8 | return markMatches.map((match) => match && trimImport((match.match(/`imported_(.*?)_component`/i) || [])[1])); 9 | }; 10 | 11 | /** 12 | * the intention of this function is to "clear some (minification) noise from the function 13 | * basically from file to file different "short" names could be used 14 | * @param fn 15 | */ 16 | export const getFunctionSignature = (fn: AnyFunction | string): string => 17 | String(fn) 18 | // quotes 19 | .replace(/(["'])/g, '`') 20 | 21 | // comments 22 | .replace(/\/\*([^\*]*)\*\//gi, '') 23 | 24 | // webpack specific 25 | .replace(/Promise.resolve\([^)]*\)/, '-we()') 26 | .replace(/\w+.e\([^)]*\)/, '-we()') 27 | .replace(/\w+.\w.bind\(/, '-wbind(') 28 | .replace(/\w+.bind\(/, '-wbind(') 29 | 30 | // prefix imported 31 | .replace(/([A-z0-9_]+)\(`imported_/g, '$(`imported_'); 32 | -------------------------------------------------------------------------------- /src/utils/useClientPhase.tsx: -------------------------------------------------------------------------------- 1 | import { useContext } from 'react'; 2 | 3 | import { importedState } from '../ui/ImportedController'; 4 | 5 | /** 6 | * returns "true" if currently is a "client" phase and all features should be active 7 | * @see {@link HydrationController} 8 | */ 9 | export const useIsClientPhase = (): boolean => { 10 | const value = useContext(importedState); 11 | 12 | if (!value) { 13 | if (process.env.NODE_ENV !== 'production') { 14 | // tslint:disable-next-line:no-console 15 | console.warn('react-imported-component: please wrap your entire application with ImportedController'); 16 | } 17 | 18 | return true; 19 | } 20 | 21 | return value.pastHydration; 22 | }; 23 | -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { Default } from '../types'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/ban-types 4 | type ObjectOrFunction = object | (() => any); 5 | 6 | export function asDefault(mayBeNotDefault: T | Default): Default { 7 | if ('default' in mayBeNotDefault) { 8 | return mayBeNotDefault; 9 | } 10 | 11 | return { 12 | default: mayBeNotDefault, 13 | }; 14 | } 15 | 16 | export const es6import = (module: any) => (module.default ? module.default : module); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "allowSyntheticDefaultImports": true, 5 | "strict": true, 6 | "strictNullChecks": true, 7 | "strictFunctionTypes": true, 8 | "noImplicitThis": true, 9 | "alwaysStrict": true, 10 | "noUnusedLocals": true, 11 | "noUnusedParameters": true, 12 | "noImplicitReturns": true, 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "removeComments": false, 16 | "importHelpers": true, 17 | "target": "es6", 18 | "moduleResolution": "node", 19 | "lib": ["dom", "es5", "scripthost", "es2015.collection", "es2015.symbol", "es2015.iterable", "es2015.promise"], 20 | "types": ["node", "jest"], 21 | "typeRoots": ["./node_modules/@types"], 22 | "jsx": "react" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-react", "tslint-react-hooks", "tslint-config-prettier"], 3 | "linterOptions": { 4 | "exclude": ["node_modules/**/*.ts"] 5 | }, 6 | "rules": { 7 | "no-bitwise": false, 8 | "quotemark": [true, "single", "jsx-double"], 9 | "no-unused-expression": [true, "allow-fast-null-checks"], 10 | "object-literal-sort-keys": false, 11 | "interface-name": false, 12 | "no-var-requires": false, 13 | "jsx-no-lambda": false, 14 | "max-classes-per-file": false, 15 | "jsx-self-close": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /wrapper.js: -------------------------------------------------------------------------------- 1 | var importedWrapper = function(marker, realImport) { 2 | if (typeof __deoptimization_sideEffect__ !== 'undefined') { 3 | __deoptimization_sideEffect__(marker, realImport); 4 | } 5 | return realImport; 6 | }; 7 | 8 | module.exports = importedWrapper; 9 | --------------------------------------------------------------------------------