├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package-lock.json ├── package.json └── test ├── index.test.js └── mocks ├── colors1.css ├── colors2.css ├── node_modules ├── index │ ├── index.css │ └── package.json ├── main │ ├── main.css │ └── package.json └── style │ ├── package.json │ └── style.css └── vendor.css /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain 2 | # consistent coding styles between different editors and IDEs. 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | full: 7 | name: Node.js 18 Full 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout the repository 11 | uses: actions/checkout@v3 12 | - name: Install Node.js 13 | uses: actions/setup-node@v3 14 | with: 15 | node-version: 18 16 | - name: Install dependencies 17 | run: npm ci 18 | - name: Run tests 19 | run: npm test 20 | env: 21 | FORCE_COLOR: 2 22 | short: 23 | runs-on: ubuntu-latest 24 | strategy: 25 | matrix: 26 | node: [16] 27 | name: Node.js ${{ matrix.node }} Quick 28 | steps: 29 | - name: Checkout the repository 30 | uses: actions/checkout@v3 31 | - name: Install Node.js ${{ matrix.node }} 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: ${{ matrix.node }} 35 | - name: Install dependencies 36 | run: npm ci 37 | - name: Run unit tests 38 | run: npm run unit 39 | env: 40 | FORCE_COLOR: 2 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ide 2 | .vscode 3 | .idea 4 | .iml 5 | 6 | # file system 7 | .DS_Store 8 | 9 | # dependencies 10 | /node_modules/ 11 | .npm 12 | 13 | # logs 14 | *.log 15 | 16 | # tests 17 | coverage 18 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | yarn-error.log 3 | package-lock.json 4 | yarn.lock 5 | 6 | test/ 7 | coverage/ 8 | .github/ 9 | .npmignore 10 | .gitignore 11 | .editorconfig 12 | .npmrc 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | ## 1.3.0 (2023-09-06) 7 | 8 | ### Features 9 | 10 | - emit dependency message announcing dependency to imported file — thanks [Daniel Jagszent](https://github.com/d--j) 11 | 12 | ## 1.2.0 (2023-08-28) 13 | 14 | ### Features 15 | 16 | - import files based on the current directory - thanks [linhe0x0](https://github.com/linhe0x0) 17 | - more unit tests — thanks [Kristoffer Nordström](https://github.com/42tte) 18 | 19 | ## 1.1.0 (2023-08-10) 20 | 21 | ### Features 22 | 23 | - enable import from `node_modules` - thanks [Kristoffer Nordström](https://github.com/42tte) 24 | - allow use of CSS [url() function syntax](https://developer.mozilla.org/en-US/docs/Web/CSS/url) - thanks [Kristoffer Nordström](https://github.com/42tte) 25 | 26 | For example: 27 | 28 | ```css 29 | @nested-import url("./test/mocks/colors2.css"); 30 | h1 { 31 | color: red; 32 | } 33 | ``` 34 | 35 | ### Fixes 36 | 37 | - updated dependencies, fixed all dependabot alerts 38 | 39 | ## 1.0.0 (2023-04-17) 40 | 41 | - renamed the API — from `@import` to `@nested-import`, to prevent clashes with [`postcss-import`](https://github.com/postcss/postcss-import) plugin 42 | - updated dependencies 43 | 44 | ## 0.2.0 (2022-02-10) 45 | 46 | - added `c8` for coverage and uvu for unit tests 47 | - enforces 100% coverage 48 | - removed `jest` 49 | - removed `console.log` which reports contents of the resolved `@import` 50 | - moved mock test files into subfolder within `test/mocks/` (previously mock vendors.css was shipping to npm) 51 | - updated to the latest v8 PostCSS (set as peer dependency) 52 | improved the algorithm to account for both single and double and missing quotes within `@import` 53 | - improved the algorithm to try-catch fs errors and report them properly 54 | - removed Travis, added GH actions 55 | - yarn is assumed to be the preferred over npm, but added npm lockfile to gitignore and `.npmrc` just in case somebody forks and tries to run it using npm i 56 | - added `prettier` and `eslint` to trigger VSCode to format/lint the code (previous setup wasn't picked up by VSCode plugins) 57 | - committed a fresh yarn lock file (GH actions will expect it too) 58 | 59 | Closes issues #2, #3, #4, #5. 60 | 61 | ## 0.1.0 (2017-04-21) 62 | 63 | - Basic nested import functionality 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Erik Harper 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PostCSS Nested Import

