Module require debugger Loading modules is tricky! Because modules are tricky. Let's try to make them a little easier. Enter a module into the moduleName field below, click Go, and see which module-loading strategies will work.
viewof moduleName = text({placeholder: "module-name", description: "Enter the module’s name, as it is on npm. For example, try wcag-contrast.", submit: "Go"})
moduleName ? html` ${[basicRequire, dynamicImport, bundleRun, scavengingForLinks, globalLeaksPattern] .filter(Boolean) .filter(a => a.type == success).length > 0 ? '' : pkg instanceof Error ? html`<p style='color:darkred'>This module doesn’t seem to be published on NPM. Perhaps the module name is incorrect or mistyped, or the module’s author hasn’t published it yet?</p>` : html`<p style='color:darkred'>😭 Okay, so: nothing worked. We tried UMD, we tried bundling, we tried searching for random files that might be loadable. Unfortunately, no 🎲. This doesn’t mean nothing can work - this is an automated tool and doesn’t have human intuition. You might want to <a href="https://unpkg.com/${moduleName}/">browse the files this module exposes on unpkg</a> to see if any of them look viable. Or you might want to ask the module’s author whether there’s a file that people can use with browsers. Where there’s a will there’s a way: good luck. (This also means that all of the grayed-out solutions below won’t work.)</p><br />`} <div> <p style='font-style:italic;margin-bottom:2rem;'>Below are multiple strategies for loading this library. Those with a thumbs-up work, those with a frowny face don’t, and the ones with a shrug work, but have caveats.</p> <style> .copiable { cursor:pointer; font-size:18px; max-width:100%; } .copiable:hover { background: yellow; } .copiable:hover::after { content: 'click to copy'; padding-left: 2rem; background: #fff; } .copiable.copied:hover::after { content: 'copied!'; } </style> ${[basicRequire, dynamicImport, bundleRun, scavengingForLinks, globalLeaksPattern] .filter(Boolean) .sort((a, b) => a.type == success ? -1 : 1) .map(({code, type, description}) => html` <div style='font-size:20px;${type == failure ? 'opacity:0.5' : ''};padding-bottom:3rem;'> ${emojis[type]} <code class='copiable'>${code}</code> <p style='opacity:0.8'>${md`${description}`}</p> </div> `)} </div>` : html`<strong>Enter a module name above and click Go to get started</strong>`
Analysis Okay, what kind of entry points do we have here! “Entry point” is a fancy term for a property in package.json that points to a file that can be initially included as the module’s starting point. That entry point might require other files, but it’s the key starting point.
pkg && html` <style> #entries tr td { vertical-align: top; } #entries tr td:first-child { padding-right:3rem; } #entries tr td:nth-child(2) { font-family: monospace; padding-right:3rem; } </style> <div> <table id='entries'> <thead> <tr> <th></th> <th>entry point</th> <th>description</th> </tr> </thead> <tr> <td>${!!pkg.main ? `✓` : ''}</td> <td>main</td> <td>The original one: this was the first kind of entry point invented, and for a long time it was almost always code that would run in the Node.js environment.</td> </tr> <tr> <td>${!!pkg.unpkg ? `✓` : ''}</td> <td>unpkg</td> <td>An entry point invented by <a href='https://unpkg.com/'>unpkg</a>, the web-friendly npm content delivery system. It’s usually a really good sign if we see one of these, because they’re supposed to be <a href='https://github.com/umdjs/umd'>UMD</a> files, which are very web-friendly.</td> </tr> <tr> <td>${!!pkg.module ? `✓` : ''}</td> <td>module</td> <td>The entry point of the future! In this one should lie a ES Module - a new module syntax for JavaScript that’s built into the language itself. It’s great if we find one of these, but not a guaranteed win, because support and interoperability of this module type is pretty scarce.</td> </tr> </table> <p>Sometimes a package.json won’t have any of these, and that's fine. In that case, we are left to assume that the entry point is <code>index.js</code></p> </div>`
basicRequire = { if (!moduleName) return undefined; const permaurl = (await fetch(`https://unpkg.com/${moduleName}`)).url.replace('https://unpkg.com/', ''); const res = { code: `require('${permaurl}')`, description: `This is the default, simplest method to require a module. It uses [d3-require](https://github.com/d3/d3-require) behind the scenes, pulls from unpkg by default, and supports UMD and AMD modules.` }; try { await require(permaurl); return { type: success, ...res } } catch (e) { return { type: failure, ...res }; } }
importSupport = { try { eval(`import('test')`); return true; } catch (e) { return false; } }
dynamicImport = { if (!moduleName || !importSupport) return undefined; const permaurl = (await fetch(`https://unpkg.com/${moduleName}?module`)).url; const res = { code: `import('${permaurl}')`, description: `This is the way of the future: it’s the only technique in the list that actually has your browser deal with importing modules, instead of bundling them all into one file. Advantage is that it’s super cool, and excellent for debugging - no need for sourcemaps, the files are more or less verbatim. The disadvantage is that browser support is [super slim](https://caniuse.com/#feat=es6-module).` }; try { await eval(`import('${permaurl}')`); return { type: success, ...res } } catch (e) { return { type: failure, ...res }; } }
bundleRun = { if (!moduleName || !pkg.version) return undefined; const res = { code: `require('https://bundle.run/${moduleName}@${pkg.version}')`, description: `[bundle.run](https://bundle.run/) is a freshened-up version of [wzrd.in](http://wzrd.in/), and the two tools serve the same purpose: to bundle modules. It runs [rollup](https://rollupjs.org/) and [browserify](http://browserify.org/) on the fly to combine multiple JavaScript files into one bundle that’s easily consumed by web browsers. The advantage is that it often works on stubborn modules that the author never expected to be run in a browser environment. The disadvantage is that it isn’t guaranteed to be as lightning-fast as unpkg.` }; try { await require(`https://bundle.run/${moduleName}@${pkg.version}`); return { type: success, ...res } } catch (e) { return { type: failure, ...res }; } }
scavengingForLinks = { if (!moduleName || !pkg.version) return undefined; const meta = await (await fetch(`https://unpkg.com/${moduleName}/?meta`)).json(); function collectPaths(root) { let list = []; function collect(node) { node.files.forEach(f => { if (f.type == 'file') list.push(f.path); else collect(f); }); } collect(root); return list; } let candidates = collectPaths(meta).filter(p => p.match(/(umd|amd|dist)/) && p.match(/js$/)); for (let candidate of candidates) { try { await require(`${moduleName}@${pkg.version}${candidate}`); return { type: success, code: `require('${moduleName}@${pkg.version}${candidate}')`, description: `This is _scavenging for dists_ mode: we looked through the files in this module and found one that mentioned dist, amd, or umd, and that worked.` } } catch (e) {} } }
globalLeaksPattern = { if (!moduleName || !pkg.version) return undefined; const permaurl = (await fetch(`https://unpkg.com/${moduleName}`)).url; let iframe = document.createElement('iframe'); let html = '<body></body>'; document.body.appendChild(iframe); let beforeKeys = new Set(Object.keys(iframe.contentWindow)); iframe.contentWindow.document.open(); let script = iframe.contentWindow.document.appendChild(iframe.contentWindow.document.createElement('script')); script.src = permaurl; await new Promise((resolve, reject) => { script.addEventListener('load', resolve); script.addEventListener('error', reject) }); let newKeys = Object.keys(iframe.contentWindow).filter(k => !beforeKeys.has(k)).filter(k => !['ENV', 'require'].includes(k)); iframe.contentWindow.document.close(); if (newKeys.length) { return { type: success, code: `require('${permaurl.replace('https://unpkg.com/', '')}').catch(() => window.${newKeys[0]})`, description: `The ‘global leak’ pattern. It’s a doozy. Essentially a module that works in node, and works as a script tag, but doesn’t work with any specific loader. So we use require(), expect it to fail, and then grab the global that it adds to the window. There may be multiple variables attached to the window that way, so for completeness, here are all of them: ${newKeys.join(', ')}.` }; } }
pkg = { if (!moduleName) return undefined; try { return await (await fetch(`https://unpkg.com/${moduleName}/package.json`)).json(); } catch (e) { return new Error('This package is not published on npm'); } }
document.addEventListener('click', e => { if (e.target.className === 'copiable') { copy(e.target.textContent); e.target.classList.add('copied'); setTimeout(() => e.target.classList.remove('copied'), 500); } })
emojis = ({ [failure]: '☹️', [success]: '👍', [maybe]: '🤷' })
import {text} from "@jashkenas/inputs"
success = Symbol('success')
failure = Symbol('failure')
maybe = Symbol('maybe')
omit = Symbol('omit')
function copy(text) { var fakeElem = document.body.appendChild(document.createElement('textarea')); fakeElem.style.position = 'absolute'; fakeElem.style.left = '-9999px'; fakeElem.setAttribute('readonly', ''); fakeElem.value = text; fakeElem.select(); try { return document.execCommand('copy'); } catch (err) { return false; } finally { fakeElem.parentNode.removeChild(fakeElem); } }