├── .editorconfig ├── .eslintrc.js ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .npmrc ├── .releaserc.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Taskfile ├── gulpfile.js ├── index.js ├── package.json ├── prettier.config.cjs └── test ├── fixture-1 ├── a.css ├── a1.css ├── main.html └── style.css ├── fixture-2 ├── a.css ├── a1.css ├── b.css ├── b1.css ├── main.html └── style.css ├── fixture-3 ├── a.css ├── main.html ├── recursive │ └── b.css └── style.css └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | mocha: true, 7 | es6: true, 8 | }, 9 | parserOptions: { 10 | ecmaVersion: 2020, 11 | }, 12 | extends: ['eslint:recommended'], 13 | plugins: [], 14 | overrides: [ 15 | { 16 | files: ['test/**/*.js'], 17 | rules: { 18 | 'max-lines': 0, 19 | }, 20 | }, 21 | ], 22 | }; 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'Test / Build / Release' 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | test: 8 | name: 'Test' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: 'Checkout repository' 12 | uses: actions/checkout@v3 13 | - name: 'Setup node' 14 | uses: actions/setup-node@v3 15 | with: 16 | node-version: 18 17 | - name: 'Install depependencies' 18 | run: npm install 19 | - name: 'Test' 20 | run: | 21 | npm run test 22 | 23 | release: 24 | name: 'Release' 25 | runs-on: ubuntu-latest 26 | needs: test 27 | if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/next') 28 | steps: 29 | - name: 'Checkout repository' 30 | uses: actions/checkout@v3 31 | - name: 'Setup Node' 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: 18 35 | - name: 'Install depependencies' 36 | run: | 37 | npm install 38 | - name: 'Build' 39 | run: | 40 | npm run build 41 | - name: 'Release' 42 | run: | 43 | npx semantic-release 44 | env: 45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 46 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | dist 4 | ~* 5 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock = false 2 | -------------------------------------------------------------------------------- /.releaserc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | '@semantic-release/commit-analyzer', 4 | '@semantic-release/release-notes-generator', 5 | '@semantic-release/changelog', 6 | [ 7 | '@semantic-release/npm', 8 | { 9 | pkgRoot: 'dist', 10 | }, 11 | ], 12 | '@semantic-release/github', 13 | '@semantic-release/git', 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [7.2.0](https://github.com/unlight/postcss-import-url/compare/v7.1.0...v7.2.0) (2022-12-14) 2 | 3 | 4 | ### Features 5 | 6 | * Add support for base64 encoded data urls ([a70fc3c](https://github.com/unlight/postcss-import-url/commit/a70fc3c75ee5d2fc00cf5883c0387598b55fe438)), closes [#30](https://github.com/unlight/postcss-import-url/issues/30) 7 | 8 | # [7.1.0](https://github.com/unlight/postcss-import-url/compare/v7.0.0...v7.1.0) (2022-11-30) 9 | 10 | 11 | ### Features 12 | 13 | * Support imports into `[@layer](https://github.com/layer)` ([7f42a8e](https://github.com/unlight/postcss-import-url/commit/7f42a8eb0e0bd547a135413de0e157770615fcf5)), closes [#28](https://github.com/unlight/postcss-import-url/issues/28) 14 | 15 | # [7.0.0](https://github.com/unlight/postcss-import-url/compare/v6.0.4...v7.0.0) (2021-02-13) 16 | 17 | 18 | ### Bug Fixes 19 | 20 | * Set element source property [#27](https://github.com/unlight/postcss-import-url/issues/27) ([a4687df](https://github.com/unlight/postcss-import-url/commit/a4687dfb5c9a77531b6869711d1ce38d09d17a6a)) 21 | 22 | 23 | ### BREAKING CHANGES 24 | 25 | * Node LTS is required (v12+) 26 | 27 | ## [6.0.4](https://github.com/unlight/postcss-import-url/compare/v6.0.3...v6.0.4) (2021-02-13) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * Revert set element source property ([da4afb8](https://github.com/unlight/postcss-import-url/commit/da4afb82207ebcb04871eac184e2866528f5f9a1)) 33 | 34 | ## [6.0.3](https://github.com/unlight/postcss-import-url/compare/v6.0.2...v6.0.3) (2021-02-13) 35 | 36 | 37 | ### Bug Fixes 38 | 39 | * Set element source property ([da96061](https://github.com/unlight/postcss-import-url/commit/da96061bdd53253e02bc23e1ce86d2e38347aeed)), closes [#27](https://github.com/unlight/postcss-import-url/issues/27) 40 | 41 | ## [6.0.2](https://github.com/unlight/postcss-import-url/compare/v6.0.1...v6.0.2) (2021-02-13) 42 | 43 | 44 | ### Bug Fixes 45 | 46 | * Revert set element source property ([e7207c8](https://github.com/unlight/postcss-import-url/commit/e7207c8ea7ad2f28e8ae2771781b4e20d38dd8e3)) 47 | 48 | ## [6.0.1](https://github.com/unlight/postcss-import-url/compare/v6.0.0...v6.0.1) (2021-02-13) 49 | 50 | 51 | ### Bug Fixes 52 | 53 | * Set element source property ([8405f32](https://github.com/unlight/postcss-import-url/commit/8405f32b9aa09bef534e67717bf7de3d52477547)), closes [#27](https://github.com/unlight/postcss-import-url/issues/27) 54 | 55 | # [6.0.0](https://github.com/unlight/postcss-import-url/compare/v5.1.0...v6.0.0) (2020-12-09) 56 | 57 | 58 | ### Features 59 | 60 | * Support PostCSS v8 ([2d17367](https://github.com/unlight/postcss-import-url/commit/2d173670da93ab88a428ade3a05a792f79503b7e)) 61 | 62 | 63 | ### BREAKING CHANGES 64 | 65 | * Support PostCSS v8 66 | 67 | # [5.1.0](https://github.com/unlight/postcss-import-url/compare/v5.0.0...v5.1.0) (2020-04-04) 68 | 69 | 70 | ### Features 71 | 72 | * supports multiple url resolves ([4997931](https://github.com/unlight/postcss-import-url/commit/4997931bf216c8b740fae7518c13cb457e840053)) 73 | 74 | # [5.0.0](https://github.com/unlight/postcss-import-url/compare/v4.0.0...v5.0.0) (2020-03-01) 75 | 76 | 77 | ### chore 78 | 79 | * Added development stuff (prettier, eslint, etc.) ([70e5497](https://github.com/unlight/postcss-import-url/commit/70e5497a750dd7e7935ed4f08fde76f30b69b955)) 80 | 81 | 82 | ### Features 83 | 84 | * Resolve relative URLs in property values ([c11c03d](https://github.com/unlight/postcss-import-url/commit/c11c03d10f8d5016d4cec811f40fed6f6140e6f1)) 85 | 86 | 87 | ### BREAKING CHANGES 88 | 89 | * Node 10+ is required 90 | 91 | # [4.0.0](https://github.com/unlight/postcss-import-url/compare/v3.0.4...v4.0.0) (2019-01-03) 92 | 93 | 94 | ### chore 95 | 96 | * Updated dependencies ([b1ed8b2](https://github.com/unlight/postcss-import-url/commit/b1ed8b2)) 97 | 98 | 99 | ### Features 100 | 101 | * Added semantic release ([69c494a](https://github.com/unlight/postcss-import-url/commit/69c494a)) 102 | 103 | 104 | ### BREAKING CHANGES 105 | 106 | * Node 6+ is required now 107 | 108 | ## 3.X.X 109 | 110 | * 3.0.4 (04 Jan 2018) - fixed [#15](https://github.com/unlight/postcss-import-url/issues/15) 111 | * 3.0.3 (02 Jan 2018) - fixed [#13](https://github.com/unlight/postcss-import-url/issues/13), set peer dependency as range >=6 <7 112 | * 3.0.1 (30 Oct 2017) - updated dependencies (postcss 6), moved postcss to peerDependencies 113 | * 3.0.0 (27 Oct 2017) - using `atRule.source.input.file` to resolve urls 114 | * 2.2.0 (19 Nov 2016) - added option modernBrowser 115 | * 2.1.1 (19 Oct 2016) - added to readme google font notice 116 | * 2.1.0 (09 Jul 2016) - replaced Object.assign by _.assign 117 | * 2.0.0 (08 Jul 2016) - added recursive option 118 | * 1.0.0 (01 Nov 2015) - first release 119 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # postcss-import-url 2 | 3 | [PostCSS](https://github.com/postcss/postcss) plugin inlines remote files. 4 | 5 | ```css 6 | /* Input example */ 7 | @import 'https://fonts.googleapis.com/css?family=Tangerine'; 8 | body { 9 | font-size: 13px; 10 | } 11 | ``` 12 | 13 | ```css 14 | /* Output example */ 15 | @font-face { 16 | font-family: 'Tangerine'; 17 | font-style: normal; 18 | font-weight: 400; 19 | src: url(https://fonts.gstatic.com/s/tangerine/v12/IurY6Y5j_oScZZow4VOxCZZM.woff2) 20 | format('woff2'); 21 | } 22 | body { 23 | font-size: 13px; 24 | } 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```js 30 | const importUrl = require('postcss-import-url'); 31 | const options = {}; 32 | postcss([importUrl(options)]).process(css, { 33 | // Define a `from` option to resolve relative @imports in the initial css to a url. 34 | from: 'https://example.com/styles.css', 35 | }); 36 | ``` 37 | 38 | See [PostCSS](https://github.com/postcss/postcss#usage) docs for examples for your environment. 39 | 40 | ## Options 41 | 42 | - `recursive` (boolean) To import URLs recursively (default: `true`) 43 | - `resolveUrls` (boolean) To transform relative URLs found in remote stylesheets into fully qualified URLs ([see #18](https://github.com/unlight/postcss-import-url/pull/18)) (default: `false`) 44 | - `modernBrowser` (boolean) Set user-agent string to 'Mozilla/5.0 AppleWebKit/537.36 Chrome/80.0.0.0 Safari/537.36', this option maybe useful for importing fonts from Google. Google check `user-agent` header string and respond can be different (default: `false`) 45 | - `userAgent` (string) Custom user-agent header (default: `null`) 46 | - `dataUrls` (boolean) Store fetched CSS as base64 encoded data URLs (default: `false`) 47 | 48 | ## Known Issues 49 | 50 | - Google fonts returns different file types per the user agent. Because postcss runs in a shell, 51 | Google returns truetype fonts rather than the better woff2 format. 52 | Use option `modernBrowser` to explicitly load woff2 fonts. 53 | -------------------------------------------------------------------------------- /Taskfile: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PATH="$PWD/node_modules/.bin":$PATH 3 | set -e 4 | 5 | build_cp() { 6 | set -x 7 | rm -rfv dist 8 | mkdir dist 9 | cp -rfv index.js dist 10 | cp -fv README.md LICENSE package.json dist 11 | set +x 12 | } 13 | 14 | "$@" 15 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var connect = require('gulp-connect'); 3 | var mocha = require('gulp-mocha'); 4 | var eslint = require('gulp-eslint'); 5 | 6 | var files = ['index.js', 'gulpfile.js']; 7 | 8 | gulp.task('lint', function () { 9 | return gulp.src(files).pipe(eslint()).pipe(eslint.format()); 10 | // .pipe(eslint.failAfterError()); 11 | }); 12 | 13 | gulp.task('test', function () { 14 | var startServer = gulp.task('start-server'); 15 | startServer(); 16 | return gulp 17 | .src('test/*.js', { read: false }) 18 | .pipe(mocha({ timeout: 5000 })) 19 | .on('end', connect.serverClose); 20 | }); 21 | 22 | gulp.task('test:w', function () { 23 | var startServer = gulp.task('start-server'); 24 | startServer(); 25 | return gulp 26 | .src('test/*.js', { read: false }) 27 | .pipe(mocha({ timeout: 5000, watch: true })) 28 | .on('end', connect.serverClose); 29 | }); 30 | 31 | gulp.task('default', gulp.series(['lint', 'test'])); 32 | 33 | gulp.task('watch', function () { 34 | gulp.watch(files, gulp.series(['lint', 'test'])); 35 | }); 36 | 37 | gulp.task('start-server', function () { 38 | connect.server({ 39 | root: './test', 40 | port: 1234, 41 | }); 42 | }); 43 | 44 | gulp.task('close-server', function () { 45 | connect.serverClose(); 46 | }); 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const hh = require('http-https'); 3 | const isUrl = require('is-url'); 4 | const trim = require('lodash.trim'); 5 | const resolveRelative = require('resolve-relative-url'); 6 | const assign = require('lodash.assign'); 7 | const url = require('url'); 8 | 9 | const defaults = { 10 | recursive: true, 11 | resolveUrls: false, 12 | modernBrowser: false, 13 | userAgent: null, 14 | dataUrls: false, 15 | }; 16 | const space = postcss.list.space; 17 | const urlRegexp = /url\(["']?.+?['"]?\)/g; 18 | 19 | function postcssImportUrl(options) { 20 | options = assign({}, defaults, options || {}); 21 | 22 | async function importUrl(tree, _, parentRemoteFile) { 23 | parentRemoteFile = parentRemoteFile || tree.source.input.file; 24 | const imports = []; 25 | tree.walkAtRules('import', function checkAtRule(atRule) { 26 | const params = space(atRule.params); 27 | let remoteFile = cleanupRemoteFile(params[0]); 28 | if (parentRemoteFile) { 29 | remoteFile = resolveRelative(remoteFile, parentRemoteFile); 30 | } 31 | if (!isUrl(remoteFile)) { 32 | return; 33 | } 34 | imports[imports.length] = createPromise(remoteFile, options).then( 35 | async r => { 36 | let newNode = postcss.parse(r.body); 37 | let hasLayer = params.find(param => param.includes('layer')); 38 | let hasSupports = params.find(param => param.includes('supports')); 39 | 40 | const mediaQueries = params 41 | .slice(hasLayer ? (hasSupports ? 3 : 2) : 1) 42 | .join(' '); 43 | 44 | if (mediaQueries) { 45 | const mediaNode = postcss.atRule({ 46 | name: 'media', 47 | params: mediaQueries, 48 | source: atRule.source, 49 | }); 50 | mediaNode.append(newNode); 51 | newNode = mediaNode; 52 | } else { 53 | newNode.source = atRule.source; 54 | } 55 | 56 | if (hasSupports) { 57 | const supportQuery = params.find(param => 58 | param.includes('supports'), 59 | ); 60 | 61 | let init = supportQuery.indexOf('('); 62 | let fin = supportQuery.indexOf(')'); 63 | let query = supportQuery.substr(init + 1, fin - init - 1); 64 | 65 | const supportsNode = postcss.atRule({ 66 | name: 'supports', 67 | params: `(${query})`, 68 | source: atRule.source, 69 | }); 70 | supportsNode.append(newNode); 71 | newNode = supportsNode; 72 | } else { 73 | newNode.source = atRule.source; 74 | } 75 | 76 | if (hasLayer) { 77 | const layer = params.find(param => param.includes('layer')); 78 | 79 | let init = layer.indexOf('('); 80 | let fin = layer.indexOf(')'); 81 | let layerName = layer.substr(init + 1, fin - init - 1); 82 | 83 | const layerNode = postcss.atRule({ 84 | name: 'layer', 85 | params: layerName, 86 | source: newNode.source, 87 | }); 88 | 89 | layerNode.append(newNode); 90 | newNode = layerNode; 91 | } 92 | 93 | if (options.resolveUrls) { 94 | // Convert relative paths to absolute paths 95 | newNode = newNode.replaceValues(urlRegexp, { fast: 'url(' }, url => 96 | resolveUrls(url, remoteFile), 97 | ); 98 | } 99 | 100 | const importedTree = await (options.recursive 101 | ? importUrl(newNode, null, r.parent) 102 | : Promise.resolve(newNode)); 103 | 104 | if (options.dataUrls) { 105 | atRule.params = `url(data:text/css;base64,${Buffer.from(importedTree.toString()).toString('base64')})`; 106 | } else { 107 | atRule.replaceWith(importedTree); 108 | } 109 | }, 110 | ); 111 | }); 112 | await Promise.all(imports); 113 | return tree; 114 | } 115 | 116 | return { 117 | postcssPlugin: 'postcss-import-url', 118 | Once: importUrl, 119 | }; 120 | } 121 | 122 | module.exports = postcssImportUrl; 123 | module.exports.postcss = true; 124 | 125 | function cleanupRemoteFile(value) { 126 | if (value.substr(0, 3) === 'url') { 127 | value = value.substr(3); 128 | } 129 | value = trim(value, '\'"()'); 130 | return value; 131 | } 132 | 133 | function resolveUrls(to, from) { 134 | return 'url("' + resolveRelative(cleanupRemoteFile(to), from) + '")'; 135 | } 136 | 137 | function createPromise(remoteFile, options) { 138 | const reqOptions = urlParse(remoteFile); 139 | reqOptions.headers = {}; 140 | reqOptions.headers['connection'] = 'keep-alive'; 141 | if (options.modernBrowser) { 142 | reqOptions.headers['user-agent'] = 143 | 'Mozilla/5.0 AppleWebKit/538.0 Chrome/88.0.0.0 Safari/538'; 144 | } 145 | if (options.userAgent) { 146 | reqOptions.headers['user-agent'] = String(options.userAgent); 147 | } 148 | function executor(resolve, reject) { 149 | const request = hh.get(reqOptions, response => { 150 | let body = ''; 151 | response.on('data', chunk => { 152 | body += chunk.toString(); 153 | }); 154 | response.on('end', () => { 155 | resolve({ 156 | body: body, 157 | parent: remoteFile, 158 | }); 159 | }); 160 | }); 161 | request.on('error', reject); 162 | request.end(); 163 | } 164 | return new Promise(executor); 165 | } 166 | 167 | function urlParse(remoteFile) { 168 | const reqOptions = url.parse(remoteFile); 169 | return reqOptions; 170 | } 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-import-url", 3 | "version": "0.0.0-dev", 4 | "description": "PostCSS plugin inlines remote files.", 5 | "main": "index.js", 6 | "keywords": [ 7 | "postcss", 8 | "css", 9 | "postcss-plugin" 10 | ], 11 | "license": "MIT", 12 | "homepage": "https://github.com/unlight/postcss-import-url", 13 | "engines": { 14 | "node": ">=10" 15 | }, 16 | "scripts": { 17 | "build": "sh Taskfile build_cp", 18 | "eslint": "node node_modules/eslint/bin/eslint index.js", 19 | "eslint:fix": "npm run eslint -- --fix", 20 | "test": "gulp", 21 | "test:w": "gulp test:w" 22 | }, 23 | "dependencies": { 24 | "http-https": "^1.0.0", 25 | "is-url": "^1.2.4", 26 | "lodash.assign": "^4.2.0", 27 | "lodash.trim": "^4.5.1", 28 | "resolve-relative-url": "^1.0.0" 29 | }, 30 | "peerDependencies": { 31 | "postcss": "^8.0.0" 32 | }, 33 | "devDependencies": { 34 | "@semantic-release/changelog": "^6.0.2", 35 | "@semantic-release/git": "^10.0.1", 36 | "expect": "^29.3.1", 37 | "gulp": "^4.0.2", 38 | "gulp-connect": "^5.7.0", 39 | "gulp-eslint": "^6.0.0", 40 | "gulp-mocha": "^8.0.0", 41 | "ololog": "^1.1.175", 42 | "postcss": "^8.2.4", 43 | "precise-commits": "^1.0.2", 44 | "prettier": "^2.8.0", 45 | "semantic-release": "^19.0.5", 46 | "tcp-ping": "^0.1.1" 47 | }, 48 | "directories": { 49 | "test": "test" 50 | }, 51 | "repository": { 52 | "type": "git", 53 | "url": "git+https://github.com/unlight/postcss-import-url.git" 54 | }, 55 | "bugs": { 56 | "url": "https://github.com/unlight/postcss-import-url/issues" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /prettier.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 80, 3 | trailingComma: 'all', 4 | tabWidth: 2, 5 | semi: true, 6 | singleQuote: true, 7 | arrowParens: 'avoid', 8 | }; 9 | -------------------------------------------------------------------------------- /test/fixture-1/a.css: -------------------------------------------------------------------------------- 1 | @import 'a1.css'; 2 | .a { 3 | content: '.a'; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture-1/a1.css: -------------------------------------------------------------------------------- 1 | .a1 { 2 | content: '.a1'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture-1/main.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixture-1/style.css: -------------------------------------------------------------------------------- 1 | @import url('./a.css'); 2 | .style { 3 | content: '.style'; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture-2/a.css: -------------------------------------------------------------------------------- 1 | @import 'a1.css'; 2 | a. { 3 | content: '.a'; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture-2/a1.css: -------------------------------------------------------------------------------- 1 | .a1 { 2 | content: '.a1'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture-2/b.css: -------------------------------------------------------------------------------- 1 | @import './b1.css'; 2 | .b { 3 | content: '.b'; 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture-2/b1.css: -------------------------------------------------------------------------------- 1 | .b1 { 2 | content: '.b1'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture-2/main.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixture-2/style.css: -------------------------------------------------------------------------------- 1 | @import url('./a.css'); 2 | @import url(http://localhost:1234/fixture-2/b.css); 3 | .style { 4 | content: '.style'; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture-3/a.css: -------------------------------------------------------------------------------- 1 | @import './recursive/b.css'; 2 | 3 | @font-face { 4 | src: url('./font.woff'); 5 | } 6 | .implicit-sibling { 7 | background-image: url('implicit-sibling.png'); 8 | } 9 | .absolute { 10 | background-image: url('http://example.com/absolute.png'); 11 | } 12 | .root-relative { 13 | background-image: url('/root-relative.png'); 14 | } 15 | .sibling { 16 | background-image: url('./sibling.png'); 17 | } 18 | .parent { 19 | background-image: url('../parent.png'); 20 | } 21 | .grandparent { 22 | background-image: url('../../grandparent.png'); 23 | } 24 | -------------------------------------------------------------------------------- /test/fixture-3/main.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/fixture-3/recursive/b.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | src: url('../font-recursive.woff'); 3 | } 4 | 5 | .sibling-recursive { 6 | background-image: url('./sibling-recursive.png'); 7 | } 8 | 9 | .parent-recursive { 10 | background-image: url('../parent-recursive.png'); 11 | } 12 | 13 | .grandparent-recursive { 14 | background-image: url('../../grandparent-recursive.png'); 15 | } 16 | 17 | .absolute-recursive { 18 | background-image: url('http://example.com/absolute-recursive.png'); 19 | } 20 | -------------------------------------------------------------------------------- /test/fixture-3/style.css: -------------------------------------------------------------------------------- 1 | @import url(http://localhost:1234/fixture-3/a.css); 2 | .style { 3 | content: '.style'; 4 | } 5 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const { default: expect } = require('expect'); 3 | const fs = require('fs'); 4 | const plugin = require('../'); 5 | const tcpp = require('tcp-ping'); 6 | const log = require('ololog'); 7 | 8 | const fixture1Css = fs.readFileSync(__dirname + '/fixture-1/style.css', { 9 | encoding: 'utf8', 10 | }); 11 | 12 | const testEqual = function ( 13 | input, 14 | output, 15 | pluginOptions, 16 | postcssOptions, 17 | done, 18 | ) { 19 | getResult(input, pluginOptions, postcssOptions).then(result => { 20 | expect(result.css.trim()).toEqual(output.trim()); 21 | expect(result.warnings()).toHaveLength(0); 22 | done(); 23 | }, done); 24 | }; 25 | 26 | const testContains = function ( 27 | input, 28 | value, 29 | pluginOptions, 30 | postcssOptions, 31 | done, 32 | ) { 33 | getResult(input, pluginOptions, postcssOptions).then(result => { 34 | expect(result.css).toContain(value); 35 | expect(result.warnings()).toHaveLength(0); 36 | done(); 37 | }, done); 38 | }; 39 | 40 | async function getResult(input, pluginOptions, postcssOptions) { 41 | return postcss([plugin(pluginOptions)]).process(input, { 42 | from: undefined, 43 | ...postcssOptions, 44 | }); 45 | } 46 | 47 | describe('import with media queries', function () { 48 | it('only screen', async () => { 49 | const input = 50 | "@import 'http://fonts.googleapis.com/css?family=Tangerine' only screen and (color)"; 51 | const result = await getResult(input); 52 | expect(result.css).toContain('@media only screen and (color)'); 53 | }); 54 | 55 | it('rule with and', function (done) { 56 | const input = 57 | "@import 'http://fonts.googleapis.com/css?family=Tangerine' screen and (orientation:landscape)"; 58 | testContains( 59 | input, 60 | '@media screen and (orientation:landscape)', 61 | {}, 62 | {}, 63 | done, 64 | ); 65 | }); 66 | 67 | it('rule projection, tv', function (done) { 68 | const input = 69 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') projection, tv"; 70 | testContains(input, '@media projection, tv', {}, {}, done); 71 | }); 72 | 73 | it('rule print', function (done) { 74 | const input = 75 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') print"; 76 | testContains(input, '@media print', {}, {}, done); 77 | }); 78 | 79 | it('rule layer', function (done) { 80 | const input = 81 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') layer(test);"; 82 | testContains(input, '@layer test {', {}, {}, done); 83 | }); 84 | 85 | it('rule anonymous layer', function (done) { 86 | const input = 87 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') layer;"; 88 | testContains(input, '@layer {', {}, {}, done); 89 | }); 90 | 91 | it('contains it', function (done) { 92 | const input = 93 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') (min-width: 25em);"; 94 | testContains(input, '(min-width: 25em)', {}, {}, done); 95 | }); 96 | 97 | describe('media query', function () { 98 | it('contains font-family', function (done) { 99 | const input = 100 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') (min-width: 25em);"; 101 | testContains(input, "font-family: 'Tangerine'", {}, {}, done); 102 | }); 103 | 104 | it('contains src local', async () => { 105 | const input = 106 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') (min-width: 25em);"; 107 | const result = await getResult(input); 108 | expect(result.css).toContain('@media (min-width: 25em) {@font-face'); 109 | }); 110 | 111 | it('contains layer', function (done) { 112 | const input = 113 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') layer(test) (min-width: 25em);"; 114 | testContains( 115 | input, 116 | '@layer test {@media (min-width: 25em) {@font-face', 117 | {}, 118 | {}, 119 | done, 120 | ); 121 | }); 122 | 123 | it('contains layer with @supports', function (done) { 124 | const input = 125 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') layer(test) @supports(display: flex);"; 126 | testContains( 127 | input, 128 | '@layer test {@supports (display: flex) {@font-face', 129 | {}, 130 | {}, 131 | done, 132 | ); 133 | }); 134 | 135 | it('contains layer with @supports and @media', function (done) { 136 | const input = 137 | "@import url('http://fonts.googleapis.com/css?family=Tangerine') layer(test) @supports(display: flex) (min-width: 25em);"; 138 | testContains( 139 | input, 140 | '@layer test {@supports (display: flex) {@media (min-width: 25em) {@font-face', 141 | {}, 142 | {}, 143 | done, 144 | ); 145 | }); 146 | }); 147 | }); 148 | 149 | describe('skip non remote files', function () { 150 | it('local', function (done) { 151 | testEqual("@import 'a.css';", "@import 'a.css';", {}, {}, done); 152 | }); 153 | 154 | it('relative parent', function (done) { 155 | const input = "@import '../a.css'"; 156 | testEqual(input, input, {}, {}, done); 157 | }); 158 | 159 | it('relative child', function (done) { 160 | const input = "@import './a/b.css'"; 161 | testEqual(input, input, {}, {}, done); 162 | }); 163 | 164 | it.skip('no protocol', async () => { 165 | const input = '@import url(//example.com/a.css)'; 166 | const result = await getResult(input); 167 | }); 168 | }); 169 | 170 | describe('import url tangerine', function () { 171 | function assertOutputTangerine(result) { 172 | expect(result.css).toContain("font-family: 'Tangerine'"); 173 | expect(result.css).toContain('font-style: normal'); 174 | expect(result.css).toContain('font-weight: 400'); 175 | expect(result.css).toContain('fonts.gstatic.com/s/tangerine'); 176 | } 177 | 178 | it('empty', async () => { 179 | const input = 180 | "@import 'http://fonts.googleapis.com/css?family=Tangerine' ;"; 181 | const result = await getResult(input); 182 | assertOutputTangerine(result); 183 | }); 184 | 185 | it('double quotes', async () => { 186 | const input = '@import "http://fonts.googleapis.com/css?family=Tangerine";'; 187 | const result = await getResult(input); 188 | assertOutputTangerine(result); 189 | }); 190 | 191 | it('single quotes', async () => { 192 | const input = "@import 'http://fonts.googleapis.com/css?family=Tangerine';"; 193 | const result = await getResult(input); 194 | assertOutputTangerine(result); 195 | }); 196 | 197 | it('url single quotes', async () => { 198 | const input = 199 | "@import url('http://fonts.googleapis.com/css?family=Tangerine');"; 200 | const result = await getResult(input); 201 | assertOutputTangerine(result); 202 | }); 203 | 204 | it('url double quotes', async () => { 205 | const input = 206 | '@import url("http://fonts.googleapis.com/css?family=Tangerine");'; 207 | const result = await getResult(input); 208 | assertOutputTangerine(result); 209 | }); 210 | 211 | it('url no quotes', async () => { 212 | const input = 213 | '@import url(http://fonts.googleapis.com/css?family=Tangerine);'; 214 | const result = await getResult(input); 215 | assertOutputTangerine(result); 216 | }); 217 | }); 218 | 219 | describe('recursive import', function () { 220 | it('ping server', done => { 221 | tcpp.probe('localhost', 1234, function (err) { 222 | done(err); 223 | }); 224 | }); 225 | 226 | var opts = { 227 | recursive: true, 228 | }; 229 | 230 | describe('fixture-1', function () { 231 | it('fixture-1 contains class a1', function (done) { 232 | const input = '@import url(http://localhost:1234/fixture-1/style.css)'; 233 | testContains(input, '.a1', opts, {}, done); 234 | }); 235 | 236 | it('fixture-1 contains class a', function (done) { 237 | const input = '@import url(http://localhost:1234/fixture-1/style.css)'; 238 | testContains(input, "content: '.a'", opts, {}, done); 239 | }); 240 | 241 | it('fixture-1 contains class style content', function (done) { 242 | const input = '@import url(http://localhost:1234/fixture-1/style.css)'; 243 | testContains(input, "content: '.style'", opts, {}, done); 244 | }); 245 | 246 | it('fixture-1 contains class a when passed as a string', function (done) { 247 | const input = fixture1Css; 248 | testContains( 249 | input, 250 | "content: '.a'", 251 | opts, 252 | { 253 | from: 'http://localhost:1234/fixture-1/style.css', 254 | }, 255 | done, 256 | ); 257 | }); 258 | }); 259 | 260 | describe('fixture-2', function () { 261 | it('fixture-2 contains class a1', function (done) { 262 | const input = '@import url(http://localhost:1234/fixture-2/style.css)'; 263 | testContains(input, "content: '.a1'", opts, {}, done); 264 | }); 265 | 266 | it('fixture-2 contains class a', function (done) { 267 | const input = '@import url(http://localhost:1234/fixture-2/style.css)'; 268 | testContains(input, "content: '.a'", opts, {}, done); 269 | }); 270 | 271 | it('fixture-2 contains class b1', function (done) { 272 | const input = '@import url(http://localhost:1234/fixture-2/style.css)'; 273 | testContains(input, "content: '.b1'", opts, {}, done); 274 | }); 275 | 276 | it('fixture-2 contains class b', function (done) { 277 | const input = '@import url(http://localhost:1234/fixture-2/style.css)'; 278 | testContains(input, "content: '.b'", opts, {}, done); 279 | }); 280 | 281 | it('fixture-2 contains class style content', function (done) { 282 | const input = '@import url(http://localhost:1234/fixture-2/style.css)'; 283 | testContains(input, "content: '.style'", opts, {}, done); 284 | }); 285 | }); 286 | 287 | describe('fixture-3 convert relative paths in property values', function () { 288 | it('does not resolve relative URLs by default', function (done) { 289 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 290 | testContains(input, "src: url('./font.woff');", {}, {}, done); 291 | }); 292 | 293 | it('does not resolve relative URLs when option.resolveURLs is false', function (done) { 294 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 295 | testContains( 296 | input, 297 | "src: url('./font.woff');", 298 | { resolveUrls: false }, 299 | {}, 300 | done, 301 | ); 302 | }); 303 | 304 | var _opts = { resolveUrls: true }; 305 | 306 | it('resolves relative URLs when option.resolveURLs is true', function (done) { 307 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 308 | testContains( 309 | input, 310 | 'src: url("http://localhost:1234/fixture-3/font.woff");', 311 | _opts, 312 | {}, 313 | done, 314 | ); 315 | }); 316 | 317 | it('does not modify absolute paths', function (done) { 318 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 319 | testContains( 320 | input, 321 | 'background-image: url("http://example.com/absolute.png");', 322 | _opts, 323 | {}, 324 | done, 325 | ); 326 | }); 327 | 328 | it('makes root relative paths absolute', function (done) { 329 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 330 | testContains( 331 | input, 332 | 'background-image: url("http://localhost:1234/root-relative.png")', 333 | _opts, 334 | {}, 335 | done, 336 | ); 337 | }); 338 | 339 | it('makes implicit sibling paths absolute', function (done) { 340 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 341 | testContains( 342 | input, 343 | 'background-image: url("http://localhost:1234/fixture-3/implicit-sibling.png")', 344 | _opts, 345 | {}, 346 | done, 347 | ); 348 | }); 349 | 350 | it('makes relative sibling paths absolute', function (done) { 351 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 352 | testContains( 353 | input, 354 | 'background-image: url("http://localhost:1234/fixture-3/sibling.png")', 355 | _opts, 356 | {}, 357 | done, 358 | ); 359 | }); 360 | 361 | it('makes parent relative paths absolute', function (done) { 362 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 363 | testContains( 364 | input, 365 | 'background-image: url("http://localhost:1234/parent.png")', 366 | _opts, 367 | {}, 368 | done, 369 | ); 370 | }); 371 | 372 | it('makes grandparent relative paths absolute', function (done) { 373 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 374 | testContains( 375 | input, 376 | 'background-image: url("http://localhost:1234/grandparent.png")', 377 | _opts, 378 | {}, 379 | done, 380 | ); 381 | }); 382 | 383 | var _optsRecursive = { resolveUrls: true, recursive: true }; 384 | 385 | // Test paths are resolved for recursively imported stylesheets 386 | it('makes relative sibling paths absolute - recursive', function (done) { 387 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 388 | testContains( 389 | input, 390 | 'background-image: url("http://localhost:1234/fixture-3/recursive/sibling-recursive.png")', 391 | _optsRecursive, 392 | {}, 393 | done, 394 | ); 395 | }); 396 | 397 | it('makes parent relative paths absolute - recursive', function (done) { 398 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 399 | testContains( 400 | input, 401 | 'background-image: url("http://localhost:1234/fixture-3/parent-recursive.png")', 402 | _optsRecursive, 403 | {}, 404 | done, 405 | ); 406 | }); 407 | 408 | it('makes grandparent relative paths absolute - recursive', function (done) { 409 | const input = '@import url(http://localhost:1234/fixture-3/style.css)'; 410 | testContains( 411 | input, 412 | 'background-image: url("http://localhost:1234/grandparent-recursive.png")', 413 | _optsRecursive, 414 | {}, 415 | done, 416 | ); 417 | }); 418 | }); 419 | }); 420 | 421 | describe('google font woff', function () { 422 | it('option modernBrowser should import woff', function (done) { 423 | const input = 424 | '@import url(http://fonts.googleapis.com/css?family=Tangerine);'; 425 | testContains( 426 | input, 427 | "woff2) format('woff2')", 428 | { modernBrowser: true }, 429 | {}, 430 | done, 431 | ); 432 | }); 433 | 434 | it('option agent should import woff', function (done) { 435 | const input = 436 | '@import url(http://fonts.googleapis.com/css?family=Tangerine);'; 437 | var opts = { 438 | userAgent: 439 | 'Mozilla/5.0 AppleWebKit/537.36 Chrome/80.0.2840.99 Safari/537.36', 440 | }; 441 | testContains(input, "woff2) format('woff2')", opts, {}, done); 442 | }); 443 | }); 444 | 445 | describe('source property', () => { 446 | it('regular import', async () => { 447 | const input = 448 | '@import url(http://fonts.googleapis.com/css?family=Tangerine)'; 449 | const result = await getResult(input); 450 | expect(result.root.source.input.css).toEqual(input); 451 | }); 452 | 453 | it('media import', async () => { 454 | const input = 455 | '@import url(http://fonts.googleapis.com/css?family=Tangerine) print'; 456 | const result = await getResult(input); 457 | expect(result.root.source.input.css).toEqual(input); 458 | }); 459 | }); 460 | 461 | describe('base64 data urls', function () { 462 | it('option dataUrls should converts imports to base64 encoded data urls', async () => { 463 | const input = '@import url(http://fonts.googleapis.com/css?family=Tangerine);'; 464 | const result = await getResult(input, { dataUrls: true }); 465 | expect(result.css.trim()).toEqual('@import url(data:text/css;base64,QGZvbnQtZmFjZSB7CiAgZm9udC1mYW1pbHk6ICdUYW5nZXJpbmUnOwogIGZvbnQtc3R5bGU6IG5vcm1hbDsKICBmb250LXdlaWdodDogNDAwOwogIHNyYzogdXJsKGh0dHA6Ly9mb250cy5nc3RhdGljLmNvbS9zL3RhbmdlcmluZS92MTcvSXVyWTZZNWpfb1NjWlpvdzRWT3hDWlpKLnR0ZikgZm9ybWF0KCd0cnVldHlwZScpOwp9Cg==);'); 466 | }); 467 | }); 468 | --------------------------------------------------------------------------------