Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add rollup transform dependencies support #438

Merged
merged 13 commits into from Jun 26, 2018
2 changes: 1 addition & 1 deletion .eslintignore
Expand Up @@ -6,4 +6,4 @@ node_modules/
parsers/
results/
specimens/
specimens/output/
output/
78 changes: 69 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 3 additions & 13 deletions package.json
Expand Up @@ -22,7 +22,7 @@
"watch": "jest --watch"
},
"devDependencies": {
"@tivac/eslint-config": "^2.2.0",
"@tivac/eslint-config": "^2.2.1",
"browserify": "^16.2.0",
"cli-tester": "^2.0.0",
"cssnano": "^4.0.0-rc.1",
Expand All @@ -34,29 +34,19 @@
"from2-string": "^1.1.0",
"husky": "^0.14.3",
"jest": "^23.1.0",
"jest-cli": "^23.1.0",
"lerna": "^3.0.0-beta.21",
"lint-staged": "^7.0.4",
"modular-css-core": "file:./packages/core",
"pegjs": "^0.10.0",
"rollup": "^0.60.4",
"rollup": "^0.61.2",
"rollup-plugin-svelte": "^4.1.0",
"shelljs": "^0.8.1",
"svelte": "^2.8.1",
"test-utils": "file:./packages/test-utils",
"watchify": "^3.9.0",
"webpack": "^4.12.1"
},
"jest": {
"coveragePathIgnorePatterns": [
"node_modules",
"parsers",
"test-utils"
],
"watchPathIgnorePatterns": [
"test/output",
"test/specimens"
]
},
"lint-staged": {
"*.js": [
"eslint --fix",
Expand Down
2 changes: 1 addition & 1 deletion packages/rollup/README.md
Expand Up @@ -46,7 +46,7 @@ export default {

### `common`

File name to use in case there are any CSS dependencies that appear in multiple bundles.
File name to use in case there are any CSS dependencies that appear in multiple bundles. Defaults to "common.css".

### `include`/`exclude`

Expand Down
159 changes: 91 additions & 68 deletions packages/rollup/rollup.js
Expand Up @@ -22,7 +22,7 @@ function extensionless(file) {

module.exports = function(opts) {
const options = Object.assign(Object.create(null), {
common : false,
common : "common.css",

json : false,
map : true,
Expand All @@ -36,117 +36,140 @@ module.exports = function(opts) {

const processor = options.processor || new Processor(options);

let runs = 0;

return {
name : "modular-css",
name : "modular-css-rollup",

transform(code, id) {
let removed = [];

if(!filter(id)) {
return null;
}

// If the file is being re-processed we need to remove it to
// avoid cache staleness issues
if(runs) {
removed = processor.remove(id);
if(id in processor.files) {
processor.dependencies(id)
.concat(id)
.forEach((file) => processor.remove(file));
}

return Promise.all(
// Run current file first since it's already in-memory
[ processor.string(id, code) ].concat(
removed.map((file) =>
processor.file(file)
)
)
)
.then((results) => {
const [ result ] = results;
return processor.string(id, code).then((result) => {
const exported = output.join(result.exports);

let out = [
const out = [
`export default ${JSON.stringify(exported, null, 4)};`,
];

// Add dependencies
out = out.concat(
processor.dependencies(id).map((file) =>
`import "${slash(file)}";`
)
);

if(options.namedExports === false) {
return {
code : out.join("\n"),
map,
};
}

Object.keys(exported).forEach((ident) => {
if(keyword.isReservedWordES6(ident) || !keyword.isIdentifierNameES6(ident)) {
this.warn(`Invalid JS identifier "${ident}", unable to export`);
if(options.namedExports) {
Object.keys(exported).forEach((ident) => {
if(keyword.isReservedWordES6(ident) || !keyword.isIdentifierNameES6(ident)) {
this.warn(`Invalid JS identifier "${ident}", unable to export`);

return;
}

return;
}

out.push(`export var ${ident} = ${JSON.stringify(exported[ident])};`);
});
out.push(`export var ${ident} = ${JSON.stringify(exported[ident])};`);
});
}

const dependencies = processor.dependencies(id);

return {
code : out.join("\n"),
map,
dependencies,
};
});
},

buildEnd() {
runs++;
},
async generateBundle(outputOptions, bundles) {
const usage = new Map();
const common = new Map();
const files = [];

let to;

if(!outputOptions.file && !outputOptions.dir) {
to = path.join(process.cwd(), outputOptions.assetFileNames || "");
} else {
to = path.join(
outputOptions.dir ? outputOptions.dir : path.dirname(outputOptions.file),
outputOptions.assetFileNames
);
}

// First pass is used to calculate JS usage of CSS dependencies
Object.keys(bundles).forEach((entry) => {
const file = {
entry,
base : extensionless(entry),

async generateBundle(outputOptions, bundle) {
const bundles = [];
const common = processor.dependencies();
css : [ ],
};

Object.keys(bundle).forEach((entry) => {
const files = Object.keys(bundle[entry].modules).filter(filter);
// Get CSS files being used by each entry point
const css = Object.keys(bundles[entry].modules).filter(filter);

if(!files.length) {
if(!css.length) {
return;
}

// remove the files being exported from the common bundle
files.forEach((file) =>
common.splice(common.indexOf(file), 1)
);
// Get dependency chains for each file
css.forEach((start) => {
const used = processor.dependencies(start).concat(start);

file.css = file.css.concat(used);

bundles.push({
entry,
files,
base : extensionless(entry),
used.forEach((dep) => {
usage.set(dep, usage.has(dep) ? usage.get(dep) + 1 : 1);
});
});

files.push(file);
});

// Common chunk only emitted if configured & if necessary
if(options.common && common.length) {
bundles.push({
// Second pass removes any dependencies appearing in multiple bundles
files.forEach((file) => {
const { css } = file;

file.css = css.filter((dep) => {
if(usage.get(dep) > 1) {
common.set(dep, true);

return false;
}

return true;
});
});

// Add any other files that weren't part of a bundle to the common chunk
Object.keys(processor.files).forEach((file) => {
if(!usage.has(file)) {
common.set(file, true);
}
});

// Common chunk only emitted if necessary
if(common.size) {
files.push({
entry : options.common,
base : extensionless(options.common),
files : common,
css : [ ...common.keys() ],
});
}

await Promise.all(
bundles.map(async ({ base, files }) => {
const css = this.emitAsset(`${base}.css`);

files
.filter(({ css }) => css.length)
.map(async ({ base, css }) => {
const id = this.emitAsset(`${base}.css`);

const result = await processor.output({
to : css,
files,
to,
files : css,
});

this.setAssetSource(css, result.css);
this.setAssetSource(id, result.css);

if(options.json) {
this.emitAsset(`${base}.json`, JSON.stringify(result.compositions, null, 4));
Expand Down