Skip to content

Commit

Permalink
Merge pull request #196 from developit/plugin-nomodule
Browse files Browse the repository at this point in the history
@wmr-plugins/nomodule
  • Loading branch information
developit committed Dec 2, 2020
2 parents 6138e24 + 00c4b87 commit 8ad2dc0
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 2 deletions.
6 changes: 6 additions & 0 deletions packages/@wmr-plugins_nomodule/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# `@wmr-plugins/nomodule`

WMR outputs modern JavaScript bundles by default.
This plugin creates legacy versions of your bundles using [@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env) and serves those versions to older browsers.

New browsers get the new stuff, old browsers get the old stuff.
91 changes: 91 additions & 0 deletions packages/@wmr-plugins_nomodule/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import babel from '@babel/standalone';

/** @param {import('../../types').Options} options */
export default function (options) {
if (options.mode && options.mode !== 'build') return;
options.plugins.push(nomodulePlugin({}));
}

/**
* @param {object} [options]
* @returns {import('rollup').Plugin}
*/
function nomodulePlugin({} = {}) {
return {
name: '@wmr-plugins/nomodule',
async generateBundle(opts, bundle) {
const downleveled = new Map();
for (const fileName in bundle) {
const chunk = bundle[fileName];
if (chunk.type !== 'chunk') continue;
const legacy = downlevel(chunk.code, fileName);
if (!legacy) continue;
const legacyFileName = chunk.fileName.replace(/\.js/, '.legacy.js');
this.emitFile({
type: 'asset',
fileName: legacyFileName,
source: legacy
});
downleveled.set(fileName, legacyFileName);
}

// Update all the HTML files with <script type=module> to add legacy loading via Shimport+Polyfills
for (const fileName in bundle) {
const asset = bundle[fileName];
if (asset.type !== 'asset' || typeof asset.source !== 'string' || !asset.fileName.match(/\.html$/)) continue;
if (!/<script(?:\s[^>]*)?\s+type=(['"])module\1/.test(asset.source)) continue;
// this is gross obviously
const POLYFILL = 'https://unpkg.com/@babel/polyfill@7.12.1/browser.js'; // https://unpkg.com/regenerator-runtime
const SHIMPORT = 'https://unpkg.com/shimport@2.0.4/index.js';
const script = `<script nomodule src="${POLYFILL}"></script><script nomodule src="${SHIMPORT}"></script><script nomodule>[].forEach.call(document.querySelectorAll('script[type=module]'),function(n){__shimport__.load(n.src.replace(/\\.js$/,'.legacy.js'))})</script>`;
if (/<\/body>/.test(asset.source)) {
asset.source = asset.source.replace(/<\/body>/, `${script}</body>`);
} else {
asset.source += script;
}
}
}
};
}

function downlevel(code, fileName) {
const result = babel.transform(code, {
compact: true,
minified: true,
configFile: false,
babelrc: false,
filename: fileName,
presets: [
[
'env',
{
targets: 'defaults',
shippedProposals: true,
loose: true,
bugfixes: true,
corejs: false,
useBuiltIns: false,
modules: false
}
]
],
plugins: [
{
visitor: {
ImportDeclaration(path) {
path.node.source.value = path.node.source.value.replace(/(\.legacy)?\.js$/, '.legacy.js');
},
Import(path) {
const p = path.parentPath;
if (!p.isCallExpression()) return;
const arg = p.get('arguments.0');
// the things I do to make TypeScript happy...
if (Array.isArray(arg) || !arg.isStringLiteral()) return;
arg.node.value = arg.node.value.replace(/(\.legacy)?\.js$/, '.legacy.js');
}
}
}
]
});
return result.code;
}
27 changes: 27 additions & 0 deletions packages/@wmr-plugins_nomodule/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@wmr-plugins/nomodule",
"publishConfig": {
"access": "public"
},
"version": "0.1.0",
"description": "Generate legacy bundles and serve them to older browsers as a fallback.",
"main": "./dist/index.js",
"type": "module",
"exports": {
"import": "./index.js",
"require": "./dist/index.cjs"
},
"scripts": {
"prepare": "npx cjyes"
},
"author": "The Preact Authors (https://preactjs.com)",
"repository": "preactjs/wmr",
"license": "MIT",
"files": [
"index.js",
"dist"
],
"dependencies": {
"@babel/standalone": "^7.11.6"
}
}
6 changes: 4 additions & 2 deletions src/lib/normalize-options.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,21 @@ export async function normalizeOptions(options, mode) {
const hasMjsConfig = await isFile(resolve(options.root, 'wmr.config.mjs'));
if (hasMjsConfig || (await isFile(resolve(options.root, 'wmr.config.js')))) {
let custom,
initialConfigFile = hasMjsConfig ? 'wmr.config.mjs' : 'wmr.config.js';
initialConfigFile = hasMjsConfig ? 'wmr.config.mjs' : 'wmr.config.js',
initialError;
try {
const resolved = resolve(options.root, initialConfigFile);
// Note: the eval() below is to prevent Rollup from transforming import() and require().
// Using the native functions allows us to load ESM and CJS with Node's own semantics.
try {
custom = await eval('(x => import(x))')(resolved);
} catch (err) {
initialError = err;
custom = eval('(x => require(x))')(resolved);
}
} catch (e) {
if (hasMjsConfig || !/import statement/.test(e)) {
throw Error(`Failed to load ${initialConfigFile}\n${e}`);
throw Error(`Failed to load ${initialConfigFile}\n${initialError}\n${e}`);
}
}
Object.defineProperty(options, '_config', { value: custom });
Expand Down

0 comments on commit 8ad2dc0

Please sign in to comment.