├── .gitignore ├── husky.config.js ├── test ├── test-cases │ ├── import-local-extends │ │ ├── expected.css │ │ └── source.css │ ├── composing-globals │ │ ├── source.css │ │ └── expected.css │ ├── import-within │ │ ├── source.css │ │ └── expected.css │ ├── import-multiple-classes │ │ ├── source.css │ │ └── expected.css │ ├── import-preserving-order │ │ ├── source.css │ │ └── expected.css │ ├── import-single-quotes │ │ ├── source.css │ │ └── expected.css │ ├── import-comment │ │ ├── source.css │ │ └── expected.css │ ├── import-only-whitelist │ │ ├── expected.css │ │ └── source.css │ ├── valid-characters │ │ ├── source.css │ │ └── expected.css │ ├── existing-import │ │ ├── source.css │ │ └── expected.css │ ├── import-spacing │ │ ├── source.css │ │ └── expected.css │ ├── resolve-composes-order │ │ ├── source.css │ │ └── expected.css │ ├── resolve-duplicates │ │ ├── source.css │ │ └── expected.css │ ├── import-consolidate │ │ ├── source.css │ │ └── expected.css │ ├── import-media │ │ ├── source.css │ │ └── expected.css │ ├── nesting │ │ ├── source.css │ │ └── expected.css │ ├── import-multiple-references │ │ ├── source.css │ │ └── expected.css │ ├── resolve-imports-order │ │ ├── source.css │ │ └── expected.css │ └── multiple-composes │ │ ├── source.css │ │ └── expected.css ├── check-import-order.test.js ├── custom-import-name.test.js ├── test.js └── topologicalSort.test.js ├── .travis.yml ├── lint-staged.config.js ├── .eslintrc ├── .editorconfig ├── LICENSE ├── src ├── topologicalSort.js └── index.js ├── package.json ├── CHANGELOG.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .nyc_output 4 | coverage 5 | node_modules 6 | -------------------------------------------------------------------------------- /husky.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | hooks: { 3 | "pre-commit": "lint-staged", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/test-cases/import-local-extends/expected.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: localName; 3 | other: rule; 4 | } 5 | -------------------------------------------------------------------------------- /test/test-cases/import-local-extends/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: localName; 3 | other: rule; 4 | } 5 | -------------------------------------------------------------------------------- /test/test-cases/composing-globals/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { composes: importName secondImport from global; other: rule; } 2 | -------------------------------------------------------------------------------- /test/test-cases/composing-globals/expected.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { composes: global(importName) global(secondImport); other: rule; } 2 | -------------------------------------------------------------------------------- /test/test-cases/import-within/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: importName from "path/library.css"; 3 | other: rule; 4 | } 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "10" 5 | - "12" 6 | - "14" 7 | 8 | script: yarn test && coveralls < coverage/lcov.info 9 | -------------------------------------------------------------------------------- /test/test-cases/import-multiple-classes/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { composes: importName secondImport from 'path/library.css'; other: rule; } 2 | -------------------------------------------------------------------------------- /test/test-cases/import-preserving-order/source.css: -------------------------------------------------------------------------------- 1 | .a { 2 | composes: b from "./b.css"; 3 | composes: c from "./c.css"; 4 | color: #aaa; 5 | } 6 | -------------------------------------------------------------------------------- /test/test-cases/import-single-quotes/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: importName from 'path/library.css'; 3 | other: rule; 4 | } 5 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "*.js": ["eslint --fix", "prettier --write"], 3 | "*.{json,md,yml,css,ts}": ["prettier --write"], 4 | }; 5 | -------------------------------------------------------------------------------- /test/test-cases/import-comment/source.css: -------------------------------------------------------------------------------- 1 | /* 2 | :local(.exportName) { 3 | composes: importName from "path/library.css"; 4 | other: rule; 5 | } 6 | */ 7 | -------------------------------------------------------------------------------- /test/test-cases/import-comment/expected.css: -------------------------------------------------------------------------------- 1 | /* 2 | :local(.exportName) { 3 | composes: importName from "path/library.css"; 4 | other: rule; 5 | } 6 | */ 7 | -------------------------------------------------------------------------------- /test/test-cases/import-only-whitelist/expected.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { imports: importName from "path/library.css"; something-else: otherLibImport from "path/other-lib.css"; } -------------------------------------------------------------------------------- /test/test-cases/import-only-whitelist/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { imports: importName from "path/library.css"; something-else: otherLibImport from "path/other-lib.css"; } -------------------------------------------------------------------------------- /test/test-cases/valid-characters/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: a -b --c _d from "path/library.css"; 3 | composes: a_ b- c-- d\% from "path/library2.css"; 4 | } 5 | -------------------------------------------------------------------------------- /test/test-cases/existing-import/source.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | something: else; 3 | } 4 | 5 | :local(.exportName) { 6 | composes: importName from 'path/library.css'; 7 | } 8 | -------------------------------------------------------------------------------- /test/test-cases/import-within/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | } 4 | :local(.exportName) { 5 | composes: i__imported_importName_0; 6 | other: rule; 7 | } 8 | -------------------------------------------------------------------------------- /test/test-cases/import-single-quotes/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | } 4 | :local(.exportName) { 5 | composes: i__imported_importName_0; 6 | other: rule; 7 | } 8 | -------------------------------------------------------------------------------- /test/test-cases/existing-import/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | something: else; 3 | i__imported_importName_0: importName; 4 | } 5 | 6 | :local(.exportName) { 7 | composes: i__imported_importName_0; 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 2018 4 | }, 5 | "env": { 6 | "es6": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "extends": ["eslint:recommended", "prettier"] 11 | } 12 | -------------------------------------------------------------------------------- /test/test-cases/import-preserving-order/expected.css: -------------------------------------------------------------------------------- 1 | :import("./b.css") { 2 | i__imported_b_0: b; 3 | } 4 | :import("./c.css") { 5 | i__imported_c_1: c; 6 | } 7 | .a { 8 | composes: i__imported_b_0; 9 | composes: i__imported_c_1; 10 | color: #aaa; 11 | } 12 | -------------------------------------------------------------------------------- /test/test-cases/import-spacing/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: importName from "path/library.css"; 3 | composes: importName2 from "path/library.css"; 4 | composes: importName importName2 from "path/library.css"; 5 | other: rule; 6 | } 7 | -------------------------------------------------------------------------------- /test/test-cases/resolve-composes-order/source.css: -------------------------------------------------------------------------------- 1 | .a { 2 | composes: c from "./c.css"; 3 | color: #bebebe; 4 | } 5 | 6 | .b { 7 | /* `b` should be after `c` */ 8 | composes: b from "./b.css"; 9 | composes: c from "./c.css"; 10 | color: #aaa; 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /test/test-cases/import-multiple-classes/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_secondImport_1: secondImport; 4 | } 5 | :local(.exportName) { composes: i__imported_importName_0 i__imported_secondImport_1; other: rule; } 6 | -------------------------------------------------------------------------------- /test/test-cases/resolve-duplicates/source.css: -------------------------------------------------------------------------------- 1 | :import("./cc.css") { 2 | smthing: somevalue; 3 | } 4 | 5 | .a { 6 | composes: a from './aa.css'; 7 | composes: b from './bb.css'; 8 | composes: c from './cc.css'; 9 | composes: a from './aa.css'; 10 | composes: c from './cc.css'; 11 | } 12 | -------------------------------------------------------------------------------- /test/test-cases/import-consolidate/source.css: -------------------------------------------------------------------------------- 1 | 2 | :local(.exportName) { 3 | composes: importName secondImport from 'path/library.css'; 4 | other: rule; 5 | } 6 | :local(.otherExport) { 7 | composes: thirdImport from 'path/library.css'; 8 | composes: otherLibImport from 'path/other-lib.css'; 9 | } 10 | -------------------------------------------------------------------------------- /test/test-cases/import-media/source.css: -------------------------------------------------------------------------------- 1 | @media screen { 2 | :local(.exportName) { 3 | composes: importName from "path/library.css"; 4 | composes: importName2 from "path/library.css"; 5 | other: rule2; 6 | } 7 | } 8 | 9 | :local(.exportName) { 10 | composes: importName from "path/library.css"; 11 | other: rule; 12 | } 13 | -------------------------------------------------------------------------------- /test/test-cases/nesting/source.css: -------------------------------------------------------------------------------- 1 | :local(.foo) { 2 | display: grid; 3 | 4 | @media (orientation: landscape) { 5 | &:local(.bar) { 6 | grid-auto-flow: column; 7 | 8 | @media (min-width: 1024px) { 9 | &:local(.baz) { 10 | composes: importName from "path/library.css"; 11 | } 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/test-cases/import-spacing/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_importName2_1: importName2; 4 | } 5 | :local(.exportName) { 6 | composes: i__imported_importName_0; 7 | composes: i__imported_importName2_1; 8 | composes: i__imported_importName_0 i__imported_importName2_1; 9 | other: rule; 10 | } 11 | -------------------------------------------------------------------------------- /test/test-cases/resolve-composes-order/expected.css: -------------------------------------------------------------------------------- 1 | :import("./b.css") { 2 | i__imported_b_1: b; 3 | } 4 | 5 | :import("./c.css") { 6 | i__imported_c_0: c; 7 | } 8 | 9 | .a { 10 | composes: i__imported_c_0; 11 | color: #bebebe; 12 | } 13 | 14 | .b { 15 | /* `b` should be after `c` */ 16 | composes: i__imported_b_1; 17 | composes: i__imported_c_0; 18 | color: #aaa; 19 | } 20 | -------------------------------------------------------------------------------- /test/test-cases/import-multiple-references/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: importName secondImport from 'path/library.css'; 3 | composes: importName from 'path/library2.css'; 4 | composes: importName2 from 'path/library.css'; 5 | } 6 | :local(.exportName2) { 7 | composes: secondImport from 'path/library.css'; 8 | composes: secondImport from 'path/library.css'; 9 | composes: thirdDep from 'path/dep3.css'; 10 | } 11 | -------------------------------------------------------------------------------- /test/test-cases/nesting/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | } 4 | 5 | :local(.foo) { 6 | display: grid; 7 | 8 | @media (orientation: landscape) { 9 | &:local(.bar) { 10 | grid-auto-flow: column; 11 | 12 | @media (min-width: 1024px) { 13 | &:local(.baz) { 14 | composes: i__imported_importName_0; 15 | } 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/test-cases/resolve-duplicates/expected.css: -------------------------------------------------------------------------------- 1 | :import("./aa.css") { 2 | i__imported_a_0: a; 3 | } 4 | 5 | :import("./bb.css") { 6 | i__imported_b_1: b; 7 | } 8 | 9 | :import("./cc.css") { 10 | smthing: somevalue; 11 | i__imported_c_2: c; 12 | } 13 | 14 | .a { 15 | composes: i__imported_a_0; 16 | composes: i__imported_b_1; 17 | composes: i__imported_c_2; 18 | composes: i__imported_a_0; 19 | composes: i__imported_c_2; 20 | } 21 | -------------------------------------------------------------------------------- /test/test-cases/import-media/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_importName2_1: importName2; 4 | } 5 | 6 | @media screen { 7 | :local(.exportName) { 8 | composes: i__imported_importName_0; 9 | composes: i__imported_importName2_1; 10 | other: rule2; 11 | } 12 | } 13 | 14 | :local(.exportName) { 15 | composes: i__imported_importName_0; 16 | other: rule; 17 | } 18 | -------------------------------------------------------------------------------- /test/test-cases/import-consolidate/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_secondImport_1: secondImport; 4 | i__imported_thirdImport_2: thirdImport; 5 | } 6 | :import("path/other-lib.css") { 7 | i__imported_otherLibImport_3: otherLibImport; 8 | } 9 | :local(.exportName) { 10 | composes: i__imported_importName_0 i__imported_secondImport_1; 11 | other: rule; 12 | } 13 | :local(.otherExport) { 14 | composes: i__imported_thirdImport_2; 15 | composes: i__imported_otherLibImport_3; 16 | } 17 | -------------------------------------------------------------------------------- /test/test-cases/valid-characters/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_a_0: a; 3 | i__imported__b_1: -b; 4 | i__imported___c_2: --c; 5 | i__imported__d_3: _d; 6 | } 7 | :import("path/library2.css") { 8 | i__imported_a__4: a_; 9 | i__imported_b__5: b-; 10 | i__imported_c___6: c--; 11 | i__imported_d___7: d\%; 12 | } 13 | :local(.exportName) { 14 | composes: i__imported_a_0 i__imported__b_1 i__imported___c_2 i__imported__d_3; 15 | composes: i__imported_a__4 i__imported_b__5 i__imported_c___6 i__imported_d___7; 16 | } 17 | -------------------------------------------------------------------------------- /test/test-cases/resolve-imports-order/source.css: -------------------------------------------------------------------------------- 1 | :import("custom-path.css") { 2 | /* empty to check the order */ 3 | } 4 | 5 | :import("./bb.css") { 6 | somevalue: localvalue; 7 | } 8 | 9 | .a { 10 | composes: aa from './aa.css'; 11 | } 12 | 13 | .b { 14 | composes: bb from './bb.css'; 15 | composes: bb from './aa.css'; 16 | } 17 | 18 | .c { 19 | composes: cc from './cc.css'; 20 | composes: cc from './aa.css'; 21 | } 22 | 23 | .d { 24 | composes: dd from './cc.css'; 25 | composes: dd from './bb.css'; 26 | composes: dd from './dd.css'; 27 | } 28 | -------------------------------------------------------------------------------- /test/test-cases/multiple-composes/source.css: -------------------------------------------------------------------------------- 1 | :local(.exportName) { 2 | composes: importName from "path/library.css", beforeName from global, importName secondImport from global, firstImport secondImport from "path/library.css"; 3 | other: rule; 4 | } 5 | 6 | :local(.duplicate) { 7 | composes: a from "./aa.css", b from "./bb.css", c from "./cc.css", a from "./aa.css", c from "./cc.css"; 8 | } 9 | 10 | :local(.spaces) { 11 | composes: importName importName2 from "path/library.css", importName3 importName4 from "path/library.css"; 12 | } 13 | 14 | :local(.unknown) { 15 | composes: foo bar, baz; 16 | } 17 | -------------------------------------------------------------------------------- /test/check-import-order.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const postcss = require("postcss"); 3 | const processor = require("../src"); 4 | 5 | describe("check-import-order", () => { 6 | it("should throw an exception", () => { 7 | const input = ` 8 | .aa { 9 | composes: b from './b.css'; 10 | composes: c from './c.css'; 11 | } 12 | 13 | .bb { 14 | composes: c from './c.css'; 15 | composes: b from './b.css'; 16 | } 17 | `; 18 | 19 | assert.throws(() => { 20 | postcss([processor({ failOnWrongOrder: true })]).process(input).css; 21 | }, /Failed to resolve order of composed modules/); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2015 Glen Maddern 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /test/test-cases/import-multiple-references/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_secondImport_1: secondImport; 4 | i__imported_importName2_3: importName2; 5 | } 6 | :import("path/library2.css") { 7 | i__imported_importName_2: importName; 8 | } 9 | :import("path/dep3.css") { 10 | i__imported_thirdDep_4: thirdDep; 11 | } 12 | :local(.exportName) { 13 | composes: i__imported_importName_0 i__imported_secondImport_1; 14 | composes: i__imported_importName_2; 15 | composes: i__imported_importName2_3; 16 | } 17 | :local(.exportName2) { 18 | composes: i__imported_secondImport_1; 19 | composes: i__imported_secondImport_1; 20 | composes: i__imported_thirdDep_4; 21 | } 22 | -------------------------------------------------------------------------------- /test/custom-import-name.test.js: -------------------------------------------------------------------------------- 1 | const assert = require("assert"); 2 | const postcss = require("postcss"); 3 | const processor = require("../src"); 4 | 5 | describe("custom-import-name", function () { 6 | it("should allow to provide a custom imported name", function () { 7 | const input = ':local(.name) { composes: abc from "def"; }'; 8 | const expected = 9 | ':import("def") {\n abc-from-def: abc;\n}\n:local(.name) { composes: abc-from-def; }'; 10 | const pipeline = postcss([ 11 | processor({ 12 | createImportedName: function (importName, path) { 13 | return importName + "-from-" + path; 14 | }, 15 | }), 16 | ]); 17 | assert.equal(pipeline.process(input).css, expected); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /test/test-cases/resolve-imports-order/expected.css: -------------------------------------------------------------------------------- 1 | :import("custom-path.css") { 2 | /* empty to check the order */ 3 | } 4 | 5 | :import("./cc.css") { 6 | i__imported_cc_3: cc; 7 | i__imported_dd_5: dd; 8 | } 9 | 10 | :import("./bb.css") { 11 | somevalue: localvalue; 12 | i__imported_bb_1: bb; 13 | i__imported_dd_6: dd; 14 | } 15 | 16 | :import("./aa.css") { 17 | i__imported_aa_0: aa; 18 | i__imported_bb_2: bb; 19 | i__imported_cc_4: cc; 20 | } 21 | 22 | :import("./dd.css") { 23 | i__imported_dd_7: dd; 24 | } 25 | 26 | .a { 27 | composes: i__imported_aa_0; 28 | } 29 | 30 | .b { 31 | composes: i__imported_bb_1; 32 | composes: i__imported_bb_2; 33 | } 34 | 35 | .c { 36 | composes: i__imported_cc_3; 37 | composes: i__imported_cc_4; 38 | } 39 | 40 | .d { 41 | composes: i__imported_dd_5; 42 | composes: i__imported_dd_6; 43 | composes: i__imported_dd_7; 44 | } 45 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | const postcss = require("postcss"); 4 | const processor = require("../src"); 5 | 6 | function normalize(str) { 7 | return str.replace(/\r\n?/g, "\n").replace(/\n$/, ""); 8 | } 9 | 10 | describe("test-cases", function () { 11 | const testDir = path.join(__dirname, "test-cases"); 12 | fs.readdirSync(testDir).forEach(function (testCase) { 13 | if (fs.existsSync(path.join(testDir, testCase, "source.css"))) { 14 | it("should " + testCase.replace(/-/g, " "), function () { 15 | const input = normalize( 16 | fs.readFileSync(path.join(testDir, testCase, "source.css"), "utf-8") 17 | ); 18 | const expected = normalize( 19 | fs.readFileSync(path.join(testDir, testCase, "expected.css"), "utf-8") 20 | ); 21 | expect(postcss([processor]).process(input).css).toEqual(expected); 22 | }); 23 | } 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/test-cases/multiple-composes/expected.css: -------------------------------------------------------------------------------- 1 | :import("path/library.css") { 2 | i__imported_importName_0: importName; 3 | i__imported_firstImport_1: firstImport; 4 | i__imported_secondImport_2: secondImport; 5 | i__imported_importName2_6: importName2; 6 | i__imported_importName3_7: importName3; 7 | i__imported_importName4_8: importName4; 8 | } 9 | 10 | :import("./aa.css") { 11 | i__imported_a_3: a; 12 | } 13 | 14 | :import("./bb.css") { 15 | i__imported_b_4: b; 16 | } 17 | 18 | :import("./cc.css") { 19 | i__imported_c_5: c; 20 | } 21 | 22 | :local(.exportName) { 23 | composes: i__imported_importName_0, global(beforeName), global(importName) global(secondImport), i__imported_firstImport_1 i__imported_secondImport_2; 24 | other: rule; 25 | } 26 | 27 | :local(.duplicate) { 28 | composes: i__imported_a_3, i__imported_b_4, i__imported_c_5, i__imported_a_3, i__imported_c_5; 29 | } 30 | 31 | :local(.spaces) { 32 | composes: i__imported_importName_0 i__imported_importName2_6, i__imported_importName3_7 i__imported_importName4_8; 33 | } 34 | 35 | :local(.unknown) { 36 | composes: foo bar, baz; 37 | } 38 | -------------------------------------------------------------------------------- /test/topologicalSort.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const assert = require("assert"); 4 | const topologicalSort = require("../src/topologicalSort"); 5 | 6 | const STRICT = true; 7 | 8 | describe("topologicalSort", () => { 9 | it("should resolve graphs", () => { 10 | const graph1 = { 11 | v1: ["v2", "v5"], 12 | v2: [], 13 | v3: ["v2", "v4", "v5"], 14 | v4: [], 15 | v5: [], 16 | }; 17 | 18 | const graph2 = { 19 | v1: ["v2", "v5"], 20 | v2: ["v4"], 21 | v3: ["v2", "v4", "v5"], 22 | v4: [], 23 | v5: [], 24 | }; 25 | 26 | assert.deepEqual(topologicalSort(graph1, STRICT), [ 27 | "v2", 28 | "v5", 29 | "v1", 30 | "v4", 31 | "v3", 32 | ]); 33 | assert.deepEqual(topologicalSort(graph2, STRICT), [ 34 | "v4", 35 | "v2", 36 | "v5", 37 | "v1", 38 | "v3", 39 | ]); 40 | }); 41 | 42 | it("should return exception if there is a cycle in the graph", () => { 43 | const graph = { 44 | v1: ["v3"], 45 | v2: [], 46 | v3: ["v1"], 47 | }; 48 | 49 | const er = topologicalSort(graph, STRICT); 50 | 51 | assert.ok(er instanceof Error, "Expected exception"); 52 | assert.deepEqual(er.nodes, ["v1", "v3"]); 53 | }); 54 | 55 | it("should resolve graph in non-strict mode", () => { 56 | const graph = { 57 | v1: ["v3"], 58 | v2: [], 59 | v3: ["v1"], 60 | }; 61 | 62 | assert.deepEqual(topologicalSort(graph, !STRICT), ["v3", "v1", "v2"]); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/topologicalSort.js: -------------------------------------------------------------------------------- 1 | const PERMANENT_MARKER = 2; 2 | const TEMPORARY_MARKER = 1; 3 | 4 | function createError(node, graph) { 5 | const er = new Error("Nondeterministic import's order"); 6 | 7 | const related = graph[node]; 8 | const relatedNode = related.find( 9 | (relatedNode) => graph[relatedNode].indexOf(node) > -1 10 | ); 11 | 12 | er.nodes = [node, relatedNode]; 13 | 14 | return er; 15 | } 16 | 17 | function walkGraph(node, graph, state, result, strict) { 18 | if (state[node] === PERMANENT_MARKER) { 19 | return; 20 | } 21 | 22 | if (state[node] === TEMPORARY_MARKER) { 23 | if (strict) { 24 | return createError(node, graph); 25 | } 26 | 27 | return; 28 | } 29 | 30 | state[node] = TEMPORARY_MARKER; 31 | 32 | const children = graph[node]; 33 | const length = children.length; 34 | 35 | for (let i = 0; i < length; ++i) { 36 | const error = walkGraph(children[i], graph, state, result, strict); 37 | 38 | if (error instanceof Error) { 39 | return error; 40 | } 41 | } 42 | 43 | state[node] = PERMANENT_MARKER; 44 | 45 | result.push(node); 46 | } 47 | 48 | function topologicalSort(graph, strict) { 49 | const result = []; 50 | const state = {}; 51 | 52 | const nodes = Object.keys(graph); 53 | const length = nodes.length; 54 | 55 | for (let i = 0; i < length; ++i) { 56 | const er = walkGraph(nodes[i], graph, state, result, strict); 57 | 58 | if (er instanceof Error) { 59 | return er; 60 | } 61 | } 62 | 63 | return result; 64 | } 65 | 66 | module.exports = topologicalSort; 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-modules-extract-imports", 3 | "version": "3.1.0", 4 | "description": "A CSS Modules transform to extract local aliases for inline imports", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": "^10 || ^12 || >= 14" 8 | }, 9 | "files": [ 10 | "src" 11 | ], 12 | "scripts": { 13 | "prettier": "prettier -l --ignore-path .gitignore . \"!test/test-cases\"", 14 | "eslint": "eslint --ignore-path .gitignore .", 15 | "lint": "yarn eslint && yarn prettier", 16 | "test:only": "jest", 17 | "test:watch": "jest --watch", 18 | "test:coverage": "jest --coverage --collectCoverageFrom=\"src/**/*\"", 19 | "pretest": "yarn lint", 20 | "test": "yarn test:coverage", 21 | "prepublishOnly": "yarn test" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/css-modules/postcss-modules-extract-imports.git" 26 | }, 27 | "keywords": [ 28 | "css-modules", 29 | "postcss", 30 | "plugin" 31 | ], 32 | "author": "Glen Maddern", 33 | "license": "ISC", 34 | "bugs": { 35 | "url": "https://github.com/css-modules/postcss-modules-extract-imports/issues" 36 | }, 37 | "homepage": "https://github.com/css-modules/postcss-modules-extract-imports", 38 | "devDependencies": { 39 | "coveralls": "^3.1.0", 40 | "eslint": "^7.10.0", 41 | "eslint-config-prettier": "^6.12.0", 42 | "husky": "^4.3.0", 43 | "jest": "^26.5.2", 44 | "lint-staged": "^10.4.0", 45 | "postcss": "^8.1.1", 46 | "prettier": "^2.1.2" 47 | }, 48 | "peerDependencies": { 49 | "postcss": "^8.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org/). 5 | 6 | ## [3.1.0](https://github.com/postcss-modules-local-by-default/compare/v3.0.0...v3.1.0) - 2024-04-03 7 | 8 | ### Features 9 | 10 | - support multiple composes, i.e. `.class { composes: a b, c, e d from global, f g from "./file.css"; }` 11 | 12 | ## [3.0.0](https://github.com/postcss-modules-local-by-default/compare/v3.0.0-rc.3...v3.0.0) - 2020-10-13 13 | 14 | ### Fixes 15 | 16 | - compatibility with plugins other plugins 17 | 18 | ## [3.0.0-rc.3](https://github.com/postcss-modules-local-by-default/compare/v3.0.0-rc.2...v3.0.0-rc.3) - 2020-10-11 19 | 20 | ### Fixes 21 | 22 | - broken release 23 | 24 | ## [3.0.0-rc.2](https://github.com/postcss-modules-local-by-default/compare/v3.0.0-rc.1...v3.0.0-rc.2) - 2020-10-08 25 | 26 | ### BREAKING CHANGE 27 | 28 | - minimum supported `postcss` version is `^8.1.0` 29 | 30 | ### Fixes 31 | 32 | - minimum supported `Node.js` version is `^10 || ^12 || >= 14` 33 | - compatibility with other plugins 34 | - compatibility with PostCSS 8 35 | 36 | ## [3.0.0-rc.1](https://github.com/postcss-modules-local-by-default/compare/v3.0.0-rc.0...v3.0.0-rc.1) - 2020-09-18 37 | 38 | ### Fixes 39 | 40 | - avoid using `postcss` directly for new rules and decls 41 | 42 | ## [3.0.0-rc.0](https://github.com/postcss-modules-local-by-default/compare/2.0.0...v3.0.0-rc.0) - 2020-09-18 43 | 44 | ### BREAKING CHANGE 45 | 46 | - minimum supported `Node.js` version is `>= 10.13.0 || >= 12.13.0 || >= 14` 47 | - minimum supported `postcss` version is `^8.0.3` 48 | - `postcss` was moved to `peerDependencies`, you need to install `postcss` in your project before use the plugin 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS Modules: Extract Imports 2 | 3 | [![Build Status](https://travis-ci.org/css-modules/postcss-modules-extract-imports.svg?branch=master)](https://travis-ci.org/css-modules/postcss-modules-extract-imports) 4 | 5 | Transforms: 6 | 7 | ```css 8 | :local(.continueButton) { 9 | composes: button from "library/button.css"; 10 | color: green; 11 | } 12 | ``` 13 | 14 | into: 15 | 16 | ```css 17 | :import("library/button.css") { 18 | button: __tmp_487387465fczSDGHSABb; 19 | } 20 | :local(.continueButton) { 21 | composes: __tmp_487387465fczSDGHSABb; 22 | color: green; 23 | } 24 | ``` 25 | 26 | ## Specification 27 | 28 | - Only a certain whitelist of properties are inspected. Currently, that whitelist is `['composes']` alone. 29 | - An extend-import has the following format: 30 | 31 | ``` 32 | composes: className [... className] from "path/to/file.css", className [... className], className [... className] from global; 33 | ``` 34 | 35 | ## Options 36 | 37 | - `failOnWrongOrder` `bool` generates exception for unpredictable imports order. 38 | 39 | ```css 40 | .aa { 41 | composes: b from "./b.css"; 42 | composes: c from "./c.css"; 43 | } 44 | 45 | .bb { 46 | /* "b.css" should be before "c.css" in this case */ 47 | composes: c from "./c.css"; 48 | composes: b from "./b.css"; 49 | } 50 | ``` 51 | 52 | ## Building 53 | 54 | ``` 55 | npm install 56 | npm test 57 | ``` 58 | 59 | [![Build Status](https://travis-ci.org/css-modules/postcss-modules-extract-imports.svg?branch=master)](https://travis-ci.org/css-modules/postcss-modules-extract-imports) 60 | 61 | - Lines: [![Coverage Status](https://coveralls.io/repos/css-modules/postcss-modules-extract-imports/badge.svg?branch=master)](https://coveralls.io/r/css-modules/postcss-modules-extract-imports?branch=master) 62 | - Statements: [![codecov.io](http://codecov.io/github/css-modules/postcss-modules-extract-imports/coverage.svg?branch=master)](http://codecov.io/github/css-modules/postcss-modules-extract-imports?branch=master) 63 | 64 | ## License 65 | 66 | ISC 67 | 68 | ## With thanks 69 | 70 | - Mark Dalgleish 71 | - Tobias Koppers 72 | - Guy Bedford 73 | 74 | --- 75 | 76 | Glen Maddern, 2015. 77 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const topologicalSort = require("./topologicalSort"); 2 | 3 | const matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/; 4 | const icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/; 5 | 6 | const VISITED_MARKER = 1; 7 | 8 | /** 9 | * :import('G') {} 10 | * 11 | * Rule 12 | * composes: ... from 'A' 13 | * composes: ... from 'B' 14 | 15 | * Rule 16 | * composes: ... from 'A' 17 | * composes: ... from 'A' 18 | * composes: ... from 'C' 19 | * 20 | * Results in: 21 | * 22 | * graph: { 23 | * G: [], 24 | * A: [], 25 | * B: ['A'], 26 | * C: ['A'], 27 | * } 28 | */ 29 | function addImportToGraph(importId, parentId, graph, visited) { 30 | const siblingsId = parentId + "_" + "siblings"; 31 | const visitedId = parentId + "_" + importId; 32 | 33 | if (visited[visitedId] !== VISITED_MARKER) { 34 | if (!Array.isArray(visited[siblingsId])) { 35 | visited[siblingsId] = []; 36 | } 37 | 38 | const siblings = visited[siblingsId]; 39 | 40 | if (Array.isArray(graph[importId])) { 41 | graph[importId] = graph[importId].concat(siblings); 42 | } else { 43 | graph[importId] = siblings.slice(); 44 | } 45 | 46 | visited[visitedId] = VISITED_MARKER; 47 | 48 | siblings.push(importId); 49 | } 50 | } 51 | 52 | module.exports = (options = {}) => { 53 | let importIndex = 0; 54 | const createImportedName = 55 | typeof options.createImportedName !== "function" 56 | ? (importName /*, path*/) => 57 | `i__imported_${importName.replace(/\W/g, "_")}_${importIndex++}` 58 | : options.createImportedName; 59 | const failOnWrongOrder = options.failOnWrongOrder; 60 | 61 | return { 62 | postcssPlugin: "postcss-modules-extract-imports", 63 | prepare() { 64 | const graph = {}; 65 | const visited = {}; 66 | const existingImports = {}; 67 | const importDecls = {}; 68 | const imports = {}; 69 | 70 | return { 71 | Once(root, postcss) { 72 | // Check the existing imports order and save refs 73 | root.walkRules((rule) => { 74 | const matches = icssImport.exec(rule.selector); 75 | 76 | if (matches) { 77 | const [, /*match*/ doubleQuotePath, singleQuotePath] = matches; 78 | const importPath = doubleQuotePath || singleQuotePath; 79 | 80 | addImportToGraph(importPath, "root", graph, visited); 81 | 82 | existingImports[importPath] = rule; 83 | } 84 | }); 85 | 86 | root.walkDecls(/^composes$/, (declaration) => { 87 | const multiple = declaration.value.split(","); 88 | const values = []; 89 | 90 | multiple.forEach((value) => { 91 | const matches = value.trim().match(matchImports); 92 | 93 | if (!matches) { 94 | values.push(value); 95 | 96 | return; 97 | } 98 | 99 | let tmpSymbols; 100 | let [ 101 | , 102 | /*match*/ symbols, 103 | doubleQuotePath, 104 | singleQuotePath, 105 | global, 106 | ] = matches; 107 | 108 | if (global) { 109 | // Composing globals simply means changing these classes to wrap them in global(name) 110 | tmpSymbols = symbols.split(/\s+/).map((s) => `global(${s})`); 111 | } else { 112 | const importPath = doubleQuotePath || singleQuotePath; 113 | 114 | let parent = declaration.parent; 115 | let parentIndexes = ""; 116 | 117 | while (parent.type !== "root") { 118 | parentIndexes = 119 | parent.parent.index(parent) + "_" + parentIndexes; 120 | parent = parent.parent; 121 | } 122 | 123 | const { selector } = declaration.parent; 124 | const parentRule = `_${parentIndexes}${selector}`; 125 | 126 | addImportToGraph(importPath, parentRule, graph, visited); 127 | 128 | importDecls[importPath] = declaration; 129 | imports[importPath] = imports[importPath] || {}; 130 | 131 | tmpSymbols = symbols.split(/\s+/).map((s) => { 132 | if (!imports[importPath][s]) { 133 | imports[importPath][s] = createImportedName(s, importPath); 134 | } 135 | 136 | return imports[importPath][s]; 137 | }); 138 | } 139 | 140 | values.push(tmpSymbols.join(" ")); 141 | }); 142 | 143 | declaration.value = values.join(", "); 144 | }); 145 | 146 | const importsOrder = topologicalSort(graph, failOnWrongOrder); 147 | 148 | if (importsOrder instanceof Error) { 149 | const importPath = importsOrder.nodes.find((importPath) => 150 | // eslint-disable-next-line no-prototype-builtins 151 | importDecls.hasOwnProperty(importPath) 152 | ); 153 | const decl = importDecls[importPath]; 154 | 155 | throw decl.error( 156 | "Failed to resolve order of composed modules " + 157 | importsOrder.nodes 158 | .map((importPath) => "`" + importPath + "`") 159 | .join(", ") + 160 | ".", 161 | { 162 | plugin: "postcss-modules-extract-imports", 163 | word: "composes", 164 | } 165 | ); 166 | } 167 | 168 | let lastImportRule; 169 | 170 | importsOrder.forEach((path) => { 171 | const importedSymbols = imports[path]; 172 | let rule = existingImports[path]; 173 | 174 | if (!rule && importedSymbols) { 175 | rule = postcss.rule({ 176 | selector: `:import("${path}")`, 177 | raws: { after: "\n" }, 178 | }); 179 | 180 | if (lastImportRule) { 181 | root.insertAfter(lastImportRule, rule); 182 | } else { 183 | root.prepend(rule); 184 | } 185 | } 186 | 187 | lastImportRule = rule; 188 | 189 | if (!importedSymbols) { 190 | return; 191 | } 192 | 193 | Object.keys(importedSymbols).forEach((importedSymbol) => { 194 | rule.append( 195 | postcss.decl({ 196 | value: importedSymbol, 197 | prop: importedSymbols[importedSymbol], 198 | raws: { before: "\n " }, 199 | }) 200 | ); 201 | }); 202 | }); 203 | }, 204 | }; 205 | }, 206 | }; 207 | }; 208 | 209 | module.exports.postcss = true; 210 | --------------------------------------------------------------------------------