2 | 3 |

PostCSS plugin for importing other stylesheet source files anywhere in your CSS.

4 | 5 |

6 | 7 | GitHub CI test 8 | 9 |

10 | 11 | Before: 12 | 13 | ```css 14 | /* vendor.css */ 15 | .vendor { 16 | background: silver; 17 | } 18 | 19 | /* index.css */ 20 | :global { 21 | @nested-import './vendor.css'; 22 | } 23 | ``` 24 | 25 | After: 26 | 27 | ```css 28 | :global { 29 | .vendor { 30 | background: silver; 31 | } 32 | } 33 | ``` 34 | 35 | ## Usage 36 | 37 | ```js 38 | postcss([require("postcss-nested-import")]); 39 | ``` 40 | 41 | See [PostCSS](https://github.com/postcss/postcss) docs for examples for your environment. 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const { readFileSync } = require("fs"); 3 | const resolve = require("resolve"); 4 | 5 | /** 6 | * @type {import('postcss').PluginCreator} 7 | */ 8 | module.exports = () => { 9 | return { 10 | AtRule: { 11 | "nested-import": (node, { result }) => { 12 | if ( 13 | !node.params || 14 | typeof node.params !== "string" || 15 | node.params.length < 3 16 | ) { 17 | return; 18 | } 19 | 20 | let id = node.params 21 | .replace(/^(url\(\s*)?['"]?/, "") 22 | .replace(/['"]?\s*(\))?$/, ""); 23 | 24 | let replacement; 25 | 26 | let basedir = process.cwd(); 27 | if (node.source && node.source.input && node.source.input.file) { 28 | basedir = path.dirname(node.source.input.file); 29 | } 30 | 31 | try { 32 | let resolvedPath = resolve.sync(id, { 33 | basedir, 34 | extensions: [".css"], 35 | moduleDirectory: ["web_modules", "node_modules"], 36 | packageFilter: (pkg) => { 37 | if (pkg.style) pkg.main = pkg.style; 38 | else if (!pkg.main || !/\.css$/.test(pkg.main)) 39 | pkg.main = "index.css"; 40 | return pkg; 41 | } 42 | }); 43 | 44 | replacement = readFileSync(resolvedPath, "utf8"); 45 | result.messages.push({ 46 | file: resolvedPath, 47 | parent: result.opts.from, 48 | plugin: "postcss-nested-import", 49 | type: "dependency" 50 | }); 51 | } catch (error) { 52 | throw node.error(`error reading file:\n${id}`); 53 | } 54 | node.replaceWith(`${node.raws.before}${replacement}`); 55 | } 56 | }, 57 | postcssPlugin: "postcss-nested-import" 58 | }; 59 | }; 60 | 61 | module.exports.postcss = true; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-nested-import", 3 | "version": "1.3.0", 4 | "description": "PostCSS plugin for importing other stylesheet source files anywhere in your CSS.", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "import" 10 | ], 11 | "author": "Erik Harper ", 12 | "license": "MIT", 13 | "repository": "eriklharper/postcss-nested-import", 14 | "bugs": { 15 | "url": "https://github.com/eriklharper/postcss-nested-import/issues" 16 | }, 17 | "homepage": "https://github.com/eriklharper/postcss-nested-import", 18 | "scripts": { 19 | "unit": "uvu . '\\.test\\.(ts|js)$'", 20 | "test": "c8 npm run unit && eslint .", 21 | "format": "prettier --write . --ignore-unknown" 22 | }, 23 | "peerDependencies": { 24 | "postcss": "^8.3.0" 25 | }, 26 | "engines": { 27 | "node": ">=12.0" 28 | }, 29 | "prettier": { 30 | "trailingComma": "none" 31 | }, 32 | "eslintConfig": { 33 | "extends": [ 34 | "@logux/eslint-config", 35 | "plugin:prettier/recommended" 36 | ], 37 | "rules": { 38 | "security/detect-non-literal-require": "off", 39 | "node/global-require": "off", 40 | "consistent-return": "off" 41 | } 42 | }, 43 | "c8": { 44 | "exclude": [ 45 | "**/*.test.*" 46 | ], 47 | "lines": 100, 48 | "check-coverage": true 49 | }, 50 | "devDependencies": { 51 | "@logux/eslint-config": "^51.0.1", 52 | "c8": "^8.0.1", 53 | "eslint": "^8.46.0", 54 | "eslint-config-prettier": "^9.0.0", 55 | "eslint-config-standard": "^17.1.0", 56 | "eslint-plugin-import": "^2.28.0", 57 | "eslint-plugin-n": "^16.0.1", 58 | "eslint-plugin-prefer-let": "^3.0.1", 59 | "eslint-plugin-prettier": "^5.0.0", 60 | "eslint-plugin-promise": "^6.1.1", 61 | "jest": "^29.6.2", 62 | "postcss": "^8.4.27", 63 | "prettier": "^3.0.1", 64 | "uvu": "^0.5.6" 65 | }, 66 | "dependencies": { 67 | "resolve": "^1.22.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | let path = require("path"); 2 | let { test } = require("uvu"); 3 | let { equal, match } = require("uvu/assert"); 4 | let postcss = require("postcss"); 5 | 6 | let nestedImport = require("../"); 7 | 8 | // ----------------------------------------------------------------------------- 9 | 10 | async function run(input, output, opts, from) { 11 | let result = await postcss([nestedImport(opts)]).process(input, { 12 | from 13 | }); 14 | equal(result.css, output); 15 | equal(result.warnings().length, 0); 16 | return result; 17 | } 18 | 19 | async function catchError(fn) { 20 | let error; 21 | try { 22 | await fn(); 23 | } catch (e) { 24 | error = e; 25 | } 26 | return error; 27 | } 28 | 29 | // ----------------------------------------------------------------------------- 30 | 31 | test("01 - replaces one instance of @nested-import", async () => { 32 | await run( 33 | `@media (prefers-color-scheme: light) { 34 | :root:not([data-theme='dark']) { 35 | @nested-import './test/mocks/colors1.css'; 36 | } 37 | }`, 38 | `@media (prefers-color-scheme: light) { 39 | :root:not([data-theme='dark']) { 40 | h1 { 41 | color: red; 42 | } 43 | } 44 | }` 45 | ); 46 | }); 47 | 48 | test("02 - two instances, different quote styles", async () => { 49 | await run( 50 | `@media (prefers-color-scheme: light) { 51 | :root:not([data-theme='dark']) { 52 | @nested-import './test/mocks/colors1.css'; 53 | @nested-import "./test/mocks/colors2.css"; 54 | @nested-import ./test/mocks/colors1.css; 55 | } 56 | }`, 57 | `@media (prefers-color-scheme: light) { 58 | :root:not([data-theme='dark']) { 59 | h1 { 60 | color: red; 61 | } 62 | h1 { 63 | color: blue; 64 | } 65 | h1 { 66 | color: red; 67 | } 68 | } 69 | }` 70 | ); 71 | }); 72 | 73 | test("03 - only @import in one line", async () => { 74 | await run( 75 | `@nested-import './test/mocks/vendor.css';`, 76 | `.vendor { 77 | background: silver; 78 | }.vendor-font { 79 | font-size: 14px; 80 | }` 81 | ); 82 | }); 83 | 84 | test("04 - replaces @nested-import nested under :global", async () => { 85 | await run( 86 | `:global { @nested-import './test/mocks/vendor.css'; background: gold; }`, 87 | `:global { .vendor { 88 | background: silver; 89 | } .vendor-font { 90 | font-size: 14px; 91 | } background: gold; }` 92 | ); 93 | }); 94 | 95 | test("05 - empty import", async () => { 96 | await run( 97 | `@media (prefers-color-scheme: light) { 98 | :root:not([data-theme='dark']) { 99 | @nested-import ; 100 | } 101 | }`, 102 | `@media (prefers-color-scheme: light) { 103 | :root:not([data-theme='dark']) { 104 | @nested-import ; 105 | } 106 | }` 107 | ); 108 | }); 109 | 110 | test("06 - throws with a meaningful message when fs error happens", async () => { 111 | let error = await catchError(() => 112 | run( 113 | `@media (prefers-color-scheme: light) { 114 | :root:not([data-theme='dark']) { 115 | @nested-import "nonexistent"; 116 | } 117 | }` 118 | ) 119 | ); 120 | match(error.message, /error reading file/); 121 | match(error.message, /nonexistent/); 122 | }); 123 | 124 | test("07 - do not import @import", async () => { 125 | await run( 126 | `@import "./test/mocks/colors2.css"; 127 | @media (prefers-color-scheme: light) { 128 | :root:not([data-theme='dark']) { 129 | @nested-import './test/mocks/colors1.css'; 130 | @import "./test/mocks/colors2.css"; 131 | } 132 | }`, 133 | `@import "./test/mocks/colors2.css"; 134 | @media (prefers-color-scheme: light) { 135 | :root:not([data-theme='dark']) { 136 | h1 { 137 | color: red; 138 | } 139 | @import "./test/mocks/colors2.css"; 140 | } 141 | }` 142 | ); 143 | }); 144 | 145 | test("08 - do import url() function", async () => { 146 | await run( 147 | `@media (prefers-color-scheme: light) { 148 | :root:not([data-theme='dark']) { 149 | @nested-import url('./test/mocks/colors1.css'); 150 | @nested-import url("./test/mocks/colors2.css"); 151 | @nested-import url(./test/mocks/colors1.css); 152 | } 153 | }`, 154 | `@media (prefers-color-scheme: light) { 155 | :root:not([data-theme='dark']) { 156 | h1 { 157 | color: red; 158 | } 159 | h1 { 160 | color: blue; 161 | } 162 | h1 { 163 | color: red; 164 | } 165 | } 166 | }` 167 | ); 168 | }); 169 | 170 | test("09 - url() function with line breaks", async () => { 171 | await run( 172 | `@media (prefers-color-scheme: light) { 173 | :root:not([data-theme='dark']) { 174 | @nested-import url( 175 | './test/mocks/colors1.css' 176 | ); 177 | @nested-import url( 178 | "./test/mocks/colors2.css" 179 | ); 180 | @nested-import url( 181 | ./test/mocks/colors1.css 182 | ); 183 | } 184 | }`, 185 | `@media (prefers-color-scheme: light) { 186 | :root:not([data-theme='dark']) { 187 | h1 { 188 | color: red; 189 | } 190 | h1 { 191 | color: blue; 192 | } 193 | h1 { 194 | color: red; 195 | } 196 | } 197 | }` 198 | ); 199 | }); 200 | 201 | test("10 - package.json filter style, main & index", async () => { 202 | await run( 203 | `@nested-import './test/mocks/node_modules/style'; @nested-import './test/mocks/node_modules/main'; @nested-import './test/mocks/node_modules/index';`, 204 | `.style {} .main {} .index {}` 205 | ); 206 | }); 207 | 208 | test("11 - import based on current directory", async () => { 209 | await run( 210 | `@media (prefers-color-scheme: light) { 211 | :root:not([data-theme='dark']) { 212 | @nested-import './mocks/colors1.css'; 213 | } 214 | }`, 215 | `@media (prefers-color-scheme: light) { 216 | :root:not([data-theme='dark']) { 217 | h1 { 218 | color: red; 219 | } 220 | } 221 | }`, 222 | undefined, 223 | path.join(__dirname, "app.css") 224 | ); 225 | }); 226 | 227 | test.run(); 228 | -------------------------------------------------------------------------------- /test/mocks/colors1.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: red; 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/colors2.css: -------------------------------------------------------------------------------- 1 | h1 { 2 | color: blue; 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/node_modules/index/index.css: -------------------------------------------------------------------------------- 1 | .index {} 2 | -------------------------------------------------------------------------------- /test/mocks/node_modules/index/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "main.js" 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/node_modules/main/main.css: -------------------------------------------------------------------------------- 1 | .main {} 2 | -------------------------------------------------------------------------------- /test/mocks/node_modules/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "main.css" 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/node_modules/style/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "style": "style.css" 3 | } 4 | -------------------------------------------------------------------------------- /test/mocks/node_modules/style/style.css: -------------------------------------------------------------------------------- 1 | .style {} 2 | -------------------------------------------------------------------------------- /test/mocks/vendor.css: -------------------------------------------------------------------------------- 1 | .vendor { 2 | background: silver; 3 | } 4 | .vendor-font { 5 | font-size: 14px; 6 | } 7 | --------------------------------------------------------------------------------