Skip to content

Commit 819b9ce

Browse files
committed
Initial
0 parents  commit 819b9ce

File tree

8 files changed

+3815
-0
lines changed

8 files changed

+3815
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
coverage
3+
lib

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
language: node_js
2+
node_js:
3+
- "4"
4+
- "6"
5+
- "node"

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2017 Bogdan Chadkin
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.

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# postcss-icss-import [![Build Status][travis-img]][travis]
2+
3+
[PostCSS] plugin for css-modules to convert @import statements to ICSS
4+
5+
[PostCSS]: https://github.com/postcss/postcss
6+
[travis-img]: https://travis-ci.org/css-modules/postcss-icss-import.svg
7+
[travis]: https://travis-ci.org/css-modules/postcss-icss-import
8+
9+
## Usage
10+
11+
```js
12+
postcss([ require('postcss-icss-import') ])
13+
```
14+
15+
See [PostCSS] docs for examples for your environment.
16+
17+
# License
18+
19+
MIT © [Bogdan Chadkin](mailto:trysound@yandex.ru)

package.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "postcss-icss-import",
3+
"version": "0.1.0",
4+
"description": "PostCSS plugin for css-modules to convert @import statements to ICSS",
5+
"main": "lib/index.js",
6+
"files": [
7+
"lib"
8+
],
9+
"scripts": {
10+
"build": "babel --out-dir lib src",
11+
"test": "jest --coverage",
12+
"precommit": "lint-staged",
13+
"prepublish": "yarn test && yarn run build"
14+
},
15+
"lint-staged": {
16+
"*.js": [
17+
"eslint",
18+
"prettier --write",
19+
"git add"
20+
]
21+
},
22+
"eslintConfig": {
23+
"parserOptions": {
24+
"ecmaVersion": 6,
25+
"sourceType": "module"
26+
},
27+
"extends": "eslint:recommended"
28+
},
29+
"babel": {
30+
"presets": [
31+
[
32+
"env",
33+
{
34+
"targets": {
35+
"node": 4
36+
}
37+
}
38+
]
39+
]
40+
},
41+
"repository": "css-modules/postcss-icss-import",
42+
"author": "Bogdan Chadkin <trysound@yandex.ru>",
43+
"license": "MIT",
44+
"devDependencies": {
45+
"babel-cli": "^6.24.1",
46+
"babel-jest": "^20.0.3",
47+
"babel-preset-env": "^1.5.1",
48+
"eslint": "^3.19.0",
49+
"husky": "^0.13.4",
50+
"jest": "^20.0.4",
51+
"lint-staged": "^3.5.1",
52+
"prettier": "^1.3.1",
53+
"strip-indent": "^2.0.0"
54+
},
55+
"dependencies": {
56+
"icss-utils": "^2.1.0",
57+
"postcss": "^6.0.1",
58+
"postcss-value-parser": "^3.3.0"
59+
}
60+
}

src/index.js

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/* eslint-env node */
2+
import postcss from "postcss";
3+
import valueParser from "postcss-value-parser";
4+
import { createICSSRules } from "icss-utils";
5+
6+
const plugin = "postcss-plugin-import";
7+
8+
const getArg = nodes =>
9+
nodes.length !== 0 && nodes[0].type === "string"
10+
? nodes[0].value
11+
: valueParser.stringify(nodes);
12+
13+
const getUrl = node => {
14+
if (node.type === "function" && node.value === "url") {
15+
return getArg(node.nodes);
16+
}
17+
if (node.type === "string") {
18+
return node.value;
19+
}
20+
return "";
21+
};
22+
23+
const parseImport = params => {
24+
const { nodes } = valueParser(params);
25+
if (nodes.length === 0) {
26+
return null;
27+
}
28+
const url = getUrl(nodes[0]);
29+
if (url.trim().length === 0) {
30+
return null;
31+
}
32+
return {
33+
url,
34+
media: valueParser.stringify(nodes.slice(1)).trim()
35+
};
36+
};
37+
38+
const isExternalUrl = url => /^\w+:\/\//.test(url) || url.startsWith("//");
39+
40+
const walkImports = (css, callback) => {
41+
css.each(node => {
42+
if (node.type === "atrule" && node.name.toLowerCase() === "import") {
43+
callback(node);
44+
}
45+
});
46+
};
47+
48+
module.exports = postcss.plugin(plugin, () => (css, result) => {
49+
const imports = {};
50+
walkImports(css, atrule => {
51+
if (atrule.nodes) {
52+
return result.warn(
53+
"It looks like you didn't end your @import statement correctly. " +
54+
"Child nodes are attached to it.",
55+
{ node: atrule }
56+
);
57+
}
58+
const parsed = parseImport(atrule.params);
59+
if (parsed === null) {
60+
return result.warn(`Unable to find uri in '${atrule.toString()}'`, {
61+
node: atrule
62+
});
63+
}
64+
if (!isExternalUrl(parsed.url)) {
65+
atrule.remove();
66+
imports[`'${parsed.url}'`] = {
67+
import: "default" +
68+
(parsed.media.length === 0 ? "" : ` ${parsed.media}`)
69+
};
70+
}
71+
});
72+
css.prepend(createICSSRules(imports, {}));
73+
});

