├── .npmrc ├── .gitignore ├── src ├── app │ ├── Button.css │ ├── Button.jsx │ ├── Panel.jsx │ └── index.jsx ├── index.html ├── importMap.json ├── index-dev.html ├── index.js └── sw.js ├── .travis.yml ├── README.md ├── package.json └── .babelrc.js /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | package-lock=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | build 3 | node_modules 4 | npm-debug.log -------------------------------------------------------------------------------- /src/app/Button.css: -------------------------------------------------------------------------------- 1 | button { 2 | padding: 20px; 3 | color: red; 4 | border: 1px solid red; 5 | } -------------------------------------------------------------------------------- /src/app/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './Button.css'; 3 | 4 | export const Button = ({children}) => ; -------------------------------------------------------------------------------- /src/app/Panel.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Button} from './Button'; 3 | 4 | export const Panel = () =>
; -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/importMap.json: -------------------------------------------------------------------------------- 1 | { 2 | "imports": { 3 | "react": "../node_modules/react/umd/react.development.js", 4 | "react-dom": "../node_modules/react-dom/umd/react-dom.development.js" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/index-dev.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: stable 4 | 5 | cache: 6 | npm: true 7 | 8 | script: 9 | - ./build.sh 10 | - 'echo "exclude: []" > build/_config.yml' # @see https://github.blog/2016-11-02-what-s-new-in-github-pages-with-jekyll-3-3/ 11 | 12 | deploy: 13 | - provider: pages 14 | skip-cleanup: true 15 | github-token: $GITHUB_TOKEN 16 | local-dir: build 17 | on: 18 | branch: master -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Main reason to use [ES Module Shims](https://github.com/guybedford/es-module-shims) here is to get [Import Maps](https://github.com/WICG/import-maps) support. 2 | 3 | UMD is not compatible with ES modules: https://github.com/umdjs/umd/issues/124 4 | 5 | https://github.com/guybedford/es-module-shims/issues/18 6 | https://stackoverflow.com/questions/49054293/serviceworker-fails-on-hard-reload-ctrl-shift-r-in-chrome -------------------------------------------------------------------------------- /src/app/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | 4 | (async () => { 5 | const {Panel} = await import('./Panel'); 6 | const {Button} = await import('./Button'); 7 | const root = document.getElementById('root'); 8 | ReactDOM.render(( 9 |
10 | 11 | 12 |
13 | ), root); 14 | })(); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pure-react-with-dynamic-imports", 3 | "version": "1.0.0", 4 | "description": "Pure react with dynamic imports in the browser", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "build": "babel src/app --out-dir build/app --source-maps --copy-files" 8 | }, 9 | "author": "Kirill Konshin", 10 | "license": "ISC", 11 | "dependencies": { 12 | "es-module-shims": "0.2.7", 13 | "react": "16.8.6", 14 | "react-dom": "16.8.6" 15 | }, 16 | "devDependencies": { 17 | "@babel/cli": "7.4.4", 18 | "@babel/core": "7.4.5", 19 | "@babel/plugin-syntax-dynamic-import": "7.2.0", 20 | "@babel/preset-react": "7.0.0", 21 | "@types/react": "16.8.20", 22 | "@babel/standalone": "7.4.5", 23 | "babel-plugin-module-resolver": "3.2.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.babelrc.js: -------------------------------------------------------------------------------- 1 | const {resolvePath} = require('babel-plugin-module-resolver'); 2 | 3 | module.exports = { 4 | presets: [ 5 | '@babel/preset-react' 6 | ], 7 | plugins: [ 8 | '@babel/plugin-syntax-dynamic-import', 9 | [ 10 | 'babel-plugin-module-resolver', 11 | { 12 | alias: { 13 | 'react': './node_modules/react/umd/react.production.min.js', 14 | 'react-dom': './node_modules/react-dom/umd/react-dom.production.min.js' 15 | }, 16 | //FIXME here should be cwd: 'build' but it does not work, so we just replace as follows to make sure we stay in build dir 17 | resolvePath: (sourcePath, currentFile, opts) => resolvePath(sourcePath, currentFile, opts).replace('../../', '../') 18 | } 19 | ] 20 | ] 21 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | if ('serviceWorker' in navigator) { 2 | (async () => { 3 | 4 | try { 5 | 6 | const production = !window.location.toString().includes('index-dev.html'); 7 | 8 | const config = { 9 | globalMap: { 10 | 'react': 'React', 11 | 'react-dom': 'ReactDOM' 12 | }, 13 | production 14 | }; 15 | 16 | const registration = await navigator.serviceWorker.register('sw.js?' + JSON.stringify(config)); 17 | 18 | await navigator.serviceWorker.ready; 19 | 20 | const launch = async () => { 21 | if (production) { 22 | await import("./app/index.js"); 23 | } else { 24 | await import("./app/index.jsx"); 25 | } 26 | }; 27 | 28 | // this launches the React app if the SW has been installed before or immediately after registration 29 | // https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim 30 | if (navigator.serviceWorker.controller) { 31 | await launch(); 32 | } else { 33 | navigator.serviceWorker.addEventListener('controllerchange', launch); 34 | } 35 | 36 | } catch (error) { 37 | console.error('Service worker registration failed', error.stack); 38 | } 39 | 40 | })(); 41 | } else { 42 | alert('Service Worker is not supported'); 43 | } 44 | -------------------------------------------------------------------------------- /src/sw.js: -------------------------------------------------------------------------------- 1 | const {globalMap, production} = JSON.parse((decodeURIComponent(self.location.search) || '?{}').substr(1)); 2 | 3 | if (!production) importScripts('../node_modules/@babel/standalone/babel.js'); 4 | 5 | //this is needed to activate the worker immediately without reload 6 | //@see https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle#clientsclaim 7 | self.addEventListener('activate', event => event.waitUntil(clients.claim())); 8 | 9 | const getGlobalByUrl = (url) => Object.keys(globalMap).reduce((res, key) => { 10 | if (res) return res; 11 | if (matchUrl(url, key)) return globalMap[key]; 12 | return res; 13 | }, null); 14 | 15 | const matchUrl = (url, key) => url.includes(`/${key}/`); 16 | 17 | const removeSpaces = str => str.split(/^ +/m).join('').trim(); 18 | 19 | const headers = new Headers({ 20 | 'Content-Type': 'application/javascript' 21 | }); 22 | 23 | self.addEventListener('fetch', (event) => { 24 | 25 | let {request: {url}} = event; 26 | 27 | const fileName = url.split('/').pop(); 28 | const ext = fileName.includes('.') ? url.split('.').pop() : ''; 29 | 30 | if (!ext && !url.endsWith('/')) { 31 | url = url + '.' + (production ? 'js' : 'jsx'); 32 | } 33 | 34 | console.log('Req', url, ext); 35 | 36 | if (globalMap && Object.keys(globalMap).some(key => matchUrl(url, key))) { 37 | event.respondWith( 38 | fetch(url) 39 | .then(response => response.text()) 40 | .then(body => { 41 | console.log('JS', url); 42 | return body; 43 | }) 44 | .then(body => new Response(removeSpaces(` 45 | const head = document.getElementsByTagName('head')[0]; 46 | const script = document.createElement('script'); 47 | script.setAttribute('type', 'text/javascript'); 48 | script.appendChild(document.createTextNode(${JSON.stringify(body)})); 49 | head.appendChild(script); 50 | export default window.${getGlobalByUrl(url)}; 51 | `), {headers} 52 | )) 53 | ) 54 | } else if (url.endsWith('.css')) { 55 | event.respondWith( 56 | fetch(url) 57 | .then(response => response.text()) 58 | .then(body => new Response(removeSpaces(` 59 | //TODO We don't track instances, so 2x imports will result in 2x style tags 60 | const head = document.getElementsByTagName('head')[0]; 61 | const style = document.createElement('style'); 62 | style.setAttribute('type', 'text/css'); 63 | style.appendChild(document.createTextNode(${JSON.stringify(body)})); 64 | head.appendChild(style); 65 | export default null; //TODO here we can export CSS module instead 66 | `), 67 | {headers} 68 | )) 69 | ) 70 | } else if (url.endsWith('.jsx')) { 71 | event.respondWith( 72 | fetch(url) 73 | .then(response => response.text()) 74 | .then(body => new Response( 75 | //TODO Cache 76 | Babel.transform(body, { 77 | presets: [ 78 | 'react', 79 | ], 80 | plugins: [ 81 | 'syntax-dynamic-import' 82 | ], 83 | sourceMaps: true 84 | }).code, 85 | {headers} 86 | )) 87 | ) 88 | } else if (url.endsWith('.js')) { // rewrite for import('./Panel') with no extension 89 | event.respondWith( 90 | fetch(url) 91 | .then(response => response.text()) 92 | .then(body => new Response(body, {headers})) 93 | ) 94 | } 95 | 96 | }); --------------------------------------------------------------------------------