diff --git a/.eslintignore b/.eslintignore index e38c4aeb4..d1d95bdff 100644 --- a/.eslintignore +++ b/.eslintignore @@ -6,4 +6,4 @@ node_modules/ parsers/ results/ specimens/ -specimens/output/ +output/ diff --git a/package-lock.json b/package-lock.json index 9aa8123a6..1be352238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -819,9 +819,9 @@ } }, "@tivac/eslint-config": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@tivac/eslint-config/-/eslint-config-2.2.0.tgz", - "integrity": "sha512-qlwXGdCBm5C4FR+xlIHAN5QgTRLVPHPOODnBadwc4JdfHHVT+cZKSc9z6hvSFgm2zyWCxpkfwyCLxwytGNa99g==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@tivac/eslint-config/-/eslint-config-2.2.1.tgz", + "integrity": "sha512-AOm2u3b1VNHMlht487Ui12FulNWh/wzy9yXSzAyxGrQ3n/z7djrW4bC09flScFixeuBXibdag2BIedzhjM1sDQ==", "dev": true }, "@types/estree": { @@ -831,9 +831,9 @@ "dev": true }, "@types/node": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.4.tgz", - "integrity": "sha512-YMLlzdeNnAyLrQew39IFRkMacAR5BqKGIEei9ZjdHsIZtv+ZWKYTu1i7QJhetxQ9ReXx8w5f+cixdHZG3zgMQA==", + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.3.5.tgz", + "integrity": "sha512-6lRwZN0Y3TuglwaaZN2XPocobmzLlhxcqDjKFjNYSsXG/TFAGYkCqkzZh4+ms8iTHHQE6gJXLHPV7TziVGeWhg==", "dev": true }, "@webassemblyjs/ast": { @@ -6772,6 +6772,66 @@ "throat": "^4.0.0" } }, + "jest-cli": { + "version": "23.1.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-23.1.0.tgz", + "integrity": "sha1-64vdTODRUlCJLjGtm2m8mdKo9r8=", + "dev": true, + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.1", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "import-local": "^1.0.0", + "is-ci": "^1.0.10", + "istanbul-api": "^1.3.1", + "istanbul-lib-coverage": "^1.2.0", + "istanbul-lib-instrument": "^1.10.1", + "istanbul-lib-source-maps": "^1.2.4", + "jest-changed-files": "^23.0.1", + "jest-config": "^23.1.0", + "jest-environment-jsdom": "^23.1.0", + "jest-get-type": "^22.1.0", + "jest-haste-map": "^23.1.0", + "jest-message-util": "^23.1.0", + "jest-regex-util": "^23.0.0", + "jest-resolve-dependencies": "^23.0.1", + "jest-runner": "^23.1.0", + "jest-runtime": "^23.1.0", + "jest-snapshot": "^23.0.1", + "jest-util": "^23.1.0", + "jest-validate": "^23.0.1", + "jest-watcher": "^23.1.0", + "jest-worker": "^23.0.1", + "micromatch": "^2.3.11", + "node-notifier": "^5.2.1", + "realpath-native": "^1.0.0", + "rimraf": "^2.5.4", + "slash": "^1.0.0", + "string-length": "^2.0.0", + "strip-ansi": "^4.0.0", + "which": "^1.2.12", + "yargs": "^11.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", + "dev": true + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "dev": true, + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, "jest-config": { "version": "23.1.0", "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-23.1.0.tgz", @@ -10420,9 +10480,9 @@ } }, "rollup": { - "version": "0.60.7", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.60.7.tgz", - "integrity": "sha512-Uj5I1A2PnDgA79P+v1dsNs1IHVydNgeJdKWRfoEJJdNMmyx07TRYqUtPUINaZ/gDusncFy1SZsT3lJnBBI8CGw==", + "version": "0.61.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.61.2.tgz", + "integrity": "sha512-NN7GaX3c9I3oz+CjEex7+JbnSxrLMTBXNRThI4qdN2Tq0K/7RiedlEzOHyuVAHPZKvQeoPiRioTWL3xhmN+oCg==", "dev": true, "requires": { "@types/estree": "0.0.39", diff --git a/package.json b/package.json index 5af8c2eef..84c63b244 100644 --- a/package.json +++ b/package.json @@ -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", @@ -34,11 +34,12 @@ "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", @@ -46,17 +47,6 @@ "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", diff --git a/packages/rollup/README.md b/packages/rollup/README.md index e6c000725..8083dc959 100644 --- a/packages/rollup/README.md +++ b/packages/rollup/README.md @@ -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` diff --git a/packages/rollup/rollup.js b/packages/rollup/rollup.js index 942468ebd..6ce612b86 100644 --- a/packages/rollup/rollup.js +++ b/packages/rollup/rollup.js @@ -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, @@ -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)); diff --git a/packages/rollup/test/__snapshots__/rollup.test.js.snap b/packages/rollup/test/__snapshots__/rollup.test.js.snap index 17caffae5..d0ffbf5e4 100644 --- a/packages/rollup/test/__snapshots__/rollup.test.js.snap +++ b/packages/rollup/test/__snapshots__/rollup.test.js.snap @@ -115,7 +115,7 @@ exports[`/rollup.js should correctly pass to/from params for relative paths 1`] "/* packages/rollup/test/specimens/relative-paths.css */ .wooga { color: red; - background: url(\\"packages/rollup/test/specimens/folder/to.png\\"); + background: url(\\"../../../specimens/folder/to.png\\"); } " `; @@ -153,7 +153,7 @@ Object { "mappings": "AAAA,+CAAC;AAED;IACI,WAAW;CACd", "names": Array [], "sources": Array [ - "packages/rollup/test/specimens/simple.css", + "../../../specimens/simple.css", ], "sourcesContent": Array [ "@value str: \\"string\\"; @@ -195,12 +195,25 @@ console.log(css); " `; +exports[`/rollup.js should respect the CSS dependency tree 2`] = ` +"/* packages/rollup/test/specimens/simple.css */ +.fooga { + color: red; +} +/* packages/rollup/test/specimens/dependencies.css */ +.wooga { + + background: blue; +} +" +`; + exports[`/rollup.js should warn & not export individual keys when they are not valid identifiers 1`] = ` Object { "code": "PLUGIN_WARNING", "id": Any<String>, "message": "Invalid JS identifier \\"fooga-wooga\\", unable to export", - "plugin": "modular-css", + "plugin": "modular-css-rollup", "toString": [Function], } `; diff --git a/packages/rollup/test/rollup.test.js b/packages/rollup/test/rollup.test.js index 598d87718..5d3540380 100644 --- a/packages/rollup/test/rollup.test.js +++ b/packages/rollup/test/rollup.test.js @@ -23,16 +23,16 @@ function error(root) { error.postcssPlugin = "error-plugin"; -const output = "./packages/rollup/test/output"; const assetFileNames = "assets/[name][extname]"; const format = "es"; const map = false; +const output = "./packages/rollup/test/output"; const sourcemap = false; describe("/rollup.js", () => { /* eslint max-statements: "off" */ - afterEach(() => shell.rm("-rf", `${output}/*`)); + beforeAll(() => shell.rm("-rf", `${output}/*`)); it("should be a function", () => expect(typeof plugin).toBe("function") @@ -82,10 +82,10 @@ describe("/rollup.js", () => { await bundle.write({ format, assetFileNames, - file : `${output}/simple.js`, + file : `${output}/css/simple.js`, }); - expect(read("assets/simple.css")).toMatchSnapshot(); + expect(read("css/assets/simple.css")).toMatchSnapshot(); }); it("should correctly pass to/from params for relative paths", async () => { @@ -102,10 +102,10 @@ describe("/rollup.js", () => { await bundle.write({ format, assetFileNames, - file : `${output}/relative-paths.js`, + file : `${output}/relative-paths/relative-paths.js`, }); - expect(read("assets/relative-paths.css")).toMatchSnapshot(); + expect(read("relative-paths/assets/relative-paths.css")).toMatchSnapshot(); }); it("should avoid generating empty CSS", async () => { @@ -121,12 +121,12 @@ describe("/rollup.js", () => { await bundle.write({ format, assetFileNames, - file : `${output}/no-css.js`, + file : `${output}/no-css/no-css.js`, }); - expect(exists("assets/no-css.css")).toBe(false); + expect(exists("no-css/assets/no-css.css")).toBe(false); }); - + it("should generate JSON", async () => { const bundle = await rollup({ input : require.resolve("./specimens/simple.js"), @@ -141,10 +141,10 @@ describe("/rollup.js", () => { await bundle.write({ format, assetFileNames, - file : `${output}/simple.js`, + file : `${output}/json/simple.js`, }); - expect(read("assets/simple.json")).toMatchSnapshot(); + expect(read("json/assets/simple.json")).toMatchSnapshot(); }); it("should provide named exports", async () => { @@ -178,12 +178,12 @@ describe("/rollup.js", () => { await bundle.write({ format, assetFileNames, - file : `${output}/simple.js`, + file : `${output}/external-source-maps/simple.js`, }); // Have to parse it into JSON so the propertyMatcher can exclude the file property // since it is a hash value and changes constantly - expect(JSON.parse(read("assets/simple.css.map"))).toMatchSnapshot({ + expect(JSON.parse(read("external-source-maps/assets/simple.css.map"))).toMatchSnapshot({ file : expect.any(String), }); }); @@ -271,10 +271,10 @@ describe("/rollup.js", () => { format, sourcemap, - file : `${output}/no-maps.js`, + file : `${output}/no-maps/no-maps.js`, }); - expect(read("assets/no-maps.css")).toMatchSnapshot(); + expect(read("no-maps/assets/no-maps.css")).toMatchSnapshot(); }); it("should respect the CSS dependency tree", async () => { @@ -283,13 +283,21 @@ describe("/rollup.js", () => { plugins : [ plugin({ namer, + map, }), ], }); - const result = await bundle.generate({ format }); + await bundle.write({ + format, + assetFileNames, + sourcemap, - expect(result.code).toMatchSnapshot(); + file : `${output}/dependencies/dependencies.js`, + }); + + expect(read("dependencies/dependencies.js")).toMatchSnapshot(); + expect(read("dependencies/assets/dependencies.css")).toMatchSnapshot(); }); it("should accept an existing processor instance", async () => { @@ -309,8 +317,6 @@ describe("/rollup.js", () => { plugins : [ plugin({ processor, - - common : "common.css", }), ], }); @@ -320,11 +326,11 @@ describe("/rollup.js", () => { sourcemap, assetFileNames, - file : `${output}/existing-processor.js`, + file : `${output}/existing-processor/existing-processor.js`, }); - expect(read("assets/existing-processor.css")).toMatchSnapshot(); - expect(read("assets/common.css")).toMatchSnapshot(); + expect(read("existing-processor/assets/existing-processor.css")).toMatchSnapshot(); + expect(read("existing-processor/assets/common.css")).toMatchSnapshot(); }); describe("errors", () => { @@ -396,7 +402,7 @@ describe("/rollup.js", () => { watcher = watch({ input : require.resolve("./specimens/watch.js"), output : { - file : `${output}/watch-output.js`, + file : `${output}/watch/watch-output.js`, format, assetFileNames, }, @@ -415,13 +421,13 @@ describe("/rollup.js", () => { watcher.on("event", watching((builds) => { if(builds === 1) { - expect(read("assets/watch-output.css")).toMatchSnapshot(); + expect(read("watch/assets/watch-output.css")).toMatchSnapshot(); // continue watching return; } - expect(read("assets/watch-output.css")).toMatchSnapshot(); + expect(read("watch/assets/watch-output.css")).toMatchSnapshot(); return done(); })); @@ -452,7 +458,7 @@ describe("/rollup.js", () => { watcher = watch({ input : require.resolve("./output/watch.js"), output : { - file : `${output}/watch-output.js`, + file : `${output}/watch-deps/watch-output.js`, format, assetFileNames, }, @@ -472,13 +478,13 @@ describe("/rollup.js", () => { watcher.on("event", watching((builds) => { if(builds === 1) { - expect(read("assets/watch-output.css")).toMatchSnapshot(); + expect(read("watch-deps/assets/watch-output.css")).toMatchSnapshot(); // continue watching return; } - expect(read("assets/watch-output.css")).toMatchSnapshot(); + expect(read("watch-deps/assets/watch-output.css")).toMatchSnapshot(); return done(); })); @@ -506,7 +512,7 @@ describe("/rollup.js", () => { watcher = watch({ input : require.resolve("./output/watch.js"), output : { - file : `${output}/watch-output.js`, + file : `${output}/watch-new/watch-output.js`, format, assetFileNames, }, @@ -529,13 +535,13 @@ describe("/rollup.js", () => { watcher.on("event", watching((builds) => { if(builds === 1) { - expect(exists("assets/watch-output.css")).toBe(false); + expect(exists("watch-new/assets/watch-output.css")).toBe(false); // continue watching return; } - expect(read("assets/watch-output.css")).toMatchSnapshot(); + expect(read("watch-new/assets/watch-output.css")).toMatchSnapshot(); return done(); })); @@ -559,8 +565,6 @@ describe("/rollup.js", () => { plugin({ namer, map, - - common : "common.css", }), ], }); @@ -571,11 +575,11 @@ describe("/rollup.js", () => { assetFileNames, chunkFileNames, - dir : `${output}/`, + dir : `${output}/splitting`, }); - expect(read("assets/chunk.css")).toMatchSnapshot(); - expect(read("assets/dependencies.css")).toMatchSnapshot(); + expect(read("splitting/assets/common.css")).toMatchSnapshot(); + expect(read("splitting/assets/dependencies.css")).toMatchSnapshot(); }); it("should support manual chunks", async () => { @@ -607,12 +611,12 @@ describe("/rollup.js", () => { assetFileNames, chunkFileNames, - dir : `${output}/`, + dir : `${output}/manual-chunks`, }); - expect(read("assets/a.css")).toMatchSnapshot(); - expect(read("assets/b.css")).toMatchSnapshot(); - expect(read("assets/shared.css")).toMatchSnapshot(); + expect(read("manual-chunks/assets/a.css")).toMatchSnapshot(); + expect(read("manual-chunks/assets/b.css")).toMatchSnapshot(); + expect(read("manual-chunks/assets/common.css")).toMatchSnapshot(); }); it("should support dynamic imports", async () => { @@ -630,8 +634,6 @@ describe("/rollup.js", () => { plugin({ namer, map, - - common : "common.css", }), ], }); @@ -642,13 +644,13 @@ describe("/rollup.js", () => { assetFileNames, chunkFileNames, - dir : `${output}/`, + dir : `${output}/dynamic-imports`, }); - expect(read("assets/a.css")).toMatchSnapshot(); - expect(read("assets/b.css")).toMatchSnapshot(); - expect(read("assets/c.css")).toMatchSnapshot(); - expect(read("assets/common.css")).toMatchSnapshot(); + expect(read("dynamic-imports/assets/a.css")).toMatchSnapshot(); + expect(read("dynamic-imports/assets/b.css")).toMatchSnapshot(); + expect(read("dynamic-imports/assets/c.css")).toMatchSnapshot(); + expect(read("dynamic-imports/assets/common.css")).toMatchSnapshot(); }); }); });