├── .eslintignore ├── test ├── units │ └── .gitkeep ├── cases │ ├── default │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── style.css │ ├── retina │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── style.css │ ├── smart │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── style.css │ ├── background │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── webpack.config.test.js │ │ ├── index.html │ │ ├── test.js │ │ └── style.css │ ├── image-set │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── media-query-order.html │ │ ├── index.html │ │ ├── style.css │ │ ├── test.js │ │ └── test.html │ ├── public-path │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── style.css │ ├── postcss-plugins │ │ ├── index.js │ │ ├── index.html │ │ ├── style.css │ │ └── webpack.config.js │ └── extract-text-webpack-plugin │ │ ├── index.js │ │ ├── webpack.config.js │ │ ├── index.html │ │ └── style.css ├── fixtures │ └── images │ │ ├── gift.png │ │ ├── home.png │ │ ├── html.png │ │ ├── tag.png │ │ ├── light.png │ │ ├── tag@2x.png │ │ ├── adaptive.png │ │ ├── gift@2x.png │ │ ├── home@2x.png │ │ ├── html@2x.png │ │ ├── light@2x.png │ │ ├── lollipop.png │ │ ├── adaptive@2x.png │ │ ├── calculator.png │ │ ├── lollipop@2x.png │ │ ├── calculator@2x.png │ │ └── retina │ │ ├── minion.png │ │ ├── radio.png │ │ ├── minion@2x.png │ │ ├── minion@3x.png │ │ ├── minion@4x.png │ │ ├── mountains.png │ │ ├── radio@2x.png │ │ ├── radio@3x.png │ │ ├── radio@4x.png │ │ ├── satelite.png │ │ ├── angry-birds.png │ │ ├── mountains@2x.png │ │ ├── mountains@3x.png │ │ ├── mountains@4x.png │ │ ├── satelite@2x.png │ │ ├── satelite@3x.png │ │ ├── satelite@4x.png │ │ ├── angry-birds@1x.png │ │ ├── angry-birds@2x.png │ │ ├── angry-birds@3x.png │ │ ├── angry-birds@4x.png │ │ ├── captain-america.png │ │ ├── captain-america@1x.png │ │ ├── captain-america@2x.png │ │ ├── captain-america@3x.png │ │ └── captain-america@4x.png ├── final.test.js ├── integration.test.js ├── default.test.js ├── retina.test.js ├── dist │ └── default.test.dev.js └── extract-text-webpack-plugin.test.js ├── example ├── index.js ├── screenshot.png ├── webpack.config.js ├── index.html └── style.css ├── index.js ├── .eslintrc ├── src ├── meta.js ├── loader.js ├── README.md ├── Plugin.js ├── computeNewBackground.js └── postcssPlugin.js ├── .gitignore ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── README.zh-CN.md └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | test/ 2 | -------------------------------------------------------------------------------- /test/units/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/default/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/retina/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/smart/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/loader'); 2 | -------------------------------------------------------------------------------- /test/cases/background/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/image-set/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/public-path/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/postcss-plugins/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /test/cases/extract-text-webpack-plugin/index.js: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | -------------------------------------------------------------------------------- /example/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/example/screenshot.png -------------------------------------------------------------------------------- /test/fixtures/images/gift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/gift.png -------------------------------------------------------------------------------- /test/fixtures/images/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/home.png -------------------------------------------------------------------------------- /test/fixtures/images/html.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/html.png -------------------------------------------------------------------------------- /test/fixtures/images/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/tag.png -------------------------------------------------------------------------------- /test/fixtures/images/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/light.png -------------------------------------------------------------------------------- /test/fixtures/images/tag@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/tag@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/adaptive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/adaptive.png -------------------------------------------------------------------------------- /test/fixtures/images/gift@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/gift@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/home@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/home@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/html@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/html@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/light@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/light@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/lollipop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/lollipop.png -------------------------------------------------------------------------------- /test/fixtures/images/adaptive@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/adaptive@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/calculator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/calculator.png -------------------------------------------------------------------------------- /test/fixtures/images/lollipop@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/lollipop@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/calculator@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/calculator@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/minion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/radio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/minion@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/minion@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/minion@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/minion@4x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/mountains.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/radio@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/radio@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/radio@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/radio@4x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/satelite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/angry-birds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/mountains@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/mountains@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/mountains@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/mountains@4x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/satelite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/satelite@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/satelite@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/satelite@4x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/angry-birds@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@1x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/angry-birds@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/angry-birds@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/angry-birds@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/angry-birds@4x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/captain-america.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america.png -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "vusion", 3 | "env": { 4 | "commonjs": true, 5 | "node": true, 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /test/fixtures/images/retina/captain-america@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@1x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/captain-america@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@2x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/captain-america@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@3x.png -------------------------------------------------------------------------------- /test/fixtures/images/retina/captain-america@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vusion/css-sprite-loader/HEAD/test/fixtures/images/retina/captain-america@4x.png -------------------------------------------------------------------------------- /test/final.test.js: -------------------------------------------------------------------------------- 1 | require('./default.test.js'); 2 | // require('./extract-text-webpack-plugin.test.js'); 3 | require('./retina.test.js'); 4 | require('./integration.test.js'); 5 | 6 | // require('./cases/background/test'); 7 | -------------------------------------------------------------------------------- /src/meta.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | PLUGIN_NAME: 'cssSpritePlugin', 3 | MODULE_MARK: 'isCSSSpriteModule', 4 | REPLACER_NAME: 'CSS_SPRITE_LOADER_IMAGE', 5 | REPLACER_RE: /CSS_SPRITE_LOADER_IMAGE\(([^)]*?),\s*([^)]*)\)/g, 6 | }; 7 | -------------------------------------------------------------------------------- /src/loader.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const postcssPlugin = require('./postcssPlugin'); 4 | const { createLoader } = require('base-css-image-loader'); 5 | 6 | const loader = createLoader([postcssPlugin]); 7 | loader.Plugin = require('./Plugin'); 8 | 9 | module.exports = loader; 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Locks 9 | package-lock.json 10 | yarn.lock 11 | 12 | # Cache 13 | .DS_Store 14 | .cache 15 | .npm 16 | 17 | # Environment 18 | .env 19 | 20 | # Dependency directories 21 | node_modules/ 22 | 23 | # Editors 24 | .vscode/ 25 | .idea/ 26 | 27 | # Build Output 28 | dest/ 29 | 30 | # Coverage 31 | lib-cov 32 | coverage 33 | .nyc_output 34 | -------------------------------------------------------------------------------- /test/integration.test.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack'); 3 | 4 | const cases = ['background', 'image-set', 'postcss-plugins', 'public-path', 'smart']; 5 | 6 | describe('Webpack Integration Tests', () => { 7 | cases.forEach((caseName) => { 8 | it('#test webpack integration case: ' + caseName, (done) => { 9 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, done); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /example/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin()], 19 | }; 20 | -------------------------------------------------------------------------------- /test/cases/retina/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin()], 19 | }; 20 | -------------------------------------------------------------------------------- /test/cases/background/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin()], 19 | }; 20 | -------------------------------------------------------------------------------- /test/cases/default/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin()], 19 | }; 20 | -------------------------------------------------------------------------------- /test/cases/background/webpack.config.test.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/', 9 | filename: '[name].js', 10 | publicPath: '/', 11 | }, 12 | context: __dirname, 13 | module: { 14 | rules: [ 15 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 16 | { test: /\.png$/, use: ['file-loader'] }, 17 | ], 18 | }, 19 | plugins: [new CSSSpritePlugin()], 20 | }; 21 | -------------------------------------------------------------------------------- /test/cases/smart/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin({ 19 | publicPath: 'dest/', 20 | imageSetFallback: true, 21 | })], 22 | }; 23 | -------------------------------------------------------------------------------- /test/cases/image-set/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: 'dest/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin({ 19 | publicPath: 'dest/', 20 | imageSetFallback: true, 21 | })], 22 | }; 23 | -------------------------------------------------------------------------------- /test/cases/public-path/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | module.exports = { 4 | entry: { 5 | bundle: './index.js', 6 | }, 7 | output: { 8 | path: __dirname + '/dest', 9 | filename: '[name].js', 10 | publicPath: '/some/public/', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index')] }, 15 | { test: /\.png$/, use: ['file-loader'] }, 16 | ], 17 | }, 18 | plugins: [new CSSSpritePlugin({ 19 | output: 'static', 20 | publicPath: 'http://cdn.163.com/cdn/static', 21 | })], 22 | }; 23 | -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # 说明 2 | + backgroundParser: 单行background解析器 3 | + backgroundBlockParser: cssBlock 中background属性集解析器,生成parsedRule 4 | 5 | + Plugin2 修改imageList结构到cssBlockList 6 | ``` 7 | cssBlockList{ 8 | 'cssRule-hash':{ 9 | parsedRule: backgroundBlockParser生成 10 | hash: 原css属性字符串生成的哈希码 11 | divWidth: 容器宽度 12 | divHeight: 容器高度 13 | images: cssblock中所有的图片引用 14 | } 15 | } 16 | ``` 17 | 修改position、size算法 18 | ``` 19 | r = css设置的size 与 雪碧图中的图片size比例 20 | background-size = r * 雪碧图的长宽 21 | background-postion = -r* 雪碧图中的图片偏移 + css设置的position 22 | 23 | ``` 24 | 修改imageSet兼容方式为media query,重算雪碧图的size和position 25 | 26 | 兼容老版本retina写法 27 | 28 | 29 | # 局限 30 | 31 | + backgroundBlockParser暂不支持多背景图解析(backgroundParser可以解析单行多图) 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /test/cases/default/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |

Sprite display

17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /test/cases/image-set/media-query-order.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 23 | 24 | 25 |
26 | 27 | 28 | -------------------------------------------------------------------------------- /test/cases/postcss-plugins/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |

Sprite display

17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /test/cases/public-path/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 | 10 |
11 |

Source images display

12 |
13 |
14 |
15 |
16 |
17 |

Sprite display

18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source Image Display

11 |
12 |
13 |
14 |
15 |
16 |

Sprite Image Display

17 |
18 |
19 |
20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /test/cases/extract-text-webpack-plugin/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin'); 3 | 4 | module.exports = { 5 | entry: { 6 | bundle: './index.js', 7 | }, 8 | output: { 9 | path: __dirname + '/dest', 10 | filename: '[name].js', 11 | }, 12 | module: { 13 | rules: [ 14 | { test: /\.css$/, use: ExtractTextPlugin.extract({ 15 | fallback: 'style-loader', 16 | use: ['css-loader', require.resolve('../../../index')], 17 | }) }, 18 | { test: /\.png$/, use: ['file-loader'] }, 19 | ], 20 | }, 21 | plugins: [ 22 | new CSSSpritePlugin(), 23 | new ExtractTextPlugin('bundle.css'), 24 | ], 25 | }; 26 | -------------------------------------------------------------------------------- /test/cases/extract-text-webpack-plugin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 | 10 |
11 |

Source images display

12 |
13 |
14 |
15 |
16 |
17 |

Sprite display

18 |
19 |
20 |
21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /test/cases/retina/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |

Sprite display

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018-present NetEase Inc. 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 | -------------------------------------------------------------------------------- /test/cases/smart/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |

Sprite display

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /test/cases/image-set/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |

Sprite display

20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | 29 | 30 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:8 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | branches: 18 | only: 19 | - master 20 | - next 21 | 22 | working_directory: ~/css-sprite-loader 23 | 24 | steps: 25 | - checkout 26 | 27 | # Download and cache dependencies 28 | - restore_cache: 29 | keys: 30 | - v1-dependencies-{{ checksum "package.json" }} 31 | # fallback to using the latest cache if no exact match is found 32 | - v1-dependencies- 33 | 34 | - run: npm install 35 | 36 | - save_cache: 37 | paths: 38 | - node_modules 39 | key: v1-dependencies-{{ checksum "package.json" }} 40 | 41 | # run tests! 42 | - run: npm run test 43 | -------------------------------------------------------------------------------- /test/cases/default/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/home.png'); 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/home.png?sprite'); 19 | } 20 | 21 | .source.query { 22 | width: 128px; 23 | height: 128px; 24 | background: url('../../fixtures/images/lollipop.png'); 25 | } 26 | .sprite.query { 27 | width: 128px; 28 | height: 128px; 29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite'); 30 | } 31 | 32 | .source.rename { 33 | width: 128px; 34 | height: 128px; 35 | background: url('../../fixtures/images/tag.png'); 36 | } 37 | .sprite.rename { 38 | width: 128px; 39 | height: 128px; 40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav'); 41 | } 42 | 43 | .source.rename-2 { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/html.png'); 47 | } 48 | .sprite.rename-2 { 49 | width: 128px; 50 | height: 128px; 51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav'); 52 | } 53 | -------------------------------------------------------------------------------- /test/cases/public-path/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/home.png'); 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/home.png?sprite'); 19 | } 20 | 21 | .source.query { 22 | width: 128px; 23 | height: 128px; 24 | background: url('../../fixtures/images/lollipop.png'); 25 | } 26 | .sprite.query { 27 | width: 128px; 28 | height: 128px; 29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite'); 30 | } 31 | 32 | .source.rename { 33 | width: 128px; 34 | height: 128px; 35 | background: url('../../fixtures/images/tag.png'); 36 | } 37 | .sprite.rename { 38 | width: 128px; 39 | height: 128px; 40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav'); 41 | } 42 | 43 | .source.rename-2 { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/html.png'); 47 | } 48 | .sprite.rename-2 { 49 | width: 128px; 50 | height: 128px; 51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav'); 52 | } 53 | -------------------------------------------------------------------------------- /test/cases/postcss-plugins/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/home.png'); 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/home.png?sprite'); 19 | } 20 | 21 | .source.query { 22 | width: 128px; 23 | height: 128px; 24 | background: url('../../fixtures/images/lollipop.png'); 25 | } 26 | .sprite.query { 27 | width: 128px; 28 | height: 128px; 29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite'); 30 | } 31 | 32 | .source.rename { 33 | width: 128px; 34 | height: 128px; 35 | background: url('../../fixtures/images/tag.png'); 36 | } 37 | .sprite.rename { 38 | width: 128px; 39 | height: 128px; 40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav'); 41 | } 42 | 43 | .source.rename-2 { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/html.png'); 47 | } 48 | .sprite.rename-2 { 49 | width: 128px; 50 | height: 128px; 51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav'); 52 | } 53 | -------------------------------------------------------------------------------- /test/cases/extract-text-webpack-plugin/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/home.png'); 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/home.png?sprite'); 19 | } 20 | 21 | .source.query { 22 | width: 128px; 23 | height: 128px; 24 | background: url('../../fixtures/images/lollipop.png'); 25 | } 26 | .sprite.query { 27 | width: 128px; 28 | height: 128px; 29 | background: url('../../fixtures/images/lollipop.png?sprite=sprite'); 30 | } 31 | 32 | .source.rename { 33 | width: 128px; 34 | height: 128px; 35 | background: url('../../fixtures/images/tag.png'); 36 | } 37 | .sprite.rename { 38 | width: 128px; 39 | height: 128px; 40 | background: url('../../fixtures/images/tag.png?sprite=sprite-nav'); 41 | } 42 | 43 | .source.rename-2 { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/html.png'); 47 | } 48 | .sprite.rename-2 { 49 | width: 128px; 50 | height: 128px; 51 | background: url('../../fixtures/images/html.png?sprite=sprite-nav'); 52 | } 53 | -------------------------------------------------------------------------------- /test/default.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack'); 4 | const expect = require('chai').expect; 5 | const { utils } = require('base-css-image-loader'); 6 | 7 | const caseName = 'default'; 8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g; 9 | 10 | describe('Webpack Integration test', () => { 11 | it('#test default config: ' + caseName, (done) => { 12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => { 13 | if (err) 14 | return done(err); 15 | 16 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'sprite.png')); 17 | const md5Code = utils.genMD5(filesContent); 18 | expect(md5Code).to.eql('d8defd30309a90e991feff6014911569'); 19 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'sprite-nav.png')); 20 | const md5Code2 = utils.genMD5(filesContent2); 21 | expect(md5Code2).to.eql('8c44ae541ba21ba1c42011335f3b0801'); 22 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'bundle.js')).toString(); 23 | expect(replaceReg.test(cssContent)).to.eql(false); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /test/cases/postcss-plugins/webpack.config.js: -------------------------------------------------------------------------------- 1 | const CSSSpritePlugin = require('../../../index').Plugin; 2 | 3 | const postcssPlugins = [ 4 | require('postcss-px-to-viewport')({ 5 | viewportWidth: 750, // (Number) The width of the viewport 6 | // viewportHeight: 1334, // (Number) The height of the viewport. 7 | unitPrecision: 3, // (Number) The decimal numbers to allow the REM units to grow to. 8 | viewportUnit: 'vw', // (String) Expected units. 9 | selectorBlackList: ['.ignore', '.hairlines'], // (Array) The selectors to ignore and leave as px. 10 | minPixelValue: 1, // (Number) Set the minimum pixel value to replace. 11 | mediaQuery: false, // (Boolean) Allow px to be converted in media queries. 12 | }), 13 | require('postcss-viewport-units')({}), 14 | ]; 15 | 16 | module.exports = { 17 | entry: { 18 | bundle: './index.js', 19 | }, 20 | output: { 21 | path: __dirname + '/dest', 22 | filename: '[name].js', 23 | publicPath: 'dest/', 24 | }, 25 | module: { 26 | rules: [ 27 | { test: /\.css$/, use: ['style-loader', 'css-loader', require.resolve('../../../index'), { 28 | loader: 'postcss-loader', 29 | options: { plugins: postcssPlugins }, 30 | }] }, 31 | { test: /\.png$/, use: ['file-loader'] }, 32 | ], 33 | }, 34 | plugins: [new CSSSpritePlugin({ 35 | plugins: postcssPlugins, 36 | })], 37 | }; 38 | -------------------------------------------------------------------------------- /test/retina.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack'); 4 | const expect = require('chai').expect; 5 | const { utils } = require('base-css-image-loader'); 6 | 7 | const caseName = 'retina'; 8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g; 9 | 10 | describe('Webpack Integration test', () => { 11 | it('#test retina config: ' + caseName, (done) => { 12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => { 13 | if (err) 14 | return done(err); 15 | 16 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'sprite.png')); 17 | const md5Code = utils.genMD5(filesContent); 18 | expect(md5Code).to.eql('873103c642eaf6ee9d736d4703f2201d'); 19 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@2x.png')); 20 | const md5Code2 = utils.genMD5(filesContent2); 21 | expect(md5Code2).to.eql('51d951f98092152d8fc56bf3380577e3'); 22 | const filesContent3 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@3x.png')); 23 | const md5Code3 = utils.genMD5(filesContent3); 24 | expect(md5Code3).to.eql('62594d2f59ff829b82c49e9d717d7759'); 25 | const filesContent4 = fs.readFileSync(path.resolve(data.outputPath, 'sprite@4x.png')); 26 | const md5Code4 = utils.genMD5(filesContent4); 27 | expect(md5Code4).to.eql('4a6a7dbace7933efe321b357d4db2fb9'); 28 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'bundle.js')).toString(); 29 | expect(replaceReg.test(cssContent)).to.eql(false); 30 | done(); 31 | }); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/dist/default.test.dev.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var fs = require('fs'); 4 | 5 | var path = require('path'); 6 | 7 | var expect = require('chai').expect; 8 | 9 | var _require = require('base-css-image-loader'), 10 | utils = _require.utils; 11 | 12 | var shell = require('shelljs'); 13 | 14 | var execa = require('execa'); 15 | 16 | var caseName = 'default'; 17 | var replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g; 18 | describe('Webpack Integration test', function () { 19 | var buildCLI = path.resolve(__dirname, '../node_modules/.bin/webpack'); 20 | var runDir = path.join('../test/cases/' + caseName); 21 | var destDir = path.join('./cases/' + caseName + '/dest'); 22 | before(function () { 23 | shell.cd(path.resolve(__dirname, runDir)); 24 | }); 25 | afterEach(function () { 26 | shell.rm('-rf', path.resolve(__dirname, destDir)); 27 | }); 28 | it('#test default config: ' + caseName, function (done) { 29 | execa(buildCLI, ['--config', './webpack.config.js']).then(function (res) { 30 | var files = fs.readdirSync(path.resolve(__dirname, destDir)); 31 | expect(files).to.eql(['background_sprite.png', 'bundle.js', 'test.png']); 32 | var filesContent = fs.readFileSync(path.resolve(__dirname, destDir + '/background_sprite.png')); 33 | var md5Code = utils.md5Create(filesContent); 34 | expect(md5Code).to.eql('d158e28d383a33dd07e4b50571556e5d'); 35 | var filesContent2 = fs.readFileSync(path.resolve(__dirname, destDir + '/test.png')); 36 | var md5Code2 = utils.md5Create(filesContent2); 37 | expect(md5Code2).to.eql('4812de9d8e0456fd3f178dbef18513e7'); 38 | var cssContent = fs.readFileSync(path.resolve(__dirname, destDir + '/bundle.js')).toString(); 39 | expect(replaceReg.test(cssContent)).to.eql(false); 40 | done(); 41 | }); 42 | }); 43 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-sprite-loader", 3 | "description": "A webpack loader to convert png into sprite image", 4 | "version": "0.3.4", 5 | "author": "moujintao ", 6 | "contributors": [ 7 | "Rainfore " 8 | ], 9 | "scripts": { 10 | "precommit": "node node_modules/vusion-hooks/precommit", 11 | "test": "mocha --timeout 5000 ./test/final.test.js", 12 | "test:default": "mocha --timeout 5000 ./test/default.test.js", 13 | "test:retina": "mocha --timeout 5000 ./test/retina.test.js", 14 | "eslint": "eslint ./src --fix" 15 | }, 16 | "main": "./index.js", 17 | "repository": "vusion/css-sprite-loader", 18 | "bugs": { 19 | "url": "http://github.com/vusion/css-sprite-loader/issues" 20 | }, 21 | "license": "MIT", 22 | "keywords": [ 23 | "css", 24 | "image", 25 | "png", 26 | "sprite", 27 | "webpack", 28 | "loader" 29 | ], 30 | "tags": [ 31 | "css", 32 | "image", 33 | "png", 34 | "sprite", 35 | "webpack", 36 | "loader" 37 | ], 38 | "dependencies": { 39 | "base-css-image-loader": "^0.2.7", 40 | "css-fruit": "^0.1.3", 41 | "postcss": "^6.0.23", 42 | "spritesmith": "^3.2.1" 43 | }, 44 | "devDependencies": { 45 | "chai": "^4.1.2", 46 | "css-loader": "^0.28.4", 47 | "eslint": "^5.12.0", 48 | "eslint-config-vusion": "^3.0.1", 49 | "extract-text-webpack-plugin": "^3.0.0", 50 | "file-loader": "^1.1.6", 51 | "html-webpack-plugin": "^2.30.1", 52 | "husky": "^0.14.3", 53 | "image-js": "^0.21.0", 54 | "mocha": "^3.5.3", 55 | "postcss-loader": "^3.0.0", 56 | "postcss-px-to-viewport": "^0.0.3", 57 | "postcss-viewport-units": "^0.1.4", 58 | "puppeteer": "^1.7.0", 59 | "shelljs": "^0.8.4", 60 | "style-loader": "^0.18.2", 61 | "url-loader": "^0.5.9", 62 | "vusion-hooks": "^0.2.1", 63 | "webpack": "^4.46.0", 64 | "webpack-cli": "^4.9.1", 65 | "webpack-dev-server": "^2.7.1" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /example/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | font-family: Rockwell; 9 | color: #565b61; 10 | } 11 | 12 | .source, .sprite { 13 | border: 1px dashed #dfe4ec; 14 | } 15 | 16 | .source.retina2x { 17 | width: 128px; 18 | height: 128px; 19 | background: url('../test/fixtures/images/retina/angry-birds@2x.png'); 20 | background-size: 100%; 21 | } 22 | .sprite.retina2x { 23 | width: 128px; 24 | height: 128px; 25 | background: url('../test/fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x'); 26 | } 27 | 28 | .source.retina-2 { 29 | width: 128px; 30 | height: 128px; 31 | background: url('../test/fixtures/images/retina/captain-america@2x.png') no-repeat; 32 | background-size: 80%; 33 | background-position: 30px 20px; 34 | } 35 | .sprite.retina-2 { 36 | width: 128px; 37 | height: 128px; 38 | background: url('../test/fixtures/images/retina/captain-america.png?sprite&retina') no-repeat; 39 | background-size: 80%; 40 | background-position: 30px 20px; 41 | } 42 | 43 | .source.bg-position-outside { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../test/fixtures/images/tag.png') no-repeat; 47 | background-position: 30px -20px; 48 | } 49 | .sprite.bg-position-outside { 50 | width: 128px; 51 | height: 128px; 52 | background: url('../test/fixtures/images/tag.png?sprite') no-repeat; 53 | background-position: 30px -20px; 54 | } 55 | 56 | .source.bg-position-and-size-outside { 57 | width: 200px; 58 | height: 128px; 59 | background: url('../test/fixtures/images/html.png') no-repeat; 60 | background-size: 200px 100px; 61 | background-position: 30px 20px; 62 | } 63 | .sprite.bg-position-and-size-outside { 64 | width: 200px; 65 | height: 128px; 66 | background: url('../test/fixtures/images/html.png?sprite') no-repeat; 67 | background-size: 200px 100px; 68 | background-position: 30px 20px; 69 | } 70 | -------------------------------------------------------------------------------- /test/extract-text-webpack-plugin.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const runWebpack = require('base-css-image-loader/test/fixtures/runWebpack'); 4 | const expect = require('chai').expect; 5 | const { utils } = require('base-css-image-loader'); 6 | 7 | const caseName = 'extract-text-webpack-plugin'; 8 | const replaceReg = /REPLACE_BACKGROUND\([^)]*\)/g; 9 | 10 | describe('Webpack Integration test', () => { 11 | it('#test extract-text-webpack-plugin config: ' + caseName, (done) => { 12 | runWebpack(caseName, { casesPath: path.resolve(__dirname, './cases') }, (err, data) => { 13 | if (err) 14 | return done(err); 15 | 16 | const files = fs.readdirSync(data.outputPath); 17 | expect(files).to.eql([ 18 | 'background_sprite.png', 19 | 'background_sprite@2x.png', 20 | 'bundle.js', 21 | 'index.css', 22 | 'test.png', 23 | 'test@2x.png', 24 | ]); 25 | const filesContent = fs.readFileSync(path.resolve(data.outputPath, 'background_sprite.png')); 26 | const md5Code = utils.md5Create(filesContent); 27 | expect(md5Code).to.eql('96059fa5a49046b499352e23fea86070'); 28 | const filesContent2 = fs.readFileSync(path.resolve(data.outputPath, 'test.png')); 29 | const md5Code2 = utils.md5Create(filesContent2); 30 | expect(md5Code2).to.eql('e1645b7464e7a59bbc9466b7f4f1562b'); 31 | const filesContent3 = fs.readFileSync(path.resolve(data.outputPath, 'background_sprite@2x.png')); 32 | const md5Code3 = utils.md5Create(filesContent3); 33 | expect(md5Code3).to.eql('96059fa5a49046b499352e23fea86070'); 34 | const filesContent4 = fs.readFileSync(path.resolve(data.outputPath, 'test@2x.png')); 35 | const md5Code4 = utils.md5Create(filesContent4); 36 | expect(md5Code4).to.eql('e1645b7464e7a59bbc9466b7f4f1562b'); 37 | const cssContent = fs.readFileSync(path.resolve(data.outputPath, 'index.css')).toString(); 38 | expect(replaceReg.test(cssContent)).to.eql(false); 39 | done(); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /test/cases/background/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 8 | 9 |
10 |

Source images display

11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |

Sprite display

34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /test/cases/retina/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/retina/angry-birds.png'); 14 | background-size: 100%; 15 | } 16 | .sprite.simple { 17 | width: 128px; 18 | height: 128px; 19 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 20 | background-size: 100%; 21 | } 22 | 23 | .source.retina { 24 | width: 128px; 25 | height: 128px; 26 | background: url('../../fixtures/images/retina/angry-birds@2x.png'); 27 | background-size: 100%; 28 | } 29 | .sprite.retina { 30 | width: 128px; 31 | height: 128px; 32 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina'); 33 | background-size: 100%; 34 | } 35 | 36 | .source.retina2x { 37 | width: 128px; 38 | height: 128px; 39 | background: url('../../fixtures/images/retina/angry-birds@2x.png'); 40 | background-size: 100%; 41 | } 42 | .sprite.retina2x { 43 | width: 128px; 44 | height: 128px; 45 | background: url('../../fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x'); 46 | } 47 | 48 | .source.retina4x { 49 | width: 128px; 50 | height: 128px; 51 | background: url('../../fixtures/images/retina/angry-birds@4x.png'); 52 | background-size: 100%; 53 | } 54 | .sprite.retina4x { 55 | width: 128px; 56 | height: 128px; 57 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina&retina@3x&retina@4x'); 58 | } 59 | 60 | .source.simple-2 { 61 | width: 128px; 62 | height: 128px; 63 | background: url('../../fixtures/images/retina/captain-america.png') no-repeat; 64 | background-size: 80%; 65 | } 66 | .sprite.simple-2 { 67 | width: 128px; 68 | height: 128px; 69 | background: url('../../fixtures/images/retina/captain-america.png?sprite') no-repeat; 70 | background-size: 80%; 71 | } 72 | 73 | .source.retina-2 { 74 | width: 128px; 75 | height: 128px; 76 | background: url('../../fixtures/images/retina/captain-america@2x.png') no-repeat; 77 | background-size: 80%; 78 | background-position: 30px 20px; 79 | } 80 | .sprite.retina-2 { 81 | width: 128px; 82 | height: 128px; 83 | background: url('../../fixtures/images/retina/captain-america.png?sprite&retina') no-repeat; 84 | background-size: 80%; 85 | background-position: 30px 20px; 86 | } 87 | 88 | .source.retina4x-2 { 89 | width: 128px; 90 | height: 128px; 91 | background: url('../../fixtures/images/retina/captain-america@4x.png') no-repeat; 92 | background-size: 80%; 93 | background-position: 30px 20px; 94 | } 95 | .sprite.retina4x-2 { 96 | width: 128px; 97 | height: 128px; 98 | background: url('../../fixtures/images/retina/captain-america.png?sprite&retina&retina@3x&retina@4x') no-repeat; 99 | background-size: 80%; 100 | background-position: 30px 20px; 101 | } 102 | -------------------------------------------------------------------------------- /test/cases/image-set/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/retina/angry-birds.png'); 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 19 | } 20 | 21 | .source.image-set { 22 | width: 128px; 23 | height: 128px; 24 | background: image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 25 | } 26 | .sprite.image-set { 27 | width: 128px; 28 | height: 128px; 29 | background: image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 30 | } 31 | 32 | .source.image-set-prefix { 33 | width: 128px; 34 | height: 128px; 35 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 36 | } 37 | .sprite.image-set-prefix { 38 | width: 128px; 39 | height: 128px; 40 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 41 | } 42 | 43 | .source.image-set-fallback { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/retina/angry-birds.png'); 47 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 48 | } 49 | .sprite.image-set-fallback { 50 | width: 128px; 51 | height: 128px; 52 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 53 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 54 | } 55 | 56 | .source.image-set-and-other-things { 57 | width: 100px; 58 | height: 150px; 59 | background: url('../../fixtures/images/retina/angry-birds.png'); 60 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 61 | background-size: 120%; 62 | background-position: 30px 20px; 63 | background-repeat: no-repeat; 64 | } 65 | .sprite.image-set-and-other-things { 66 | width: 100px; 67 | height: 150px; 68 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 69 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 70 | background-size: 120%; 71 | background-position: 30px 20px; 72 | } 73 | 74 | .source.image-default-resolution { 75 | width: 128px; 76 | height: 128px; 77 | background: -webkit-image-set( 78 | url('../../fixtures/images/retina/minion@3x.png') 3x, 79 | url('../../fixtures/images/retina/minion.png') 1x, 80 | url('../../fixtures/images/retina/minion@2x.png') 2x 81 | ); 82 | } 83 | .sprite.image-default-resolution { 84 | width: 128px; 85 | height: 128px; 86 | background: -webkit-image-set( 87 | url('../../fixtures/images/retina/minion@3x.png?sprite') 3x 88 | url('../../fixtures/images/retina/minion.png?sprite') 1x, 89 | url('../../fixtures/images/retina/minion@2x.png?sprite') 2x, 90 | ); 91 | } 92 | 93 | .source.image-set-different { 94 | width: 128px; 95 | height: 128px; 96 | background: url('../../fixtures/images/retina/captain-america.png'); 97 | background: -webkit-image-set( 98 | url('../../fixtures/images/retina/captain-america.png') 1x, 99 | url('../../fixtures/images/retina/captain-america@2x.png') 2x, 100 | url('../../fixtures/images/retina/captain-america@3x.png') 3x 101 | ); 102 | } 103 | .sprite.image-set-different { 104 | width: 128px; 105 | height: 128px; 106 | background: url('../../fixtures/images/retina/captain-america.png?sprite'); 107 | background: -webkit-image-set( 108 | url('../../fixtures/images/retina/captain-america.png?sprite') 1x, 109 | url('../../fixtures/images/retina/captain-america@2x.png?sprite=other-sprite') 2x, 110 | url('../../fixtures/images/retina/captain-america@3x.png') 3x 111 | ); 112 | } 113 | -------------------------------------------------------------------------------- /src/Plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { BasePlugin } = require('base-css-image-loader'); 4 | const SpriteSmith = require('spritesmith'); 5 | const postcss = require('postcss'); 6 | const computeNewBackground = require('./computeNewBackground'); 7 | const meta = require('./meta'); 8 | 9 | class CSSSpritePlugin extends BasePlugin { 10 | constructor(options) { 11 | options = options || {}; 12 | super(); 13 | this.REPLACE_AFTER_OPTIMIZE_TREE = true; 14 | Object.assign(this, meta); 15 | 16 | this.options = Object.assign(this.options, { 17 | // @inherit: output: './', 18 | // @inherit: filename: '[fontName].[ext]?[hash]', 19 | // @inherit: publicPath: undefined, 20 | padding: 40, 21 | queryParam: 'sprite', 22 | defaultName: 'sprite', 23 | filter: 'query', 24 | imageSetFallback: false, 25 | plugins: [], 26 | }, options); 27 | this.data = {}; // { [group: string]: { [md5: string]: { id: string, oldBackground: Background } } } 28 | } 29 | 30 | apply(compiler) { 31 | this.plugin(compiler, 'thisCompilation', (compilation, params) => { 32 | this.plugin(compilation, 'optimizeTree', (chunks, modules, callback) => this.optimizeTree(compilation, chunks, modules, callback)); 33 | }); 34 | super.apply(compiler); 35 | } 36 | 37 | optimizeTree(compilation, chunks, modules, callback) { 38 | const promises = Object.keys(this.data).map((groupName) => { 39 | const group = this.data[groupName]; 40 | const keys = Object.keys(group); 41 | // Make sure same cachebuster in uncertain file loaded order 42 | !this.watching && keys.sort(); 43 | const files = Array.from(new Set(keys.map((key) => group[key].filePath))); 44 | 45 | return new Promise((resolve, reject) => SpriteSmith.run({ 46 | src: files, 47 | algorithm: 'binary-tree', 48 | padding: this.options.padding, 49 | }, (err, result) => err ? reject(err) : resolve(result))) 50 | .then((result) => { 51 | const output = this.getOutput({ 52 | name: groupName, 53 | ext: 'png', 54 | content: result.image, 55 | }, compilation); 56 | 57 | compilation.assets[output.path] = { 58 | source: () => result.image, 59 | size: () => result.image.length, 60 | }; 61 | 62 | const coordinates = result.coordinates; 63 | return Promise.all(keys.map((key) => { 64 | const item = group[key]; 65 | // Add new background according to result of sprite 66 | const background = computeNewBackground( 67 | item.oldBackground, 68 | output.url, 69 | item.blockSize, 70 | coordinates[item.filePath], 71 | result.properties, 72 | +item.resolution.slice(0, -1), 73 | ); 74 | background.valid = true; 75 | const content = background.toString(); 76 | 77 | // @TODO: Should process in postcssPlugin? 78 | return postcss(this.options.plugins).process(`background: ${content};`, { 79 | from: undefined, 80 | to: undefined, 81 | }).then((result) => { 82 | item.content = result.root.nodes[0].value; 83 | }); 84 | })); 85 | }); 86 | }); 87 | 88 | return Promise.all(promises).then(() => callback()).catch((e) => callback(e)); 89 | } 90 | 91 | /** 92 | * @override 93 | * Replace Function 94 | */ 95 | REPLACER_FUNC(groupName, id) { 96 | return this.data[groupName][id].content; 97 | } 98 | 99 | /** 100 | * @override 101 | * Replace Function to escape 102 | */ 103 | REPLACER_FUNC_ESCAPED(groupName, id) { 104 | return this.data[groupName][id].content; 105 | } 106 | } 107 | 108 | module.exports = CSSSpritePlugin; 109 | -------------------------------------------------------------------------------- /test/cases/smart/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | height: 200px; 12 | background: url('../../fixtures/images/retina/angry-birds.png') no-repeat; 13 | } 14 | .sprite.simple { 15 | height: 200px; 16 | background: url('../../fixtures/images/retina/angry-birds.png?sprite') no-repeat; 17 | } 18 | 19 | .source.no-width { 20 | height: 200px; 21 | background: image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x) no-repeat; 22 | } 23 | .sprite.no-width { 24 | height: 200px; 25 | background: image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x) no-repeat; 26 | } 27 | 28 | .source.background-size { 29 | height: 200px; 30 | background: image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x) no-repeat; 31 | background-size: 50%; 32 | } 33 | .sprite.background-size { 34 | height: 200px; 35 | background: image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x) no-repeat; 36 | background-size: 50%; 37 | } 38 | 39 | /* .source.image-set-prefix { 40 | width: 128px; 41 | height: 128px; 42 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 43 | } 44 | .sprite.image-set-prefix { 45 | width: 128px; 46 | height: 128px; 47 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 48 | } 49 | 50 | .source.image-set-fallback { 51 | width: 128px; 52 | height: 128px; 53 | background: url('../../fixtures/images/retina/angry-birds.png'); 54 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 55 | } 56 | .sprite.image-set-fallback { 57 | width: 128px; 58 | height: 128px; 59 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 60 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 61 | } 62 | 63 | .source.image-set-and-other-things { 64 | width: 100px; 65 | height: 150px; 66 | background: url('../../fixtures/images/retina/angry-birds.png'); 67 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png') 1x, url('../../fixtures/images/retina/angry-birds@2x.png') 2x); 68 | background-size: 120%; 69 | background-position: 30px 20px; 70 | background-repeat: no-repeat; 71 | } 72 | .sprite.image-set-and-other-things { 73 | width: 100px; 74 | height: 150px; 75 | background: url('../../fixtures/images/retina/angry-birds.png?sprite'); 76 | background: -webkit-image-set(url('../../fixtures/images/retina/angry-birds.png?sprite') 1x, url('../../fixtures/images/retina/angry-birds@2x.png?sprite') 2x); 77 | background-size: 120%; 78 | background-position: 30px 20px; 79 | } 80 | 81 | .source.image-default-resolution { 82 | width: 128px; 83 | height: 128px; 84 | background: -webkit-image-set( 85 | url('../../fixtures/images/retina/minion@3x.png') 3x, 86 | url('../../fixtures/images/retina/minion.png') 1x, 87 | url('../../fixtures/images/retina/minion@2x.png') 2x 88 | ); 89 | } 90 | .sprite.image-default-resolution { 91 | width: 128px; 92 | height: 128px; 93 | background: -webkit-image-set( 94 | url('../../fixtures/images/retina/minion@3x.png?sprite') 3x 95 | url('../../fixtures/images/retina/minion.png?sprite') 1x, 96 | url('../../fixtures/images/retina/minion@2x.png?sprite') 2x, 97 | ); 98 | } 99 | 100 | .source.image-set-different { 101 | width: 128px; 102 | height: 128px; 103 | background: url('../../fixtures/images/retina/captain-america.png'); 104 | background: -webkit-image-set( 105 | url('../../fixtures/images/retina/captain-america.png') 1x, 106 | url('../../fixtures/images/retina/captain-america@2x.png') 2x, 107 | url('../../fixtures/images/retina/captain-america@3x.png') 3x 108 | ); 109 | } 110 | .sprite.image-set-different { 111 | width: 128px; 112 | height: 128px; 113 | background: url('../../fixtures/images/retina/captain-america.png?sprite'); 114 | background: -webkit-image-set( 115 | url('../../fixtures/images/retina/captain-america.png?sprite') 1x, 116 | url('../../fixtures/images/retina/captain-america@2x.png?sprite=other-sprite') 2x, 117 | url('../../fixtures/images/retina/captain-america@3x.png') 3x 118 | ); 119 | } */ 120 | -------------------------------------------------------------------------------- /test/cases/background/test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const webpack = require('webpack'); 3 | const webpackDevServer = require('webpack-dev-server'); 4 | const path = require('path'); 5 | const { Image } = require('image-js'); 6 | 7 | function fetchPropFrom(meta) { 8 | const m = JSON.parse(meta); 9 | return { 10 | top: m.top, 11 | height: m.height, 12 | }; 13 | } 14 | 15 | (async () => { 16 | const browser = await puppeteer.launch(); 17 | const page = await browser.newPage(); 18 | const server = await new Promise((res, rej) => { 19 | const compiler = webpack(require('./webpack.config.test.js')); 20 | const server = new webpackDevServer(compiler, { 21 | contentBase: __dirname + '/', 22 | compress: true, 23 | port: 8080, 24 | }); 25 | server.listen(8080, 'localhost', res); 26 | res(server); 27 | }); 28 | await page.goto('http://localhost:8080'); 29 | 30 | page.on('console', (msg) => { 31 | for (let i = 0; i < msg.args.length; ++i) 32 | console.log(`${i}: ${msg.args[i]}`); 33 | }); 34 | const metas = await page.evaluate(() => { 35 | const contrast = 'source'; 36 | const experimental = 'sprite'; 37 | const experiments = [ 38 | 'simple', 39 | 'bg-image', 40 | 'with-color', 41 | 'with-color-outside', 42 | 'with-color-override', 43 | 'bg-size', 44 | 'bg-size-pixel', 45 | 'bg-size-height', 46 | 'bg-size-cover', 47 | 'bg-size-contain', 48 | 'bg-size-override', 49 | 'bg-position', 50 | 'bg-position-outside', 51 | 'bg-position-override', 52 | 'bg-position-and-size', 53 | 'bg-position-and-size-outside', 54 | 'without-image-set', 55 | 'image-set', 56 | 'image-set-fallback', 57 | 'image-set-and-others']; 58 | console.log(experiments, contrast); 59 | return metas = experiments.map((exp) => { 60 | const contrastSelector = `.${contrast}.${exp}`; 61 | const experimentsSelector = `.${experimental}.${exp}`; 62 | const source = document.querySelector(contrastSelector); 63 | const sprite = document.querySelector(experimentsSelector); 64 | console.log(source, sprite); 65 | if (!source || !sprite) 66 | return false; 67 | 68 | return { 69 | name: exp, 70 | source: JSON.stringify(source.getBoundingClientRect()), 71 | sprite: JSON.stringify(sprite.getBoundingClientRect()), 72 | }; 73 | }).filter(Boolean); 74 | }); 75 | 76 | // normalize meta 77 | const experimentsList = metas.map((meta) => ({ 78 | name: meta.name, 79 | source: fetchPropFrom(meta.source), 80 | sprite: fetchPropFrom(meta.sprite), 81 | })); 82 | 83 | await page.screenshot({ 84 | path: './666.png', 85 | fullPage: true, 86 | }); 87 | 88 | const img = await Image.load('./666.png'); 89 | 90 | const grey = img.grey(); 91 | grey.save('./gray666.png'); 92 | const { width } = grey; 93 | 94 | const rslt = experimentsList.map((experiment) => { 95 | const { sprite, source, name } = experiment; 96 | console.log(sprite, source, name); 97 | if (source.height === 0 || sprite.height === 0) 98 | return { code: 999, msg: '元素未渲染', errCSSBlock: name }; 99 | if (sprite.height !== source.height || sprite.top !== source.top) 100 | return { code: 606, msg: '页面元素未对应', errCSSBlock: name }; 101 | const sourcePart = grey.crop({ x: 0, y: source.top, width: width / 2, height: source.height }); 102 | const spritePart = grey.crop({ x: width / 2, y: sprite.top, width: width / 2, height: sprite.height }); 103 | const leftPartArray = sourcePart.getPixelsArray(); 104 | const rigthPartArray = spritePart.getPixelsArray(); 105 | if (leftPartArray.length !== rigthPartArray.length) { 106 | return { 107 | code: -1, 108 | msg: '图片大小不匹配', 109 | errCSSBlock: name, 110 | }; 111 | } else { 112 | let len = leftPartArray.length - 1; 113 | while (len-- && Math.abs(leftPartArray[len][0] - rigthPartArray[len][0]) < 10) {} 114 | console.log( 115 | len, 116 | leftPartArray[len], 117 | rigthPartArray[len]); 118 | if (len === -1) { 119 | return { 120 | code: 200, 121 | msg: '校验成功', 122 | CSSBlock: name, 123 | }; 124 | } else { 125 | return { 126 | code: 996, 127 | msg: `校验失败 @ ${len}`, 128 | errCSSBlock: name, 129 | }; 130 | } 131 | } 132 | }); 133 | console.log(rslt); 134 | await new Promise((res) => { 135 | server.close(res); 136 | }); 137 | await browser.close(); 138 | })(); 139 | -------------------------------------------------------------------------------- /test/cases/image-set/test.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | const webpack = require('webpack'); 3 | const WebpackDevServer = require('webpack-dev-server'); 4 | const path = require('path'); 5 | const { Image } = require('image-js'); 6 | 7 | function fetchPropFrom(meta) { 8 | const m = JSON.parse(meta); 9 | return { 10 | top: m.top, 11 | height: m.height, 12 | }; 13 | } 14 | 15 | (async () => { 16 | const browser = await puppeteer.launch(); 17 | const page = await browser.newPage(); 18 | const server = await new Promise((res, rej) => { 19 | const compiler = webpack(require('./webpack.config.test.js')); 20 | const server = new WebpackDevServer(compiler, { 21 | contentBase: __dirname + '/', 22 | compress: true, 23 | port: 8080, 24 | }); 25 | server.listen(8080, 'localhost', res); 26 | res(server); 27 | }); 28 | await page.goto('http://localhost:8080'); 29 | 30 | page.on('console', (msg) => { 31 | for (let i = 0; i < msg.args.length; ++i) 32 | console.log(`${i}: ${msg.args[i]}`); 33 | }); 34 | const metas = await page.evaluate(() => { 35 | const contrast = 'source'; 36 | const experimental = 'sprite'; 37 | const experiments = [ 38 | 'simple', 39 | 'bg-image', 40 | 'with-color', 41 | 'with-color-outside', 42 | 'with-color-override', 43 | 'bg-size', 44 | 'bg-size-pixel', 45 | 'bg-size-height', 46 | 'bg-size-cover', 47 | 'bg-size-contain', 48 | 'bg-size-override', 49 | 'bg-position', 50 | 'bg-position-outside', 51 | 'bg-position-override', 52 | 'bg-position-and-size', 53 | 'bg-position-and-size-outside', 54 | 'without-image-set', 55 | 'image-set', 56 | 'image-set-fallback', 57 | 'image-set-and-others']; 58 | console.log(experiments, contrast); 59 | const metas = experiments.map((exp) => { 60 | const contrastSelector = `.${contrast}.${exp}`; 61 | const experimentsSelector = `.${experimental}.${exp}`; 62 | /* eslint-disable no-undef */ 63 | const source = document.querySelector(contrastSelector); 64 | const sprite = document.querySelector(experimentsSelector); 65 | console.log(source, sprite); 66 | if (!source || !sprite) 67 | return false; 68 | 69 | return { 70 | name: exp, 71 | source: JSON.stringify(source.getBoundingClientRect()), 72 | sprite: JSON.stringify(sprite.getBoundingClientRect()), 73 | }; 74 | }).filter(Boolean); 75 | return metas; 76 | }); 77 | 78 | // normalize meta 79 | const experimentsList = metas.map((meta) => ({ 80 | name: meta.name, 81 | source: fetchPropFrom(meta.source), 82 | sprite: fetchPropFrom(meta.sprite), 83 | })); 84 | 85 | await page.screenshot({ 86 | path: './666.png', 87 | fullPage: true, 88 | }); 89 | 90 | const img = await Image.load('./666.png'); 91 | 92 | const grey = img.grey(); 93 | grey.save('./gray666.png'); 94 | const { width } = grey; 95 | 96 | const rslt = experimentsList.map((experiment) => { 97 | const { sprite, source, name } = experiment; 98 | console.log(sprite, source, name); 99 | if (source.height === 0 || sprite.height === 0) 100 | return { code: 999, msg: '元素未渲染', errCSSBlock: name }; 101 | if (sprite.height !== source.height || sprite.top !== source.top) 102 | return { code: 606, msg: '页面元素未对应', errCSSBlock: name }; 103 | const sourcePart = grey.crop({ x: 0, y: source.top, width: width / 2, height: source.height }); 104 | const spritePart = grey.crop({ x: width / 2, y: sprite.top, width: width / 2, height: sprite.height }); 105 | const leftPartArray = sourcePart.getPixelsArray(); 106 | const rigthPartArray = spritePart.getPixelsArray(); 107 | if (leftPartArray.length !== rigthPartArray.length) { 108 | return { 109 | code: -1, 110 | msg: '图片大小不匹配', 111 | errCSSBlock: name, 112 | }; 113 | } else { 114 | let len = leftPartArray.length - 1; 115 | while (len-- && Math.abs(leftPartArray[len][0] - rigthPartArray[len][0]) < 10) {} 116 | console.log( 117 | len, 118 | leftPartArray[len], 119 | rigthPartArray[len]); 120 | if (len === -1) { 121 | return { 122 | code: 200, 123 | msg: '校验成功', 124 | CSSBlock: name, 125 | }; 126 | } else { 127 | return { 128 | code: 996, 129 | msg: `校验失败 @ ${len}`, 130 | errCSSBlock: name, 131 | }; 132 | } 133 | } 134 | }); 135 | console.log(rslt); 136 | await new Promise((res) => { 137 | server.close(res); 138 | }); 139 | await browser.close(); 140 | })(); 141 | -------------------------------------------------------------------------------- /src/computeNewBackground.js: -------------------------------------------------------------------------------- 1 | const { Background, BackgroundPosition, BackgroundSize, Percentage, Length } = require('css-fruit'); 2 | 3 | function checkBlockSize(blockSize) { 4 | if (!blockSize.valid) 5 | return false; 6 | if (!blockSize.width || !blockSize.height) 7 | return false; 8 | return (blockSize.width.toString() === '0' || blockSize.width.unit === 'px') 9 | && (blockSize.height.toString() === '0' || blockSize.height.unit === 'px'); 10 | } 11 | 12 | function checkBackgroundPosition(position) { 13 | // top right center ... 14 | if (position.x.offset._type === 'length' && position.y.offset._type === 'length') 15 | return true; 16 | if ((position.x.offset.unit === 'px' || position.x.offset.toString() === '0') 17 | && (position.y.offset.unit === 'px' || position.y.offset.toString() === '0')) 18 | return true; 19 | throw new TypeError(`Only support px-unit in 'background-position'`); 20 | } 21 | 22 | /** 23 | * 根据原有块中的背景属性值、块的大小、原有图片本身大小、生成雪碧图的大小、分辨率要求,计算出新背景的各种属性值 24 | * 这个函数很复杂,写哭了。。😭 25 | * @param {Background} oldBackground 26 | * @param {no units} oldBlockSize 27 | * @param {no units} imageDimension 28 | * @param {no units} spriteSize 29 | * @param {number} dppx 30 | */ 31 | module.exports = function computeNewBackground(oldBackground, url, oldBlockSize, imageDimension, spriteSize, dppx) { 32 | const background = new Background(); 33 | background.color = oldBackground.color; 34 | background.repeat = 'no-repeat'; 35 | background.image = `url('${url.replace(/'/g, "\\'")}')`; 36 | // background.clip 37 | // background.origin 38 | // background.attachment 39 | background.valid = true; 40 | 41 | /** 42 | * background-position 43 | * 检查原有的 background-position,没有的话按'0px 0px'计算 44 | * 必须用像素值,否则报错 45 | */ 46 | if (oldBackground.position === undefined) 47 | oldBackground.position = new BackgroundPosition('0px 0px'); 48 | else 49 | checkBackgroundPosition(oldBackground.position); 50 | background.position = new BackgroundPosition(`0px 0px`); 51 | background.position.x.offset.number = oldBackground.position.x.offset.number - imageDimension.x; 52 | background.position.y.offset.number = oldBackground.position.y.offset.number - imageDimension.y; 53 | 54 | /** 55 | * background-size 56 | * 检查原有的 background-size,没有的话按图片本身大小/分辨率来计算 57 | */ 58 | let oldSize = oldBackground.size; 59 | if (String(oldSize) === 'auto') 60 | oldSize = undefined; 61 | if (!oldSize && dppx !== 1) 62 | oldSize = new BackgroundSize(imageDimension.width / dppx + 'px' + ' ' + imageDimension.height / dppx + 'px'); 63 | 64 | /** 65 | * blockSize 66 | * 检查原有块的大小,没有或不明确的按图片本身大小/分辨率来计算(这是猜测,可能有偏差) 67 | */ 68 | let blockSize = new BackgroundSize(oldBlockSize.width + ' ' + oldBlockSize.height); 69 | if (!checkBlockSize(blockSize)) 70 | blockSize = new BackgroundSize(imageDimension.width / dppx + 'px' + ' ' + imageDimension.height / dppx + 'px'); 71 | 72 | if (oldSize) { // Don't process 'auto' 73 | const spriteRadio = { 74 | x: 1, 75 | y: 1, 76 | }; 77 | 78 | const blockWHRatio = blockSize.width.number / blockSize.height.number; 79 | const imageWHRatio = imageDimension.width / imageDimension.height; 80 | 81 | /** 82 | * cover or contain amounts to ... 83 | * 84 | * | | bWHRatio >= iWHRatio | bWHRatio < iWHRatio | 85 | * | ------- | -------------------- | ------------------- | 86 | * | cover | 100% auto | auto 100% | 87 | * | contain | auto 100% | 100% auto | 88 | */ 89 | 90 | if ((oldSize.toString() === 'cover' && blockWHRatio >= imageWHRatio) 91 | || (oldSize.toString() === 'contain' && blockWHRatio < imageWHRatio)) { 92 | oldSize = new BackgroundSize('100% auto'); 93 | } 94 | if ((oldSize.toString() === 'cover' && blockWHRatio < imageWHRatio) 95 | || (oldSize.toString() === 'contain' && blockWHRatio >= imageWHRatio)) { 96 | oldSize = new BackgroundSize('auto 100%'); 97 | } 98 | 99 | // Handle case of width 100 | if (oldSize.width._type === 'percentage') { 101 | spriteRadio.x = blockSize.width.number * oldSize.width.number * 0.01 / imageDimension.width; 102 | } else if (oldSize.width._type === 'length') { 103 | if (oldSize.width.unit !== 'px') 104 | throw new Error(`Only support px-unit or percentage in 'background-size'`); 105 | spriteRadio.x = oldSize.width.number / imageDimension.width; 106 | } 107 | 108 | // Handle case of height 109 | if (oldSize.height._type === 'percentage') { 110 | spriteRadio.y = blockSize.height.number * oldSize.height.number * 0.01 / imageDimension.height; 111 | } else if (oldSize.height._type === 'length') { 112 | if (oldSize.height.unit !== 'px') 113 | throw new Error(`Only support px-unit or percentage in 'background-size'`); 114 | spriteRadio.y = oldSize.height.number / imageDimension.height; 115 | } 116 | 117 | // Handle case of auto 118 | if (oldSize.width === 'auto') 119 | spriteRadio.x = spriteRadio.y; 120 | else if (oldSize.height === 'auto') 121 | spriteRadio.y = spriteRadio.x; 122 | 123 | background.size = new BackgroundSize( 124 | (spriteSize.width * spriteRadio.x).toFixed(0) + 'px', 125 | (spriteSize.height * spriteRadio.y).toFixed(0) + 'px', 126 | ); 127 | 128 | background.position.x.offset.number = oldBackground.position.x.offset.number - (imageDimension.x * spriteRadio.x).toFixed(0); 129 | background.position.y.offset.number = oldBackground.position.y.offset.number - (imageDimension.y * spriteRadio.y).toFixed(0); 130 | } 131 | 132 | return background; 133 | }; 134 | -------------------------------------------------------------------------------- /test/cases/background/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | 5 | .part { 6 | display: inline-block; 7 | width: 50%; 8 | } 9 | 10 | .source.simple { 11 | width: 128px; 12 | height: 128px; 13 | background: url('../../fixtures/images/home.png') no-repeat; 14 | } 15 | .sprite.simple { 16 | width: 128px; 17 | height: 128px; 18 | background: url('../../fixtures/images/home.png?sprite') no-repeat; 19 | } 20 | 21 | .source.background-image { 22 | width: 128px; 23 | height: 128px; 24 | background-image: url('../../fixtures/images/home.png'); 25 | } 26 | .sprite.background-image { 27 | width: 128px; 28 | height: 128px; 29 | background-image: url('../../fixtures/images/home.png?sprite'); 30 | } 31 | 32 | .source.with-color { 33 | width: 128px; 34 | height: 128px; 35 | background: #eee url('../../fixtures/images/home.png'); 36 | } 37 | .sprite.with-color { 38 | width: 128px; 39 | height: 128px; 40 | background: #eee url('../../fixtures/images/home.png?sprite'); 41 | } 42 | 43 | .source.with-color-outside { 44 | width: 128px; 45 | height: 128px; 46 | background: url('../../fixtures/images/home.png'); 47 | background-color: #eee; 48 | } 49 | .sprite.with-color-outside { 50 | width: 128px; 51 | height: 128px; 52 | background: url('../../fixtures/images/home.png?sprite'); 53 | background-color: #eee; 54 | } 55 | 56 | .source.with-color-override { 57 | width: 128px; 58 | height: 128px; 59 | background-color: #eee; 60 | background: url('../../fixtures/images/home.png'); 61 | } 62 | .sprite.with-color-override { 63 | width: 128px; 64 | height: 128px; 65 | background-color: #eee; 66 | background: url('../../fixtures/images/home.png?sprite'); 67 | } 68 | 69 | .source.bg-size { 70 | width: 100px; 71 | height: 100px; 72 | background-image: url('../../fixtures/images/lollipop.png'); 73 | background-repeat: no-repeat; 74 | background-size: 100%; 75 | } 76 | .sprite.bg-size { 77 | width: 100px; 78 | height: 100px; 79 | background-image: url('../../fixtures/images/lollipop.png?sprite'); 80 | background-repeat: no-repeat; 81 | background-size: 100%; 82 | } 83 | 84 | .source.bg-size-pixel { 85 | width: 100px; 86 | height: 100px; 87 | background-image: url('../../fixtures/images/lollipop.png'); 88 | background-repeat: no-repeat; 89 | background-size: 80px; 90 | } 91 | .sprite.bg-size-pixel { 92 | width: 100px; 93 | height: 100px; 94 | background-image: url('../../fixtures/images/lollipop.png?sprite'); 95 | background-repeat: no-repeat; 96 | background-size: 80px; 97 | } 98 | 99 | .source.bg-size-height { 100 | width: 100px; 101 | height: 150px; 102 | background-image: url('../../fixtures/images/lollipop.png'); 103 | background-repeat: no-repeat; 104 | background-size: auto 120px; 105 | } 106 | .sprite.bg-size-height { 107 | width: 100px; 108 | height: 150px; 109 | background-image: url('../../fixtures/images/lollipop.png?sprite'); 110 | background-repeat: no-repeat; 111 | background-size: auto 120px; 112 | } 113 | 114 | .source.bg-size-cover { 115 | width: 100px; 116 | height: 150px; 117 | background-image: url('../../fixtures/images/lollipop.png'); 118 | background-repeat: no-repeat; 119 | background-size: cover; 120 | } 121 | .sprite.bg-size-cover { 122 | width: 100px; 123 | height: 150px; 124 | background-image: url('../../fixtures/images/lollipop.png?sprite'); 125 | background-repeat: no-repeat; 126 | background-size: cover; 127 | } 128 | 129 | .source.bg-size-contain { 130 | width: 100px; 131 | height: 100px; 132 | background-image: url('../../fixtures/images/lollipop.png'); 133 | background-repeat: no-repeat; 134 | background-size: contain; 135 | } 136 | .sprite.bg-size-contain { 137 | width: 100px; 138 | height: 100px; 139 | background-image: url('../../fixtures/images/lollipop.png?sprite'); 140 | background-repeat: no-repeat; 141 | background-size: contain; 142 | } 143 | 144 | .source.bg-size-override { 145 | width: 100px; 146 | height: 150px; 147 | background-size: 100%; 148 | background: url('../../fixtures/images/lollipop.png') no-repeat; 149 | } 150 | .sprite.bg-size-override { 151 | width: 100px; 152 | height: 150px; 153 | background-size: 100%; 154 | background: url('../../fixtures/images/lollipop.png?sprite') no-repeat; 155 | } 156 | 157 | .source.bg-position { 158 | width: 128px; 159 | height: 128px; 160 | background: url('../../fixtures/images/tag.png') 30px -20px no-repeat; 161 | } 162 | .sprite.bg-position { 163 | width: 128px; 164 | height: 128px; 165 | background: url('../../fixtures/images/tag.png?sprite') 30px -20px no-repeat; 166 | } 167 | 168 | .source.bg-position-outside { 169 | width: 128px; 170 | height: 128px; 171 | background: url('../../fixtures/images/tag.png') no-repeat; 172 | background-position: 30px -20px; 173 | } 174 | .sprite.bg-position-outside { 175 | width: 128px; 176 | height: 128px; 177 | background: url('../../fixtures/images/tag.png?sprite') no-repeat; 178 | background-position: 30px -20px; 179 | } 180 | 181 | .source.bg-position-override { 182 | width: 128px; 183 | height: 128px; 184 | background-position: 30px -20px; 185 | background: url('../../fixtures/images/tag.png') no-repeat; 186 | } 187 | .sprite.bg-position-override { 188 | width: 128px; 189 | height: 128px; 190 | background-position: 30px -20px; 191 | background: url('../../fixtures/images/tag.png?sprite') no-repeat; 192 | } 193 | 194 | .source.bg-position-and-size { 195 | width: 100px; 196 | height: 150px; 197 | background: url('../../fixtures/images/html.png') 30px 20px no-repeat; 198 | background-size: 100%; 199 | } 200 | .sprite.bg-position-and-size { 201 | width: 100px; 202 | height: 150px; 203 | background: url('../../fixtures/images/html.png?sprite') 30px 20px no-repeat; 204 | background-size: 100%; 205 | } 206 | 207 | .source.bg-position-and-size-outside { 208 | width: 200px; 209 | height: 200px; 210 | background: url('../../fixtures/images/html.png') no-repeat; 211 | background-size: 200px 100px; 212 | background-position: 30px 20px; 213 | } 214 | .sprite.bg-position-and-size-outside { 215 | width: 200px; 216 | height: 200px; 217 | background: url('../../fixtures/images/html.png?sprite') no-repeat; 218 | background-size: 200px 100px; 219 | background-position: 30px 20px; 220 | } 221 | -------------------------------------------------------------------------------- /test/cases/image-set/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CSS Sprite Loader 6 | 7 | 90 | 91 | 92 |
93 |

Source images display

94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |

Sprite display

117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | 140 | 141 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # css-sprite-loader 2 | 3 | - [README in English](README.md) 4 | 5 | 这是一款可以自动将 PNG 图片合并成雪碧图的 Webpack loader。 6 | 7 | ![screenshot](./example/screenshot.png) 8 | 9 | ## 示例 10 | 11 | 给需要合并的背景图添加 sprite 后缀参数: 12 | 13 | ``` css 14 | .foo { 15 | background: url('../images/gift.png?sprite'); 16 | } 17 | ``` 18 | 19 | css-sprite-loader 会自动生成雪碧图: 20 | 21 | ``` css 22 | .foo { 23 | background: url(dest/sprite.png?5d40e339682970eb14baf6110a83ddde) -100px 0 no-repeat; 24 | } 25 | ``` 26 | 27 | ## 特性 28 | 29 | 与别的类似的雪碧图加载器不同的是: 30 | 31 | - 通过路径参数可以很轻松地决定是否使用雪碧图。 32 | - 全面支持 CSS 的`background`属性,包括`background-position`、`background-size`等。保证处理前后效果一致。例如: 33 | 34 | ``` css 35 | .bg-position-and-size { 36 | width: 100px; 37 | height: 150px; 38 | background: url('../images/html.png?sprite') 30px 20px no-repeat; 39 | background-size: 100%; 40 | } 41 | ``` 42 | 43 | 会重新自动计算位置和大小,转换成 44 | 45 | ``` css 46 | .bg-position-and-size { 47 | width: 100px; 48 | height: 150px; 49 | background: url('dest/sprite.png?dc5323f7f35c65a3d6c7f253dcc07bad') -101.25px -111.25px / 231px 231px no-repeat; 50 | } 51 | ``` 52 | 53 | > **注意**: 54 | > - 使用`background-position`,必须用像素值,且位置为左上; 55 | > - 使用`background-size`时,推荐在同一个块中指定像素值的`width`和`height`属性,loader 会按这两个值计算。否则会以图片本身的大小计算(这是种猜测,可能会与原始情况有偏差) 56 | 57 | - 提供 retina 和 image-set 两种方式,用于解决高分辨率图片的问题,参见下文[retina](#retina2x-retina3x-retina4x-) 和 [image-set](#image-set)。 58 | 59 | ## 安装 60 | 61 | ``` shell 62 | npm install --save-dev css-sprite-loader 63 | ``` 64 | 65 | ## 配置 66 | 67 | 除了在 Webpack 配置中添加 loader,还需要添加 Plugin。 68 | 69 | ``` javascript 70 | const CSSSpritePlugin = require('css-sprite-loader').Plugin; 71 | 72 | module.exports = { 73 | ... 74 | module: { 75 | rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'css-sprite-loader'] }], 76 | }, 77 | plugins: [new CSSSpritePlugin()], 78 | }; 79 | ``` 80 | 81 | ### background 属性的选项 82 | 83 | #### sprite 参数 84 | 85 | 是否将当前的图片打入雪碧图,或指定打入哪个雪碧图。例如: 86 | 87 | ``` css 88 | .foo { 89 | background: url('../images/gift.png?sprite'); 90 | } 91 | 92 | .bar { 93 | background: url('../images/light.png?sprite=sprite-nav'); 94 | } 95 | ``` 96 | 97 | 以上图片将会被打入两个雪碧图中: 98 | 99 | ``` css 100 | .foo { 101 | background: url('dest/sprite.png?fee16babb11468e0724c07bd3cf2f4cf'); 102 | } 103 | 104 | .bar { 105 | background: url('dest/sprite-nav.png?56d33b3ab0389c5b349cec93380b7ceb'); 106 | } 107 | ``` 108 | 109 | #### retina@2x, retina@3x, retina@4x, ... 110 | 111 | 是否支持某种分辨率的 retina 图片。比如你的 images 目录中有以下文件: 112 | 113 | ``` 114 | images/ 115 | angry-birds.png 116 | angry-birds@2x.png 117 | angry-birds@4x.png 118 | ``` 119 | 120 | 那么你的 CSS 可以写成如下格式: 121 | 122 | ``` css 123 | .baz { 124 | width: 128px; 125 | height: 128px; 126 | background: url('../images/retina/angry-birds.png?sprite&retina@2x&retina@4x'); 127 | background-size: 100%; 128 | } 129 | ``` 130 | 131 | 会转换为 132 | 133 | ``` css 134 | .baz { 135 | width: 128px; 136 | height: 128px; 137 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') -296px 0 / 424px 424px no-repeat; 138 | } 139 | 140 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 141 | .baz { 142 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat; 143 | } 144 | } 145 | 146 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 147 | .baz { 148 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat; 149 | } 150 | } 151 | ``` 152 | 153 | 你也可以将@2x作为默认分辨率: 154 | 155 | ``` 156 | images/ 157 | angry-birds@1x.png 158 | angry-birds@2x.png 159 | angry-birds@4x.png 160 | ``` 161 | 162 | ``` css 163 | .baz { 164 | width: 128px; 165 | height: 128px; 166 | background: url('../images/retina/angry-birds@2x.png?sprite&retina@1x&retina@4x'); 167 | background-size: 100%; 168 | } 169 | ``` 170 | 171 | 会转换为 172 | 173 | ``` css 174 | .baz { 175 | width: 128px; 176 | height: 128px; 177 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat; 178 | } 179 | 180 | @media (-webkit-max-device-pixel-ratio: 1), (max-resolution: 1dppx) { 181 | .baz { 182 | background: url('dest/sprite@1x.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 128px 128px no-repeat; 183 | } 184 | } 185 | 186 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 187 | .baz { 188 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat; 189 | } 190 | } 191 | ``` 192 | 193 | > **注意**: 194 | > 这里的`retina@1x`对应的原始图片路径要显式命名为`xxx@1x`,最后会被打入到`sprite@1x`当中。 195 | 196 | #### image-set function 197 | 198 | 也可以用 image-set 函数来设置不同分辨率的 retina 图片。 199 | 200 | image-set 函数这一特性目前处于[Stage 2](https://www.w3.org/TR/css-images-4/#image-set-notation),[浏览器的兼容情况](https://developer.mozilla.org/en-US/docs/Web/CSS/image-set#Browser_compatibility)在这里,但我们用 PostCSS 做了支持。 201 | 202 | > **注意**: 203 | > 要使用这个特性,在 css-sprite-loader 之前不能提前将 image-set 做降级处理。例如之前有`postcss-preset-env`,可以将它的选项设置为: 204 | > ``` js 205 | > { 206 | > features: { 207 | > 'image-set-function': false, 208 | > }, 209 | > } 210 | > ``` 211 | 212 | 比如你的 images 目录中有以下文件: 213 | 214 | ``` 215 | images/ 216 | angry-birds.png 217 | angry-birds@2x.png 218 | angry-birds@4x.png 219 | ``` 220 | 221 | 那么你的 CSS 可以写成如下格式: 222 | 223 | ``` css 224 | .baz { 225 | width: 128px; 226 | height: 128px; 227 | background: image-set('../images/retina/angry-birds.png?sprite' 1x, '../images/retina/angry-birds@2x.png?sprite' 2x); 228 | background-size: 100%; 229 | } 230 | ``` 231 | 232 | 会转换为 233 | 234 | ``` css 235 | .baz { 236 | width: 128px; 237 | height: 128px; 238 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat; 239 | } 240 | 241 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 242 | .baz { 243 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat; 244 | } 245 | } 246 | ``` 247 | 248 | 如果有需要,也可以指定高分辨率图不打包、或指定在不同的分组。 249 | 250 | ``` css 251 | .baz { 252 | width: 128px; 253 | height: 128px; 254 | background: image-set( 255 | '../images/retina/angry-birds.png?sprite' 1x, 256 | '../images/retina/angry-birds@2x.png?sprite-nav' 2x, 257 | '../images/retina/angry-birds@4x.png' 4x, 258 | ); 259 | background-size: 100%; 260 | } 261 | ``` 262 | 263 | 会转换为 264 | 265 | ``` css 266 | .baz { 267 | width: 128px; 268 | height: 128px; 269 | background: url('dest/sprite.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 212px 212px no-repeat; 270 | } 271 | 272 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 273 | .baz { 274 | background: url('dest/sprite-nav@2x.png?369108fb0a164b04ee10def7ed6d4226') -148px 0 / 276px 128px no-repeat; 275 | } 276 | } 277 | 278 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 279 | .baz { 280 | background: url('dest/angry-birds@4x?4a6a7dbace7933efe321b357d4db2fb9') no-repeat; 281 | } 282 | } 283 | ``` 284 | 285 | ### loader 参数 286 | 287 | 暂无。 288 | 289 | ### plugin 参数 290 | 291 | #### defaultName 292 | 293 | 默认雪碧图分组名 294 | 295 | - Type: `string` 296 | - Default: `sprite` 297 | 298 | #### filename 299 | 300 | 用于设置生成文件名的模板,类似于 Webpack 的 output.filename。模板支持以下占位符: 301 | 302 | - `[ext]` 生成资源文件后缀 303 | - `[name]` 分组名 304 | - `[hash]` 生成文件中 svg 文件的 hash 值(默认使用16进制 md5 hash,所有文件使用 svg 的 hash,其他文件的 hash 有时会发生改变) 305 | - `[:hash::]` 生成 hash 的样式 306 | - `hashType` hash 类型,比如:`sha1`, `md5`, `sha256`, `sha512` 307 | - `digestType` 数字进制:`hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64` 308 | - `length` 字符长度 309 | 310 | 311 | - Type: `string` 312 | - Default: `'[name].[ext]?[hash]'` 313 | 314 | #### output 315 | 316 | 生成的图片文件相对于 webpack 的 output 的相对路径。**必须是一个相对路径。** 317 | 318 | - Type: `string` 319 | - Default: `'./'` 320 | 321 | #### publicPath 322 | 323 | 图片在 CSS url 中的路径,与 Webpack 的 publicPath 相同,此选项用于覆盖它。 324 | 325 | - Type: `string` 326 | - Default: `''` 327 | 328 | #### padding 329 | 330 | 雪碧图中小图片之间的间距 331 | 332 | - Type: `number` 333 | - Default: `'sprite'` 334 | 335 | #### filter 336 | 337 | 如何筛选参与合并雪碧图的小图片文件,可选值:`'all'`、`'query'`、`RegExp` 338 | 339 | - `'all'`: 所有被引用的小图片都要被合并 340 | - `'query'`: 只有在路径中添加了`?sprite`后缀参数的小图片才会被合并 341 | - `RegExp`: 根据正则表达式来匹配路径 342 | 343 | - Type: `string` 344 | - Default: `'query'` 345 | 346 | #### queryParam 347 | 348 | 自定义路径中的后缀参数 key,当`filter: 'query'`才生效。 349 | 350 | - Type: `string` 351 | - Default: `'sprite'` 352 | 353 | #### imageSetFallback 354 | 355 | 是否对不需要走雪碧图流程的`image-set`也做降级处理。因为部分浏览器已经支持`-webkit-image-set`,可能不需要做降级处理。 356 | 357 | - Type: `boolean` 358 | - Default: `false` 359 | 360 | 361 | #### plugins 362 | 363 | 处理完雪碧图之后,运行 postcss 的插件列表。这些插件不会处理整个文件,只会处理与雪碧图相同的几行代码。比如使用一些单位转换的插件`require('postcss-px-to-viewport')`。 364 | 365 | - Type: `Array` 366 | - Default: `[]` 367 | 368 | ## 修改日志 369 | 370 | 参见[Releases](https://github.com/vusion/css-sprite-loader/releases) 371 | 372 | ## 贡献指南 373 | 374 | 参见[Contributing Guide](https://github.com/vusion/DOCUMENTATION/issues/4) 375 | 376 | ## 开源协议 377 | 378 | [MIT](LICENSE) 379 | 380 | -------------------------------------------------------------------------------- /src/postcssPlugin.js: -------------------------------------------------------------------------------- 1 | const postcss = require('postcss'); 2 | const { utils } = require('base-css-image-loader'); 3 | const meta = require('./meta'); 4 | const { default: CSSFruit, Background, URL } = require('css-fruit'); 5 | 6 | CSSFruit.config({ 7 | forceParsing: { 8 | url: true, 9 | 'image-set': true, 10 | length: true, 11 | percentage: true, 12 | }, 13 | }); 14 | 15 | function genMediaQuery(resolution, defaultResolution, selector, content) { 16 | const dppx = resolution.slice(0, -1); 17 | 18 | if (resolution > defaultResolution) { 19 | return ` 20 | @media (-webkit-min-device-pixel-ratio: ${dppx}), (min-resolution: ${dppx}dppx) { 21 | ${selector} { 22 | ${content} 23 | } 24 | } 25 | `; 26 | } else if (resolution < defaultResolution) { 27 | return ` 28 | @media (-webkit-max-device-pixel-ratio: ${dppx}), (max-resolution: ${dppx}dppx) { 29 | ${selector} { 30 | ${content} 31 | } 32 | } 33 | `; 34 | } 35 | } 36 | 37 | module.exports = postcss.plugin('css-sprite-parser', ({ loaderContext }) => (styles, result) => { 38 | const promises = []; 39 | const plugin = loaderContext[meta.PLUGIN_NAME]; 40 | const options = plugin.options; 41 | const data = plugin.data; 42 | 43 | let imageSetFallback = options.imageSetFallback; 44 | if (imageSetFallback === true) 45 | imageSetFallback = { preserve: true }; 46 | 47 | styles.walkRules((rule) => { 48 | const decls = rule.nodes.filter((node) => node.type === 'decl' && node.prop.startsWith('background')); 49 | if (!decls.length) 50 | return; 51 | 52 | /** 53 | * Core variable 0 54 | */ 55 | const oldBackground = CSSFruit.absorb(decls); 56 | if (!oldBackground.valid) { 57 | rule.warn(result, 'Invalid background'); 58 | return; 59 | } 60 | 61 | if (!oldBackground.image) 62 | return; 63 | 64 | // For browsers 65 | if (oldBackground.image._type === 'image-set') 66 | oldBackground.image.prefix = '-webkit-'; 67 | const oldBackgroundString = oldBackground.toString(); 68 | 69 | /** 70 | * Core variable 1 71 | */ 72 | const ruleItem = { 73 | id: 'ID' + utils.genMD5(oldBackgroundString), 74 | defaultResolution: '1x', 75 | }; 76 | 77 | /** 78 | * Core variable 2 79 | */ 80 | const imageSet = []; 81 | let oldResolutions; 82 | if (oldBackground.image._type === 'url') { 83 | imageSet.push({ 84 | url: oldBackground.image, 85 | src: undefined, 86 | resolution: undefined, 87 | needSprite: false, 88 | groupName: undefined, 89 | }); 90 | } else if (oldBackground.image._type === 'image-set') { 91 | oldResolutions = Object.keys(oldBackground.image.values); 92 | oldResolutions.forEach((resolution) => { 93 | imageSet.push({ 94 | url: oldBackground.image.values[resolution], 95 | src: undefined, 96 | resolution, 97 | needSprite: false, 98 | groupName: undefined, 99 | }); 100 | }); 101 | } else 102 | return; // Other type like linear-gradient 103 | 104 | // Check whether need sprite 105 | const checkWhetherNeedSprite = (url) => { 106 | if (!url.path.endsWith('.png')) 107 | return false; 108 | if (options.filter === 'query') 109 | return !!(url.query && url.query[options.queryParam]); 110 | else if (options.filter instanceof RegExp) 111 | return options.filter.test(url.path); 112 | else if (options.filter === 'all') 113 | return true; 114 | else 115 | throw new TypeError(`Unknow filter value '${options.filter}'`); 116 | }; 117 | 118 | let someNeedSprite = false; 119 | imageSet.forEach((image) => { 120 | image.needSprite = checkWhetherNeedSprite(image.url); 121 | if (image.needSprite) 122 | someNeedSprite = image.needSprite; 123 | }); 124 | 125 | if (!someNeedSprite && !(oldBackground.image._type === 'image-set' && imageSetFallback)) 126 | return; 127 | 128 | // Fill image object, add retina image in imageSet 129 | if (oldBackground.image._type === 'url') { 130 | const image = imageSet[0]; 131 | const query = image.url.query; 132 | const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName; 133 | image.src = image.url.path; 134 | 135 | // According to query retina, collect image set 136 | const pathRE = /(^.*?)(?:@(\d+x))?\.png$/; 137 | const paramRE = /^retina@?(\d+x)$/; 138 | const found = image.url.path.match(pathRE); 139 | if (!found) 140 | throw new Error('Error format of filePath'); 141 | let [, basePath, defaultResolution] = found; 142 | if (!defaultResolution) 143 | defaultResolution = '1x'; 144 | // 路径本身指示默认分辨率 145 | image.groupName = baseGroupName; 146 | image.resolution = ruleItem.defaultResolution = defaultResolution; 147 | 148 | Object.keys(query).forEach((param) => { 149 | // @compat: old version 150 | if (param === 'retina') 151 | param = 'retina@2x'; 152 | 153 | const found = param.match(paramRE); 154 | if (!found) 155 | return; 156 | 157 | const resolution = found[1]; 158 | const url = new URL(image.url.toString()); 159 | url.path = `${basePath}@${resolution}.png`; 160 | imageSet.push({ 161 | url, 162 | src: url.path, 163 | resolution, 164 | needSprite: image.needSprite, 165 | groupName: `${baseGroupName}@${resolution}`, 166 | }); 167 | }); 168 | } else if (oldBackground.image._type === 'image-set') { 169 | imageSet.forEach((image, index) => { 170 | const query = image.url.query; 171 | const baseGroupName = query && typeof query[options.queryParam] === 'string' ? query[options.queryParam] : options.defaultName; 172 | image.src = image.url.path; 173 | image.groupName = `${baseGroupName}@${image.resolution}`; 174 | 175 | if (index === 0) { 176 | // 第一项指示默认分辨率 177 | image.groupName = baseGroupName; 178 | ruleItem.defaultResolution = image.resolution; 179 | } 180 | }); 181 | } 182 | 183 | /** 184 | * Core variable 3 185 | */ 186 | const blockSize = { 187 | width: undefined, 188 | height: undefined, 189 | }; 190 | // Check width & height 191 | rule.walkDecls((decl) => { 192 | if (decl.prop === 'width') 193 | blockSize.width = decl.value; 194 | else if (decl.prop === 'height') 195 | blockSize.height = decl.value; 196 | }); 197 | 198 | promises.push(Promise.all(imageSet.map((image) => new Promise((resolve, reject) => { 199 | loaderContext.resolve(loaderContext.context, image.src, (err, result) => err ? reject(err) : resolve(result)); 200 | }))).then((filePaths) => { 201 | // Clean decls in source 202 | decls.forEach((decl) => decl.remove()); 203 | 204 | const outputs = []; 205 | filePaths.forEach((filePath, index) => { 206 | if (!filePath) 207 | throw new Error(`Cannot resolve file path '${imageSet[index].src}'`); 208 | loaderContext.addDependency(filePath); 209 | 210 | const image = imageSet[index]; 211 | const groupItem = { 212 | id: ruleItem.id, 213 | groupName: image.groupName, 214 | filePath, 215 | oldBackground, 216 | blockSize, 217 | resolution: image.resolution, 218 | content: undefined, // new background cached 219 | }; 220 | 221 | let content = `${meta.REPLACER_NAME}(${image.groupName}, ${groupItem.id})`; 222 | if (image.needSprite) { 223 | if (!data[image.groupName]) 224 | data[image.groupName] = {}; 225 | // background 的各种内容没变,id 一定不会变 226 | if (!data[image.groupName][groupItem.id]) 227 | data[image.groupName][groupItem.id] = groupItem; 228 | } else { 229 | const background = new Background(oldBackgroundString); 230 | background.image = image.url; 231 | content = background.toString(); 232 | } 233 | 234 | if (image.resolution === ruleItem.defaultResolution) 235 | rule.append({ prop: 'background', value: content }); 236 | else { 237 | // No problem in async function 238 | outputs.push(genMediaQuery(image.resolution, ruleItem.defaultResolution, rule.selector, `background: ${content};`)); 239 | } 240 | }); 241 | 242 | if (oldBackground.image._type === 'image-set' && !someNeedSprite && imageSetFallback.preserve) 243 | outputs.push(` 244 | ${rule.selector} { 245 | background: ${oldBackgroundString}; 246 | } 247 | `); 248 | if (outputs.length) 249 | rule.after(outputs.join('')); 250 | })); 251 | }); 252 | 253 | if (promises.length) { 254 | plugin.shouldGenerate = true; 255 | loaderContext._module[meta.MODULE_MARK] = true; 256 | } 257 | 258 | return Promise.all(promises); 259 | }); 260 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-sprite-loader 2 | 3 | - [中文说明](README.zh-CN.md) 4 | 5 | Webpack loader for creating PNG sprites. 6 | 7 | [![CircleCI][circleci-img]][circleci-url] 8 | [![NPM Version][npm-img]][npm-url] 9 | [![Dependencies][david-img]][david-url] 10 | [![NPM Download][download-img]][download-url] 11 | 12 | [circleci-img]: https://img.shields.io/circleci/project/github/vusion/css-sprite-loader.svg?style=flat-square 13 | [circleci-url]: https://circleci.com/gh/vusion/css-sprite-loader 14 | [npm-img]: http://img.shields.io/npm/v/css-sprite-loader.svg?style=flat-square 15 | [npm-url]: http://npmjs.org/package/css-sprite-loader 16 | [david-img]: http://img.shields.io/david/vusion/css-sprite-loader.svg?style=flat-square 17 | [david-url]: https://david-dm.org/vusion/css-sprite-loader 18 | [download-img]: https://img.shields.io/npm/dm/css-sprite-loader.svg?style=flat-square 19 | [download-url]: https://npmjs.org/package/css-sprite-loader 20 | 21 | ![screenshot](./example/screenshot.png) 22 | 23 | ## Example 24 | 25 | Just add a `?sprite` query after background image url: 26 | 27 | ``` css 28 | .foo { 29 | background: url('../assets/gift.png?sprite'); 30 | } 31 | ``` 32 | 33 | Then `css-sprite-loader` will generate a sprite image. 34 | 35 | ``` css 36 | .foo { 37 | background: url(/sprite.png?5d40e339682970eb14baf6110a83ddde) no-repeat; 38 | background-position: -100px -0px; 39 | } 40 | ``` 41 | 42 | ## Features 43 | 44 | Our loader works in a way different to others: 45 | 46 | - Easy to toggle whether to use sprite or not by specifying path query. 47 | - Fully support css `background` property, includes `background-position`, `background-size` and others. Make sure there are same effect before and after handling. For example: 48 | 49 | ``` css 50 | .bg-position-and-size { 51 | width: 100px; 52 | height: 150px; 53 | background: url('../images/html.png?sprite') 30px 20px no-repeat; 54 | background-size: 100%; 55 | } 56 | ``` 57 | 58 | will be newly computed position and size like this: 59 | 60 | ``` css 61 | .bg-position-and-size { 62 | width: 100px; 63 | height: 150px; 64 | background: url('dest/sprite.png?dc5323f7f35c65a3d6c7f253dcc07bad') -101.25px -111.25px / 231px 231px no-repeat; 65 | } 66 | ``` 67 | 68 | > **NOTE** 69 | > - When using `background-position`, value must be pixel and position must be left and top. 70 | > - When using `background-size`, it is recommended to specify pixel `width` and `height` properties. New values of background will be computed through them. Otherwise, new values will be computed according to source image size. This may not be consistent with the original display not in some cases. 71 | 72 | - Provide two options `retina` and `image-set` to solve high resolution image problem. See below sections [retina](#retina2x-retina3x-retina4x-) and [image-set](#image-set). 73 | 74 | ## Install 75 | 76 | ``` shell 77 | npm install --save-dev css-sprite-loader 78 | ``` 79 | 80 | ## Config 81 | 82 | You need add a loader and a plugin in Webpack config file. 83 | 84 | ``` javascript 85 | const CSSSpritePlugin = require('css-sprite-loader').Plugin; 86 | 87 | module.exports = { 88 | ... 89 | module: { 90 | rules: [{ test: /\.css$/, use: ['style-loader', 'css-loader', 'css-sprite-loader'] }], 91 | }, 92 | plugins: [new CSSSpritePlugin()], 93 | }; 94 | ``` 95 | 96 | ### Options of background property 97 | 98 | #### sprite param 99 | 100 | Whether to pack this image into sprite. Or set which sprite group to pack. For example: 101 | 102 | ``` css 103 | .foo { 104 | background: url('../images/gift.png?sprite'); 105 | } 106 | 107 | .bar { 108 | background: url('../images/light.png?sprite=sprite-nav'); 109 | } 110 | ``` 111 | 112 | images will be packed into two sprites. 113 | 114 | ``` css 115 | .foo { 116 | background: url('dest/sprite.png?fee16babb11468e0724c07bd3cf2f4cf'); 117 | } 118 | 119 | .bar { 120 | background: url('dest/sprite-nav.png?56d33b3ab0389c5b349cec93380b7ceb'); 121 | } 122 | ``` 123 | 124 | #### retina@2x, retina@3x, retina@4x, ... 125 | 126 | Whether to use retina images in some resolution. For example, if you have a directory: 127 | 128 | ``` 129 | images/ 130 | angry-birds.png 131 | angry-birds@2x.png 132 | angry-birds@4x.png 133 | ``` 134 | 135 | Then you can write CSS in this form 136 | 137 | ``` css 138 | .baz { 139 | width: 128px; 140 | height: 128px; 141 | background: url('../../fixtures/images/retina/angry-birds.png?sprite&retina@2x&retina@4x'); 142 | background-size: 100%; 143 | } 144 | ``` 145 | 146 | They will be converted to 147 | 148 | ``` css 149 | .baz { 150 | width: 128px; 151 | height: 128px; 152 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') -296px 0 / 424px 424px no-repeat; 153 | } 154 | 155 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 156 | .baz { 157 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat; 158 | } 159 | } 160 | 161 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 162 | .baz { 163 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat; 164 | } 165 | } 166 | ``` 167 | 168 | You can also use @2x as default resolution: 169 | 170 | ``` 171 | images/ 172 | angry-birds@1x.png 173 | angry-birds@2x.png 174 | angry-birds@4x.png 175 | ``` 176 | 177 | ``` css 178 | .baz { 179 | width: 128px; 180 | height: 128px; 181 | background: url('../../fixtures/images/retina/angry-birds@2x.png?sprite&retina@1x&retina@4x'); 182 | background-size: 100%; 183 | } 184 | ``` 185 | 186 | This will be converted to 187 | 188 | ``` css 189 | .baz { 190 | width: 128px; 191 | height: 128px; 192 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat; 193 | } 194 | 195 | @media (-webkit-max-device-pixel-ratio: 1), (max-resolution: 1dppx) { 196 | .baz { 197 | background: url('dest/sprite@1x.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 128px 128px no-repeat; 198 | } 199 | } 200 | 201 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 202 | .baz { 203 | background: url('dest/sprite@4x.png?4a6a7dbace7933efe321b357d4db2fb9') 30px 20px / 213px 102px no-repeat; 204 | } 205 | } 206 | ``` 207 | 208 | > **NOTE** 209 | > Here, the original image path corresponding to `retina@1x` should be explicitly named `xxx@1x`, and finally it will be packed into `sprite@1x`. 210 | 211 | #### image-set function 212 | 213 | Image-set function is another way to set different resolution images. 214 | 215 | Image-set function feature is in [Stage 2](https://www.w3.org/TR/css-images-4/#image-set-notation). Here is [Browsers Compatibility](https://developer.mozilla.org/en-US/docs/Web/CSS/image-set#Browser_compatibility). In this loader, it will be processed by PostCSS. 216 | 217 | > **NOTE** 218 | > If you want to use this feature, make sure that `image-set` won't be processed before css-sprite-loader. For example, before this loader, there is a postcss-loader and plugin of it `postcss-preset-env` willing polyfill `image-set`. You can disable it by setting options like: 219 | > ``` js 220 | > { 221 | > features: { 222 | > 'image-set-function': false, 223 | > }, 224 | > } 225 | > ``` 226 | 227 | For example, if you have a directory: 228 | 229 | ``` 230 | images/ 231 | angry-birds.png 232 | angry-birds@2x.png 233 | angry-birds@4x.png 234 | ``` 235 | 236 | Then you can write CSS in this form 237 | 238 | ``` css 239 | .baz { 240 | width: 128px; 241 | height: 128px; 242 | background: image-set('../images/retina/angry-birds.png?sprite' 1x, '../images/retina/angry-birds@2x.png?sprite' 2x); 243 | background-size: 100%; 244 | } 245 | ``` 246 | 247 | This will be converted to 248 | 249 | ``` css 250 | .baz { 251 | width: 128px; 252 | height: 128px; 253 | background: url('dest/sprite.png?369108fb0a164b04ee10def7ed6d4226') 0 0 / 212px 212px no-repeat; 254 | } 255 | 256 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 257 | .baz { 258 | background: url('dest/sprite@2x.png?51d951f98092152d8fc56bf3380577e3') -148px 0 / 276px 128px no-repeat; 259 | } 260 | } 261 | ``` 262 | 263 | If required, you can specify an image not to be packed or packed in a different group. 264 | 265 | ``` css 266 | .baz { 267 | width: 128px; 268 | height: 128px; 269 | background: image-set( 270 | '../images/retina/angry-birds.png?sprite' 1x, 271 | '../images/retina/angry-birds@2x.png?sprite-nav' 2x, 272 | '../images/retina/angry-birds@4x.png' 4x, 273 | ); 274 | background-size: 100%; 275 | } 276 | ``` 277 | 278 | will be converted to 279 | 280 | ``` css 281 | .baz { 282 | width: 128px; 283 | height: 128px; 284 | background: url('dest/sprite.png?e5cf95daa8d2c40e290009620b13fba3') 0 0 / 212px 212px no-repeat; 285 | } 286 | 287 | @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 2dppx) { 288 | .baz { 289 | background: url('dest/sprite-nav@2x.png?369108fb0a164b04ee10def7ed6d4226') -148px 0 / 276px 128px no-repeat; 290 | } 291 | } 292 | 293 | @media (-webkit-min-device-pixel-ratio: 4), (min-resolution: 4dppx) { 294 | .baz { 295 | background: url('dest/angry-birds@4x?4a6a7dbace7933efe321b357d4db2fb9') no-repeat; 296 | } 297 | } 298 | ``` 299 | 300 | ### loader options 301 | 302 | None. 303 | 304 | ### plugin options 305 | 306 | #### defaultName 307 | 308 | Default sprite group name. 309 | 310 | - Type: `string` 311 | - Default: `'sprite'` 312 | 313 | #### filename 314 | 315 | Output filename format like output. filename of Webpack. The following tokens will be replaced: 316 | 317 | - `[ext]` the extension of the resource 318 | - `[name]` the group name 319 | - `[hash]` the hash of svg file (Buffer) (by default it's the hex digest of the md5 hash, and all file will use hash of the svg file) 320 | - `[:hash::]` optionally one can configure 321 | - other `hashType`s, i. e. `sha1`, `md5`, `sha256`, `sha512` 322 | - other `digestType`s, i. e. `hex`, `base26`, `base32`, `base36`, `base49`, `base52`, `base58`, `base62`, `base64` 323 | - and `length` the length in chars 324 | 325 | 326 | - Type: `string` 327 | - Default: `'[name].[ext]?[hash]'` 328 | 329 | #### output 330 | 331 | Output path of emitted image files, relative to webpack output path. **Must be a relative path.** 332 | 333 | - Type: `string` 334 | - Default: `'./'` 335 | 336 | #### publicPath 337 | 338 | Image public path in css url, same as webpack output.publicPath. This option is for overriding it. 339 | 340 | - Type: `string` 341 | - Default: `''` 342 | 343 | #### padding 344 | 345 | The padding between small images in sprite. 346 | 347 | - Type: `number` 348 | - Default: `20` 349 | 350 | #### filter 351 | 352 | - Type: `string` 353 | - Default: `'all'` 354 | 355 | Options: `'all'`、`'query'`、`RegExp` 356 | 357 | How to filter source image files for merging: 358 | 359 | - `'all'`: All imported images will be merged. 360 | - `'query'`: Only image path with `?sprite` query param will be merged. 361 | - `RegExp`: Only image path matched by RegExp 362 | 363 | #### queryParam 364 | 365 | Customize key of query param in svg path. Only works when `filter: 'query'`. 366 | 367 | - Type: `string` 368 | - Default: `'sprite'` 369 | 370 | #### imageSetFallback 371 | 372 | Whether to process images without sprite query in `image-set`. They may be no need to polyfill because some browsers already support `-webkit-image-set`. 373 | 374 | - Type: `boolean` 375 | - Default: `false` 376 | 377 | #### plugins 378 | 379 | Postcss plugins will be processed on related codes after creating sprite image. For example, you can use `require('postcss-px-to-viewport')` to convert units of background value. 380 | 381 | - Type: `Array` 382 | - Default: `[]` 383 | 384 | ## Changelog 385 | 386 | See [Releases](https://github.com/vusion/css-sprite-loader/releases) 387 | 388 | ## Contributing 389 | 390 | See [Contributing Guide](https://github.com/vusion/DOCUMENTATION/issues/8) 391 | 392 | ## License 393 | 394 | [MIT](LICENSE) 395 | --------------------------------------------------------------------------------