test/test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
/* eslint-env jest */
2+
import postcss from "postcss";
3+
import stripIndent from "strip-indent";
4+
import plugin from "../src";
5+
6+
const strip = input => stripIndent(input).replace(/^\n/, "");
7+
const compile = input => postcss([plugin]).process(input);
8+
const runWarnings = input =>
9+
compile(input).then(result => result.warnings().map(warning => warning.text));
10+
const runCSS = input => compile(strip(input)).then(result => strip(result.css));
11+
12+
test("convert @import", () => {
13+
return expect(
14+
runCSS(`
15+
@import "foo.css";
16+
@import 'bar.css';
17+
@import url(baz.css);
18+
@import url("foobar.css");
19+
@import url('foobarbaz.css');
20+
.foo {}
21+
`)
22+
).resolves.toEqual(
23+
strip(`
24+
:import('foo.css') {
25+
import: default
26+
}
27+
:import('bar.css') {
28+
import: default
29+
}
30+
:import('baz.css') {
31+
import: default
32+
}
33+
:import('foobar.css') {
34+
import: default
35+
}
36+
:import('foobarbaz.css') {
37+
import: default
38+
}
39+
.foo {}
40+
`)
41+
);
42+
});
43+
44+
test("convert @import with media queries", () => {
45+
return expect(
46+
runCSS(`
47+
@import "foo.css" screen;
48+
@import 'bar.css' screen;
49+
@import url(baz.css) screen;
50+
@import url("foobar.css") screen and (min-width: 25em);
51+
@import url('foobarbaz.css') print, screen and (min-width: 25em);
52+
.foo {}
53+
`)
54+
).resolves.toEqual(
55+
strip(`
56+
:import('foo.css') {
57+
import: default screen
58+
}
59+
:import('bar.css') {
60+
import: default screen
61+
}
62+
:import('baz.css') {
63+
import: default screen
64+
}
65+
:import('foobar.css') {
66+
import: default screen and (min-width: 25em)
67+
}
68+
:import('foobarbaz.css') {
69+
import: default print, screen and (min-width: 25em)
70+
}
71+
.foo {}
72+
`)
73+
);
74+
});
75+
76+
test("convert camelcased @import", () => {
77+
return expect(
78+
runCSS(`
79+
@IMPORT 'path.css';
80+
`)
81+
).resolves.toEqual(
82+
strip(`
83+
:import('path.css') {
84+
import: default
85+
}
86+
`)
87+
);
88+
});
89+
90+
test("not convert external uri", () => {
91+
const input = `
92+
@import 'http://path';
93+
@import 'https://path';
94+
@import '//path';
95+
`;
96+
return expect(runCSS(input)).resolves.toEqual(strip(input));
97+
});
98+
99+
test("not warn when a user closed an import with ;", () => {
100+
return expect(runWarnings(`@import url('http://');`, [])).resolves.toEqual(
101+
[]
102+
);
103+
});
104+
105+
test("warn when a user didn't close an import with ;", () => {
106+
return expect(
107+
runWarnings(`@import url('http://') :root{}`)
108+
).resolves.toEqual([
109+
"It looks like you didn't end your @import statement correctly. " +
110+
"Child nodes are attached to it."
111+
]);
112+
});
113+
114+
test("warn on invalid url", () => {
115+
return expect(
116+
runWarnings(`
117+
@import foo-bar;
118+
@import ;
119+
@import '';
120+
@import "";
121+
@import " ";
122+
@import url();
123+
@import url('');
124+
@import url("");
125+
`)
126+
).resolves.toEqual([
127+
`Unable to find uri in '@import foo-bar'`,
128+
`Unable to find uri in '@import '`,
129+
`Unable to find uri in '@import '''`,
130+
`Unable to find uri in '@import ""'`,
131+
`Unable to find uri in '@import " "'`,
132+
`Unable to find uri in '@import url()'`,
133+
`Unable to find uri in '@import url('')'`,
134+
`Unable to find uri in '@import url("")'`
135+
]);
136+
});
137+
138+
test("convert only top-level @import", () => {
139+
const input = `
140+
.foo {
141+
@import 'path.css';
142+
}
143+
`;
144+
return expect(runCSS(input)).resolves.toEqual(strip(input));
145+
});

0 commit comments

Comments
 (0)