Skip to content
Permalink
Browse files

feat: add rollup-rewriter package (#545)

  • Loading branch information...
tivac committed Jan 21, 2019
1 parent 935cd43 commit b483ed68ad2cb49bfe0c77cabce66f75f994e11c

Large diffs are not rendered by default.

Oops, something went wrong.
@@ -52,6 +52,7 @@
"@modular-css/postcss": "file:packages/postcss",
"@modular-css/processor": "file:packages/processor",
"@modular-css/rollup": "file:packages/rollup",
"@modular-css/rollup-rewriter": "file:packages/rollup-rewriter",
"@modular-css/shortnames": "file:packages/namer",
"@modular-css/svelte": "file:packages/svelte",
"@modular-css/webpack": "file:packages/webpack"
@@ -0,0 +1,5 @@
coverage/
profiling/
test/
.*
CHANGELOG.md
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2019 Pat Cavit

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1,99 @@
# @modular-css/rollup-rewriter [![NPM Version](https://img.shields.io/npm/v/@modular-css/rollup-rewriter.svg)](https://www.npmjs.com/package/@modular-css/rollup-rewriter) [![NPM License](https://img.shields.io/npm/l/@modular-css/rollup-rewriter.svg)](https://www.npmjs.com/package/@modular-css/rollup-rewriter) [![NPM Downloads](https://img.shields.io/npm/dm/@modular-css/rollup-rewriter.svg)](https://www.npmjs.com/package/@modular-css/rollup-rewriter)

<p align="center">
<a href="https://gitter.im/modular-css/modular-css"><img src="https://img.shields.io/gitter/room/modular-css/modular-css.svg" alt="Gitter" /></a>
</p>

Rewrite dynamic imports so they automatically load their CSS dependencies using JS chunk -> CSS chunk dependency information from [`modular-css`](https://github.com/tivac/modular-css). Avoid the dreaded [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content) automatically without having to manually juggle CSS files & JS chunks.

Turn this:

```js
const module = await import("./expensive-styled-module.js");
```

into this

```js
const module = await Promise.all([
lazyload("./expensive-styled-module.css"),
import("./expensive-styled-module.js")
])
.then((result) => result[result.length - 1]);
```

- [Install](#install)
- [Limitations](#-limitations-)
- [Usage](#usage)
- [Options](#options)

## Install

```bash
> npm i @modular-css/rollup-rewriter
```

## ⚠ Limitations ⚠

This plugin does not yet do everything for you instantly and perfectly. Maybe someday! Here's a list of current limitations:

- Only supports some of the rollup output [`format`](https://rollupjs.org/guide/en#output-format) values.
- Currently `es`, `esm`, `amd`, and `system`.
- Doesn't dynamically add the `loader` option into the module so it can be inlined or extracted by rollup.
- It's just injected at the top of the module scope, so you can't depend on packaging yet. Can't be injected earlier because the full module dependency tree & chunks must be known first.
- Probably easier to ensure it's available globally and only use `loadfn`.
- Doesn't allow for adjusting URLs to add a CDN-prefix or anything else.
- This would be straightforward, just not implemented yet. A PR would be very welcome!

## Usage

### API

```js
const bundle = await rollup({
input : "./index.js",
plugins : [
require("@modular-css/rollup")(),
require("@modular-css/rollup-rewriter")({
loadfn : "...",
}),
],
});
```

### Config file

```js
import css from "@modular-css/rollup";
import rewrite from "@modular-css/rollup-rewriter";
export default {
input : "./index.js",
output : {
dest : "./gen/bundle.js",
format : "umd"
},
plugins : [
css(),
rewrite({
loadfn : "...",
}),
],
};
```

## Options

### `loader` (string)

The `loader` option can be set if you want the plugin to inject a reference to a CSS loader that returns a promise (I like [`lazyload-css`](https://npmjs.com/lazyload-css)). This loader must be available statically, so this option is most useful in `es`/`esm` mode where it can be loaded via `import`.

### `loadfn` (string)

The name of the promise-returning function that will be used to load CSS. The function will be passed the path to the CSS file and is expected to return a promise that resolves once the file is loaded.

The `loadfn` option is **required**.

### `verbose` (boolean)

When enabled will cause the plugin to output more information about the processing as it occurs.
@@ -0,0 +1,31 @@
const search = `'use strict';`;

exports.regex = (deps) => new RegExp(
`require\\(\\[['"]\\.\\/(${deps})['"]\\], resolve, reject\\)`,
"g"
);

exports.loader = (options, str) => {
const s = str.toString();
const i = s.indexOf(search);

if(i === -1) {
throw new Error("Unable to find strict mode declaration");
}

// + 1 is for the newline...
str.appendRight(
i + search.length + 1,
`import lazyload from "./css.js";\n`
);
};

exports.load = (options, imports, statement) => `
Promise.all([
${imports},
new Promise(function (resolve, reject) { ${statement} })
])
.then((results) => resolve(results[results.length - 1]))
.catch(reject)
`;

@@ -0,0 +1,17 @@
exports.regex = (deps) => new RegExp(
`\\bimport\\(['"]\\.\\/(${deps})['"]\\)`,
"g"
);

exports.loader = (options, str) => str.prepend(
`import lazyload from "./css.js";\n`
);

exports.load = (options, imports, statement) => `
Promise.all([
${imports},
${statement}
])
.then((results) => results[results.length - 1])
`;

@@ -0,0 +1,30 @@
const search = `'use strict';`;

exports.regex = (deps) => new RegExp(
`\\bmodule\\.import\\(['"]\\.\\/(${deps})['"]\\)`,
"g"
);

exports.loader = (options, str) => {
const s = str.toString();
const i = s.indexOf(search);

if(i === -1) {
throw new Error("Unable to find strict mode declaration");
}

// + 1 is for the newline...
str.appendRight(
i + search.length + 1,
`import lazyload from "./css.js";\n`
);
};

exports.load = (options, imports, statement) => `
Promise.all([
${imports},
${statement}
])
.then((results) => results[results.length - 1])
`;

@@ -0,0 +1,32 @@
{
"name": "@modular-css/rollup-rewriter",
"version": "20.0.1",
"description": "Rewrite dynamic imports to include all their CSS dependencies",
"main": "./rewriter.js",
"repository": "tivac/modular-css",
"bugs": {
"url": "https://github.com/tivac/modular-css/issues"
},
"author": "Pat Cavit <npm@patcavit.com>",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"keywords": [
"rollup",
"rollup-plugin",
"css",
"css-modules",
"modular-css",
"postcss"
],
"dependencies": {
"dedent": "^0.7.0",
"escape-string-regexp": "^1.0.5",
"magic-string": "^0.25.1"
},
"peerDependencies": {
"@modular-css/rollup": "^21",
"rollup": "^1.1.1"
}
}
@@ -0,0 +1,95 @@
"use strict";

const MagicString = require("magic-string");
const dedent = require("dedent");
const escape = require("escape-string-regexp");

const formats = {
es : require("./formats/es.js"),
amd : require("./formats/amd.js"),
system : require("./formats/system.js"),

// Just an alias...
esm : require("./formats/es.js"),
};

const supported = new Set(Object.keys(formats).sort());

module.exports = (opts) => {
const options = Object.assign(Object.create(null), {
loader : false,
loadfn : false,
verbose : false,
}, opts);

if(!options.loadfn) {
throw new Error("options.loadfn must be configured");
}

// eslint-disable-next-line no-console, no-empty-function
const log = options.verbose ? console.log.bind(console, "[rewriter]") : () => {};

return {
name : "@modular-css/rollup-rewriter",

generateBundle({ format }, chunks) {
if(!supported.has(format)) {
// This throws, so execution stops here even though it doesn't look like it
this.error(`Unsupported format: ${format}. Supported formats are ${JSON.stringify([ ...supported.values() ])}`);
}

Object.entries(chunks).forEach(([ entry, chunk ]) => {
const {
isAsset = false,
assets = [],
code = "",
dynamicImports = [],
} = chunk;

// Guard against https://github.com/rollup/rollup/issues/2659
const deps = dynamicImports.filter(Boolean);

if(isAsset || !deps.length || !assets.length) {
return;
}

const { regex, loader, load } = formats[format];

const search = regex(deps.map(escape).join("|"));

const str = new MagicString(code);

if(options.loader) {
loader(options, str);
}

// Yay stateful regexes
search.lastIndex = 0;

let result = search.exec(code);

while(result) {
// Pull useful values out of the regex result
const [ statement, file ] = result;
const { index } = result;

const imports = chunks[file].assets.map((dep) =>
`${options.loadfn}("./${dep}")`
);

str.overwrite(
index,
index + statement.length,
dedent(load(options, imports.join(",\n"), statement))
);

result = search.exec(code);
}

log("Updating", entry);

chunk.code = str.toString();
});
},
};
};
Oops, something went wrong.

0 comments on commit b483ed6

Please sign in to comment.
You can’t perform that action at this time.