├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── snowpack.config.json ├── src ├── _redirects ├── es-module-shims.js ├── icons │ └── sample.png ├── index.html ├── index.js ├── manifest.webmanifest └── routes │ ├── about │ └── index.js │ └── home │ └── index.js └── workbox-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | src/web_modules/ 3 | src/sw.js 4 | src/workbox-*.js 5 | src/workbox-*.js.map 6 | src/sw.js.map 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # es-react-pwa 2 | 3 | minimalist and modern react boilerplate designed for minimal configuration. 4 | 5 | [demo](https://es-react-pwa.netlify.com/) 6 | 7 | ## usage 8 | 9 | ### Install and build 10 | 11 | ```js 12 | npm install && npm run build && npm run pwa 13 | ``` 14 | 15 | ### Serve locally 16 | 17 | ```js 18 | npm run serve 19 | ``` 20 | 21 | ## contents: 22 | 23 | * [snowpack](https://snowpack.dev) 24 | * [styled-components](https://styled-components.com) 25 | * [es-module-shims](https://github.com/guybedford/es-module-shims) 26 | * [kv-storage](https://github.com/WICG/kv-storage) 27 | * [workbox](https://developers.google.com/web/tools/workbox) 28 | * [htm](https://github.com/developit/htm) 29 | 30 | ## concepts: 31 | 32 | ### import maps 33 | 34 | included is es module shims, which includes polyfill support for `importmaps`. these allow direct global imports. this is key in not requiring babel. 35 | 36 | ### web modules (snowpack) 37 | 38 | snowpack allows you to treeshake es module dependencies in a convinient way. there is a production command that will do the treeshaking, and a post npm install step which will localize the es modules. 39 | 40 | ```sh 41 | npm run prepare 42 | ``` 43 | 44 | ```sh 45 | npm run optimize 46 | ``` 47 | 48 | ### progressive web app (workbox) 49 | 50 | workbox is configured to be run during build to generate the caching configuration. 51 | 52 | intialization: 53 | 54 | ```sh 55 | npm run pwa:init 56 | ``` 57 | 58 | to generate caching with each build 59 | 60 | ```sh 61 | npm run pwa 62 | ``` 63 | 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "es-react-pwa", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "clean": "rm -rf node_modules && rm -rf src/web_modules && rm package-lock.json", 8 | "serve": "servor src", 9 | "prepare": "snowpack --dest \"src/web_modules/\"", 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "optimize": "snowpack build --dest \"src/web_modules/\"", 12 | "pwa": "workbox generateSW workbox-config.js", 13 | "pwa:init": "workbox wizard" 14 | }, 15 | "precommit": [ 16 | "optimize", 17 | "pwa" 18 | ], 19 | "author": "Matt Hoffner", 20 | "license": "ISC", 21 | "devDependencies": { 22 | "servor": "^4.0.2", 23 | "snowpack": "^2.0.0", 24 | "workbox-cli": "latest" 25 | }, 26 | "dependencies": { 27 | "htm": "^2.2.1", 28 | "react": "^16.0.0", 29 | "react-dom": "^16.0.0", 30 | "styled-components": "^4.4.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /snowpack.config.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "install": [ 4 | "htm", 5 | "react", 6 | "react-dom", 7 | "styled-components" 8 | ], 9 | "scripts": { 10 | "mount:public": "mount public --to /", 11 | "mount:web_modules": "mount web_modules --to /src" 12 | } 13 | } -------------------------------------------------------------------------------- /src/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /src/es-module-shims.js: -------------------------------------------------------------------------------- 1 | /* ES Module Shims 0.4.5 */ 2 | (function () { 3 | 'use strict'; 4 | 5 | const resolvedPromise = Promise.resolve(); 6 | 7 | let baseUrl; 8 | 9 | function createBlob (source) { 10 | return URL.createObjectURL(new Blob([source], { type: 'application/javascript' })); 11 | } 12 | 13 | const hasDocument = typeof document !== 'undefined'; 14 | 15 | // support browsers without dynamic import support (eg Firefox 6x) 16 | let dynamicImport; 17 | try { 18 | dynamicImport = (0, eval)('u=>import(u)'); 19 | } 20 | catch (e) { 21 | if (hasDocument) { 22 | self.addEventListener('error', e => importShim.e = e.error); 23 | dynamicImport = blobUrl => { 24 | const topLevelBlobUrl = createBlob( 25 | `import*as m from'${blobUrl}';self.importShim.l=m;self.importShim.e=null` 26 | ); 27 | const s = document.createElement('script'); 28 | s.type = 'module'; 29 | s.src = topLevelBlobUrl; 30 | document.head.appendChild(s); 31 | return new Promise((resolve, reject) => { 32 | s.addEventListener('load', () => { 33 | document.head.removeChild(s); 34 | importShim.e ? reject(importShim.e) : resolve(importShim.l, baseUrl); 35 | }); 36 | }); 37 | }; 38 | } 39 | } 40 | 41 | if (hasDocument) { 42 | const baseEl = document.querySelector('base[href]'); 43 | if (baseEl) 44 | baseUrl = baseEl.href; 45 | } 46 | 47 | if (!baseUrl && typeof location !== 'undefined') { 48 | baseUrl = location.href.split('#')[0].split('?')[0]; 49 | const lastSepIndex = baseUrl.lastIndexOf('/'); 50 | if (lastSepIndex !== -1) 51 | baseUrl = baseUrl.slice(0, lastSepIndex + 1); 52 | } 53 | 54 | let esModuleShimsSrc; 55 | if (hasDocument) { 56 | esModuleShimsSrc = document.currentScript && document.currentScript.src; 57 | } 58 | 59 | const backslashRegEx = /\\/g; 60 | function resolveIfNotPlainOrUrl (relUrl, parentUrl) { 61 | // strip off any trailing query params or hashes 62 | parentUrl = parentUrl && parentUrl.split('#')[0].split('?')[0]; 63 | if (relUrl.indexOf('\\') !== -1) 64 | relUrl = relUrl.replace(backslashRegEx, '/'); 65 | // protocol-relative 66 | if (relUrl[0] === '/' && relUrl[1] === '/') { 67 | return parentUrl.slice(0, parentUrl.indexOf(':') + 1) + relUrl; 68 | } 69 | // relative-url 70 | else if (relUrl[0] === '.' && (relUrl[1] === '/' || relUrl[1] === '.' && (relUrl[2] === '/' || relUrl.length === 2 && (relUrl += '/')) || 71 | relUrl.length === 1 && (relUrl += '/')) || 72 | relUrl[0] === '/') { 73 | const parentProtocol = parentUrl.slice(0, parentUrl.indexOf(':') + 1); 74 | // Disabled, but these cases will give inconsistent results for deep backtracking 75 | //if (parentUrl[parentProtocol.length] !== '/') 76 | // throw new Error('Cannot resolve'); 77 | // read pathname from parent URL 78 | // pathname taken to be part after leading "/" 79 | let pathname; 80 | if (parentUrl[parentProtocol.length + 1] === '/') { 81 | // resolving to a :// so we need to read out the auth and host 82 | if (parentProtocol !== 'file:') { 83 | pathname = parentUrl.slice(parentProtocol.length + 2); 84 | pathname = pathname.slice(pathname.indexOf('/') + 1); 85 | } 86 | else { 87 | pathname = parentUrl.slice(8); 88 | } 89 | } 90 | else { 91 | // resolving to :/ so pathname is the /... part 92 | pathname = parentUrl.slice(parentProtocol.length + (parentUrl[parentProtocol.length] === '/')); 93 | } 94 | 95 | if (relUrl[0] === '/') 96 | return parentUrl.slice(0, parentUrl.length - pathname.length - 1) + relUrl; 97 | 98 | // join together and split for removal of .. and . segments 99 | // looping the string instead of anything fancy for perf reasons 100 | // '../../../../../z' resolved to 'x/y' is just 'z' 101 | const segmented = pathname.slice(0, pathname.lastIndexOf('/') + 1) + relUrl; 102 | 103 | const output = []; 104 | let segmentIndex = -1; 105 | for (let i = 0; i < segmented.length; i++) { 106 | // busy reading a segment - only terminate on '/' 107 | if (segmentIndex !== -1) { 108 | if (segmented[i] === '/') { 109 | output.push(segmented.slice(segmentIndex, i + 1)); 110 | segmentIndex = -1; 111 | } 112 | } 113 | 114 | // new segment - check if it is relative 115 | else if (segmented[i] === '.') { 116 | // ../ segment 117 | if (segmented[i + 1] === '.' && (segmented[i + 2] === '/' || i + 2 === segmented.length)) { 118 | output.pop(); 119 | i += 2; 120 | } 121 | // ./ segment 122 | else if (segmented[i + 1] === '/' || i + 1 === segmented.length) { 123 | i += 1; 124 | } 125 | else { 126 | // the start of a new segment as below 127 | segmentIndex = i; 128 | } 129 | } 130 | // it is the start of a new segment 131 | else { 132 | segmentIndex = i; 133 | } 134 | } 135 | // finish reading out the last segment 136 | if (segmentIndex !== -1) 137 | output.push(segmented.slice(segmentIndex)); 138 | return parentUrl.slice(0, parentUrl.length - pathname.length) + output.join(''); 139 | } 140 | } 141 | 142 | /* 143 | * Import maps implementation 144 | * 145 | * To make lookups fast we pre-resolve the entire import map 146 | * and then match based on backtracked hash lookups 147 | * 148 | */ 149 | const emptyImportMap = { imports: {}, scopes: {} }; 150 | 151 | function resolveUrl (relUrl, parentUrl) { 152 | return resolveIfNotPlainOrUrl(relUrl, parentUrl) || (relUrl.indexOf(':') !== -1 ? relUrl : resolveIfNotPlainOrUrl('./' + relUrl, parentUrl)); 153 | } 154 | 155 | async function hasStdModule (name) { 156 | try { 157 | await dynamicImport(name); 158 | return true; 159 | } 160 | catch (e) { 161 | return false; 162 | } 163 | } 164 | 165 | async function resolveAndComposePackages (packages, outPackages, baseUrl, parentMap, parentUrl) { 166 | outer: for (let p in packages) { 167 | const resolvedLhs = resolveIfNotPlainOrUrl(p, baseUrl) || p; 168 | let target = packages[p]; 169 | if (typeof target === 'string') 170 | target = [target]; 171 | else if (!Array.isArray(target)) 172 | continue; 173 | 174 | for (const rhs of target) { 175 | if (typeof rhs !== 'string') 176 | continue; 177 | const mapped = resolveImportMap(parentMap, resolveIfNotPlainOrUrl(rhs, baseUrl) || rhs, parentUrl); 178 | if (mapped && (!mapped.startsWith('std:') || await hasStdModule(mapped))) { 179 | outPackages[resolvedLhs] = mapped; 180 | continue outer; 181 | } 182 | } 183 | targetWarning(p, packages[p], 'bare specifier did not resolve'); 184 | } 185 | } 186 | 187 | async function resolveAndComposeImportMap (json, baseUrl, parentMap) { 188 | const outMap = { imports: Object.assign({}, parentMap.imports), scopes: Object.assign({}, parentMap.scopes) }; 189 | 190 | if (json.imports) 191 | await resolveAndComposePackages(json.imports, outMap.imports, baseUrl, parentMap, null); 192 | 193 | if (json.scopes) 194 | for (let s in json.scopes) { 195 | const resolvedScope = resolveUrl(s, baseUrl); 196 | await resolveAndComposePackages(json.scopes[s], outMap.scopes[resolvedScope] || (outMap.scopes[resolvedScope] = {}), baseUrl, parentMap, resolvedScope); 197 | } 198 | 199 | return outMap; 200 | } 201 | 202 | function getMatch (path, matchObj) { 203 | if (matchObj[path]) 204 | return path; 205 | let sepIndex = path.length; 206 | do { 207 | const segment = path.slice(0, sepIndex + 1); 208 | if (segment in matchObj) 209 | return segment; 210 | } while ((sepIndex = path.lastIndexOf('/', sepIndex - 1)) !== -1) 211 | } 212 | 213 | function applyPackages (id, packages) { 214 | const pkgName = getMatch(id, packages); 215 | if (pkgName) { 216 | const pkg = packages[pkgName]; 217 | if (pkg === null) return; 218 | if (id.length > pkgName.length && pkg[pkg.length - 1] !== '/') 219 | targetWarning(pkgName, pkg, "should have a trailing '/'"); 220 | else 221 | return pkg + id.slice(pkgName.length); 222 | } 223 | } 224 | 225 | function targetWarning (match, target, msg) { 226 | console.warn("Package target " + msg + ", resolving target '" + target + "' for " + match); 227 | } 228 | 229 | function resolveImportMap (importMap, resolvedOrPlain, parentUrl) { 230 | let scopeUrl = parentUrl && getMatch(parentUrl, importMap.scopes); 231 | while (scopeUrl) { 232 | const packageResolution = applyPackages(resolvedOrPlain, importMap.scopes[scopeUrl]); 233 | if (packageResolution) 234 | return packageResolution; 235 | scopeUrl = getMatch(scopeUrl.slice(0, scopeUrl.lastIndexOf('/')), importMap.scopes); 236 | } 237 | return applyPackages(resolvedOrPlain, importMap.imports) || resolvedOrPlain.indexOf(':') !== -1 && resolvedOrPlain; 238 | } 239 | 240 | /* es-module-lexer 0.3.13 */ 241 | function parse(Q,B="@"){if(!A)return init.then(()=>parse(Q));const C=(A.__heap_base.value||A.__heap_base)+4*Q.length+-A.memory.buffer.byteLength;if(C>0&&A.memory.grow(Math.ceil(C/65536)),function(A,Q){const B=A.length;let C=0;for(;C"function"==typeof atob?Uint8Array.from(atob(A),A=>A.charCodeAt(0)):Buffer.from(A,"base64"))("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")).then(WebAssembly.instantiate).then(({exports:Q})=>{A=Q;}); 242 | 243 | class WorkerShim { 244 | constructor(aURL, options = {}) { 245 | if (options.type !== 'module') 246 | return new Worker(aURL, options); 247 | 248 | if (!esModuleShimsSrc) 249 | throw new Error('es-module-shims.js must be loaded with a script tag for WorkerShim support.'); 250 | 251 | options.importMap = options.importMap || emptyImportMap; 252 | 253 | const workerScriptUrl = createBlob( 254 | `importScripts('${esModuleShimsSrc}');importShim.map=${JSON.stringify(options.importMap)};importShim('${new URL(aURL, baseUrl).href}').catch(e=>setTimeout(()=>{throw e}))` 255 | ); 256 | 257 | return new Worker(workerScriptUrl, Object.assign({}, options, { type: undefined })); 258 | } 259 | } 260 | 261 | let id = 0; 262 | const registry = {}; 263 | 264 | async function loadAll (load, seen) { 265 | if (load.b || seen[load.u]) 266 | return; 267 | seen[load.u] = 1; 268 | await load.L; 269 | return Promise.all(load.d.map(dep => loadAll(dep, seen))); 270 | } 271 | 272 | async function topLevelLoad (url, source) { 273 | await init; 274 | const load = getOrCreateLoad(url, source); 275 | const seen = {}; 276 | await loadAll(load, seen); 277 | lastLoad = undefined; 278 | resolveDeps(load, seen); 279 | const module = await dynamicImport(load.b); 280 | // if the top-level load is a shell, run its update function 281 | if (load.s) 282 | (await dynamicImport(load.s)).u$_(module); 283 | return module; 284 | } 285 | 286 | async function importShim$1 (id, parentUrl) { 287 | return topLevelLoad(await resolve(id, parentUrl || baseUrl)); 288 | } 289 | 290 | self.importShim = importShim$1; 291 | 292 | const meta = {}; 293 | const wasmModules = {}; 294 | 295 | const edge = navigator.userAgent.match(/Edge\/\d\d\.\d+$/); 296 | 297 | Object.defineProperties(importShim$1, { 298 | map: { value: emptyImportMap, writable: true }, 299 | m: { value: meta }, 300 | w: { value: wasmModules }, 301 | l: { value: undefined, writable: true }, 302 | e: { value: undefined, writable: true } 303 | }); 304 | importShim$1.fetch = url => fetch(url); 305 | 306 | let lastLoad; 307 | function resolveDeps (load, seen) { 308 | if (load.b || !seen[load.u]) 309 | return; 310 | seen[load.u] = 0; 311 | 312 | for (const dep of load.d) 313 | resolveDeps(dep, seen); 314 | 315 | // "execution" 316 | const source = load.S; 317 | // edge doesnt execute sibling in order, so we fix this up by ensuring all previous executions are explicit dependencies 318 | let resolvedSource = edge && lastLoad ? `import '${lastLoad}';` : ''; 319 | 320 | const [imports] = load.a; 321 | 322 | if (!imports.length) { 323 | resolvedSource += source; 324 | } 325 | else { 326 | // once all deps have loaded we can inline the dependency resolution blobs 327 | // and define this blob 328 | let lastIndex = 0, depIndex = 0; 329 | for (const { s: start, e: end, d: dynamicImportIndex } of imports) { 330 | // dependency source replacements 331 | if (dynamicImportIndex === -1) { 332 | const depLoad = load.d[depIndex++]; 333 | let blobUrl = depLoad.b; 334 | if (!blobUrl) { 335 | // circular shell creation 336 | if (!(blobUrl = depLoad.s)) { 337 | blobUrl = depLoad.s = createBlob(`export function u$_(m){${ 338 | depLoad.a[1].map( 339 | name => name === 'default' ? `$_default=m.default` : `${name}=m.${name}` 340 | ).join(',') 341 | }}${ 342 | depLoad.a[1].map(name => 343 | name === 'default' ? `let $_default;export{$_default as default}` : `export let ${name}` 344 | ).join(';') 345 | }\n//# sourceURL=${depLoad.r}?cycle`); 346 | } 347 | } 348 | // circular shell execution 349 | else if (depLoad.s) { 350 | resolvedSource += source.slice(lastIndex, start - 1) + '/*' + source.slice(start - 1, end + 1) + '*/' + source.slice(start - 1, start) + blobUrl + source[end] + `;import*as m$_${depIndex} from'${depLoad.b}';import{u$_ as u$_${depIndex}}from'${depLoad.s}';u$_${depIndex}(m$_${depIndex})`; 351 | lastIndex = end + 1; 352 | depLoad.s = undefined; 353 | continue; 354 | } 355 | resolvedSource += source.slice(lastIndex, start - 1) + '/*' + source.slice(start - 1, end + 1) + '*/' + source.slice(start - 1, start) + blobUrl; 356 | lastIndex = end; 357 | } 358 | // import.meta 359 | else if (dynamicImportIndex === -2) { 360 | meta[load.r] = { url: load.r }; 361 | resolvedSource += source.slice(lastIndex, start) + 'importShim.m[' + JSON.stringify(load.r) + ']'; 362 | lastIndex = end; 363 | } 364 | // dynamic import 365 | else { 366 | resolvedSource += source.slice(lastIndex, dynamicImportIndex + 6) + 'Shim(' + source.slice(start, end) + ', ' + JSON.stringify(load.r); 367 | lastIndex = end; 368 | } 369 | } 370 | 371 | resolvedSource += source.slice(lastIndex); 372 | } 373 | 374 | let sourceMappingResolved = ''; 375 | const sourceMappingIndex = resolvedSource.lastIndexOf('//# sourceMappingURL='); 376 | if (sourceMappingIndex > -1) { 377 | const sourceMappingEnd = resolvedSource.indexOf('\n',sourceMappingIndex); 378 | const sourceMapping = resolvedSource.slice(sourceMappingIndex, sourceMappingEnd > -1 ? sourceMappingEnd : undefined); 379 | sourceMappingResolved = `\n//# sourceMappingURL=` + resolveUrl(sourceMapping.slice(21), load.r); 380 | } 381 | load.b = lastLoad = createBlob(resolvedSource + sourceMappingResolved + '\n//# sourceURL=' + load.r); 382 | load.S = undefined; 383 | } 384 | 385 | function getOrCreateLoad (url, source) { 386 | let load = registry[url]; 387 | if (load) 388 | return load; 389 | 390 | load = registry[url] = { 391 | // url 392 | u: url, 393 | // response url 394 | r: undefined, 395 | // fetchPromise 396 | f: undefined, 397 | // source 398 | S: undefined, 399 | // linkPromise 400 | L: undefined, 401 | // analysis 402 | a: undefined, 403 | // deps 404 | d: undefined, 405 | // blobUrl 406 | b: undefined, 407 | // shellUrl 408 | s: undefined, 409 | }; 410 | 411 | if (url.startsWith('std:')) 412 | return Object.assign(load, { 413 | r: url, 414 | f: resolvedPromise, 415 | L: resolvedPromise, 416 | b: url 417 | }); 418 | 419 | load.f = (async () => { 420 | if (!source) { 421 | const res = await importShim$1.fetch(url); 422 | if (!res.ok) 423 | throw new Error(`${res.status} ${res.statusText} ${res.url}`); 424 | 425 | load.r = res.url; 426 | 427 | const contentType = res.headers.get('content-type'); 428 | if (contentType.match(/^(text|application)\/(x-)?javascript(;|$)/)) { 429 | source = await res.text(); 430 | } 431 | else if (contentType.match(/^application\/json(;|$)/)) { 432 | source = `export default JSON.parse(${JSON.stringify(await res.text())})`; 433 | } 434 | else if (contentType.match(/^text\/css(;|$)/)) { 435 | source = `const s=new CSSStyleSheet();s.replaceSync(${JSON.stringify(await res.text())});export default s`; 436 | } 437 | else if (contentType.match(/^application\/wasm(;|$)/)) { 438 | const module = wasmModules[url] = await WebAssembly.compile(await res.arrayBuffer()); 439 | let deps = WebAssembly.Module.imports ? WebAssembly.Module.imports(module).map(impt => impt.module) : []; 440 | 441 | const aDeps = []; 442 | load.a = [aDeps, WebAssembly.Module.exports(module).map(expt => expt.name)]; 443 | 444 | const depStrs = deps.map(dep => JSON.stringify(dep)); 445 | 446 | let curIndex = 0; 447 | load.S = depStrs.map((depStr, idx) => { 448 | const index = idx.toString(); 449 | const strStart = curIndex + 17 + index.length; 450 | const strEnd = strStart + depStr.length - 2; 451 | aDeps.push({ 452 | s: strStart, 453 | e: strEnd, 454 | d: -1 455 | }); 456 | curIndex += strEnd + 3; 457 | return `import*as m${index} from${depStr};` 458 | }).join('') + 459 | `const module=importShim.w[${JSON.stringify(url)}],exports=new WebAssembly.Instance(module,{` + 460 | depStrs.map((depStr, idx) => `${depStr}:m${idx},`).join('') + 461 | `}).exports;` + 462 | load.a[1].map(name => name === 'default' ? `export default exports.${name}` : `export const ${name}=exports.${name}`).join(';'); 463 | return deps; 464 | } 465 | else { 466 | throw new Error(`Unknown Content-Type "${contentType}"`); 467 | } 468 | } 469 | try { 470 | load.a = parse(source, load.u); 471 | } 472 | catch (e) { 473 | console.warn(e); 474 | load.a = [[], []]; 475 | } 476 | load.S = source; 477 | return load.a[0].filter(d => d.d === -1).map(d => source.slice(d.s, d.e)); 478 | })(); 479 | 480 | load.L = load.f.then(async deps => { 481 | load.d = await Promise.all(deps.map(async depId => { 482 | const depLoad = getOrCreateLoad(await resolve(depId, load.r || load.u)); 483 | await depLoad.f; 484 | return depLoad; 485 | })); 486 | }); 487 | 488 | return load; 489 | } 490 | 491 | let importMapPromise; 492 | 493 | if (hasDocument) { 494 | // preload import maps 495 | for (const script of document.querySelectorAll('script[type="importmap-shim"][src]')) 496 | script._f = fetch(script.src); 497 | // load any module scripts 498 | for (const script of document.querySelectorAll('script[type="module-shim"]')) 499 | topLevelLoad(script.src || `${baseUrl}?${id++}`, script.src ? null : script.innerHTML); 500 | } 501 | 502 | async function resolve (id, parentUrl) { 503 | if (!importMapPromise) { 504 | importMapPromise = resolvedPromise; 505 | if (hasDocument) 506 | for (const script of document.querySelectorAll('script[type="importmap-shim"]')) { 507 | importMapPromise = importMapPromise.then(async () => { 508 | importShim$1.map = await resolveAndComposeImportMap(script.src ? await (await (script._f || fetch(script.src))).json() : JSON.parse(script.innerHTML), script.src || baseUrl, importShim$1.map); 509 | }); 510 | } 511 | } 512 | await importMapPromise; 513 | return resolveImportMap(importShim$1.map, resolveIfNotPlainOrUrl(id, parentUrl) || id, parentUrl) || throwUnresolved(id, parentUrl); 514 | } 515 | 516 | function throwUnresolved (id, parentUrl) { 517 | throw Error("Unable to resolve specifier '" + id + (parentUrl ? "' from " + parentUrl : "'")); 518 | } 519 | 520 | self.WorkerShim = WorkerShim; 521 | 522 | }()); -------------------------------------------------------------------------------- /src/icons/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/matthoffner/es-react-pwa/b3145471a0564cbbb9151bde42dc8562ae175857/src/icons/sample.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | es-react-pwa 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 27 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | import htm from 'htm'; 4 | 5 | window.html = htm.bind(React.createElement) 6 | 7 | const Route = { 8 | '/': React.lazy(() => import('./routes/home/index.js')), 9 | '/about': React.lazy(() => import('./routes/about/index.js')), 10 | } 11 | 12 | ReactDOM.render( 13 | html` 14 | <${React.Suspense} fallback=${html`
loading...
`}> 15 | <${Route[location.pathname] || Route['*']} /> 16 | 17 | `, 18 | document.body 19 | ) -------------------------------------------------------------------------------- /src/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "es-react-pwa", 3 | "name": "es-react-pwa", 4 | "description": "A demo using es modules, react and pwa with snowpack", 5 | "start_url": "/", 6 | "background_color": "#3367D6", 7 | "display": "standalone", 8 | "display_override": ["window-controls-overlay"], 9 | "scope": "/", 10 | "icons": [ 11 | { 12 | "src": "icons/sample.png", 13 | "sizes": "48x48", 14 | "type": "image/png" 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/about/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Title = styled.h1` 5 | font-size: 1.5em; 6 | text-align: center; 7 | color: palevioletred; 8 | `; 9 | const Wrapper = styled.section` 10 | padding: 4em; 11 | background: papayawhip; 12 | `; 13 | 14 | export default () => { 15 | return html` 16 | <${React.Fragment}> 17 | <${Wrapper}> 18 | About 19 | 20 |

21 | This is an example of a modern React template that doesn't require any transpiling or bundling thanks to ES modules, importmaps along with workbox for offline PWA capabilities. 22 |

23 | 24 | `; 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/home/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styled from 'styled-components'; 3 | 4 | const Title = styled.h1` 5 | font-size: 1.5em; 6 | text-align: center; 7 | color: palevioletred; 8 | `; 9 | const Wrapper = styled.section` 10 | padding: 4em; 11 | background: lightblue; 12 | box-shadow: 10px; 13 | `; 14 | 15 | export default () => { 16 | return html` 17 | <${React.Fragment}> 18 | <${Wrapper}> 19 | <${Title}>es-react-pwa 20 | 21 | About 22 | 23 | `; 24 | } 25 | -------------------------------------------------------------------------------- /workbox-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "globDirectory": "./src", 3 | "globPatterns": [ 4 | "**/*.{html,js}" 5 | ], 6 | "swDest": "src/sw.js", 7 | "clientsClaim": true, 8 | "skipWaiting": true 9 | }; 10 | --------------------------------------------------------------------------------