├── .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 |
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 | });
--------------------------------------------------------------------------------