├── test ├── fixtures │ ├── import.no.name.js │ ├── import.nocss.js │ ├── invalid.css │ ├── nocss.js │ ├── require.js │ ├── simple.css │ ├── simple.scss │ ├── import.js │ ├── import.scss.js │ ├── require.expected.empty.js │ ├── require.expected.js │ ├── import.no.modules.expected.js │ ├── import.expected.js │ ├── import.scss.expected.js │ └── postcss.config.js ├── mocha.opts ├── .eslintrc ├── bootstrap.js ├── helpers.js ├── postcss-client.js ├── plugin.js └── postcss-server.js ├── .gitignore ├── index.js ├── .eslintignore ├── .flowconfig ├── .npmignore ├── .travis.yml ├── .babelrc ├── LICENSE ├── src ├── postcss-client.js ├── postcss-server.js └── plugin.js ├── package.json ├── README.md └── .eslintrc /test/fixtures/import.no.name.js: -------------------------------------------------------------------------------- 1 | import 'simple.css'; -------------------------------------------------------------------------------- /test/fixtures/import.nocss.js: -------------------------------------------------------------------------------- 1 | import notStyles from './foo.cs'; -------------------------------------------------------------------------------- /test/fixtures/invalid.css: -------------------------------------------------------------------------------- 1 | .simple { 2 | color: magenta; 3 | -------------------------------------------------------------------------------- /test/fixtures/nocss.js: -------------------------------------------------------------------------------- 1 | var styles = require('./simple'); 2 | -------------------------------------------------------------------------------- /test/fixtures/require.js: -------------------------------------------------------------------------------- 1 | var styles = require('./simple.css'); 2 | -------------------------------------------------------------------------------- /test/fixtures/simple.css: -------------------------------------------------------------------------------- 1 | .simple { 2 | color: magenta; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/simple.scss: -------------------------------------------------------------------------------- 1 | .simple { 2 | color: magenta; 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /coverage/ 4 | /.nyc_output/ 5 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require babel-register 2 | --require test/bootstrap.js 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | module.exports = require('./dist/plugin'); 4 | -------------------------------------------------------------------------------- /test/fixtures/import.js: -------------------------------------------------------------------------------- 1 | import styles from "./simple.css"; 2 | 3 | console.log(styles); 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | /coverage/ 4 | /.nyc_output/ 5 | /test/fixtures/ 6 | -------------------------------------------------------------------------------- /test/fixtures/import.scss.js: -------------------------------------------------------------------------------- 1 | import styles from "./simple.scss"; 2 | 3 | console.log(styles); 4 | -------------------------------------------------------------------------------- /test/fixtures/require.expected.empty.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var styles = {}; // @related-file ./simple.css 4 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | dist/.* 3 | coverage/.* 4 | scripts/.* 5 | 6 | [include] 7 | 8 | [libs] 9 | 10 | [options] 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .babelrc 2 | .eslintignore 3 | .eslintrc 4 | .flowconfig 5 | .nyc_output 6 | .travis.yml 7 | coverage/ 8 | src/ 9 | test/ 10 | -------------------------------------------------------------------------------- /test/fixtures/require.expected.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var styles = { 4 | 'simple': '_simple_jvai8_1' 5 | }; // @related-file ./simple.css 6 | -------------------------------------------------------------------------------- /test/fixtures/import.no.modules.expected.js: -------------------------------------------------------------------------------- 1 | var styles = { 2 | "simple": "_simple_jvai8_1" 3 | }; // @related-file ./simple.css 4 | 5 | console.log(styles); 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '8' 4 | - '6' 5 | after_script: 6 | - npm install coveralls@2 && cat ./coverage/lcov.info | coveralls 7 | sudo: false 8 | -------------------------------------------------------------------------------- /test/fixtures/import.expected.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var styles = { 4 | "simple": "_simple_jvai8_1" 5 | }; // @related-file ./simple.css 6 | 7 | console.log(styles); 8 | -------------------------------------------------------------------------------- /test/fixtures/import.scss.expected.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var styles = { 4 | "simple": "_simple_jvai8_1" 5 | }; // @related-file ./simple.scss 6 | 7 | console.log(styles); 8 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expressions": "off", 4 | "flowtype/require-return-type": ["error", "always", { "excludeArrowFunctions": true }], 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import chai from 'chai'; 4 | import chaiString from 'chai-string'; 5 | import sinonChai from 'sinon-chai'; 6 | 7 | chai.use(chaiString); 8 | chai.use(sinonChai); 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "6.11.1" 6 | } 7 | }] 8 | ], 9 | "plugins": [ 10 | "transform-flow-strip-types" 11 | ], 12 | "env": { 13 | "coverage": { 14 | "plugins": [ 15 | "istanbul", 16 | ] 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/postcss.config.js: -------------------------------------------------------------------------------- 1 | var stringHash = require('string-hash'); 2 | 3 | module.exports = (ctx) => ({ 4 | plugins: [ 5 | require('postcss-modules')({ 6 | getJSON: ctx.extractModules, 7 | generateScopedName: (name, filename, css) => { 8 | if (!filename.match(/\.css$/)) { 9 | return name + '_through_invalid_file'; 10 | } 11 | 12 | const i = css.indexOf(`.${ name }`); 13 | const lineNumber = css.substr(0, i).split(/[\r\n]/).length; 14 | const hash = stringHash(css).toString(36).substr(0, 5); 15 | 16 | return `_${ name }_${ hash }_${ lineNumber }`; 17 | }, 18 | }), 19 | ], 20 | }); 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Whitney Young 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/helpers.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import * as babel from 'babel-core'; 6 | 7 | const fixtures = path.join(__dirname, 'fixtures'); 8 | 9 | export const babelNoModules = { 10 | presets: [ ['env', { modules: false, targets: { node: 'current' } }] ], 11 | }; 12 | 13 | export const transform = ( 14 | filename: string, 15 | babelOptionOverrides: ?{ [string]: mixed }, 16 | extensions: ?string[], 17 | ): Promise => { 18 | const file = path.join(fixtures, filename); 19 | 20 | const options = Object.assign({ 21 | babelrc: false, 22 | presets: [ ['env', { targets: { node: 'current' } }] ], 23 | plugins: [ 24 | ['../../src/plugin.js', { 25 | config: 'fixtures/postcss.config.js', 26 | extensions, 27 | }], 28 | ], 29 | }, babelOptionOverrides); 30 | 31 | return new Promise((resolve: (any) => void, reject: (Error) => void) => { 32 | babel.transformFile(file, options, (err: ?Error, result: any) => { 33 | if (err) { reject(err); } 34 | else { resolve(result.code); } 35 | }); 36 | }); 37 | }; 38 | 39 | export const read = (filename: string): Promise => { 40 | const file = path.join(fixtures, filename); 41 | const options = { 42 | encoding: 'utf8', 43 | }; 44 | 45 | return new Promise((resolve: (any) => void, reject: (Error) => void) => { 46 | fs.readFile(file, options, (err: ?ErrnoError, result: string) => { 47 | if (err) { reject(err); } 48 | else { resolve(result); } 49 | }); 50 | }); 51 | }; 52 | -------------------------------------------------------------------------------- /src/postcss-client.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import net from 'net'; 4 | 5 | // exponential backoff, roughly 100ms-6s 6 | const retries = [1, 2, 3, 4, 5].map((num) => Math.exp(num) * 40); 7 | const streams = { stdout: process.stdout }; // overwritable by tests 8 | 9 | const communicate = async function communicate( 10 | socketPath: string, 11 | message: string, 12 | ): Promise { 13 | await new Promise((resolve: () => void, reject: (Error) => void) => { 14 | const client = net.connect(socketPath, () => { 15 | client.end(message); 16 | client.pipe(streams.stdout); 17 | }); 18 | 19 | client.on('error', (err: ErrnoError) => reject(err)); 20 | client.on('close', (err: ?Error) => { 21 | if (err) { reject(err); } 22 | else { resolve(); } 23 | }); 24 | }); 25 | }; 26 | 27 | const main = async function main(...args: string[]): Promise { 28 | try { await communicate(...args); } 29 | catch (err) { 30 | const recoverable = ( 31 | err.code === 'ECONNREFUSED' || 32 | err.code === 'ENOENT' 33 | ); 34 | 35 | if (recoverable && retries.length) { 36 | await new Promise((resolve: () => void, reject: (Error) => void) => { 37 | setTimeout(() => { 38 | main(...args).then(resolve, reject); 39 | }, retries.shift()); 40 | }); 41 | } 42 | } 43 | }; 44 | 45 | /* istanbul ignore if */ 46 | if ((require: any).main === module) { 47 | (async(): Promise => { 48 | try { await main(...process.argv.slice(2)); } 49 | catch (err) { process.stderr.write(`${err.stack}\n`); process.exit(1); } 50 | })(); 51 | } 52 | 53 | export { 54 | main, 55 | streams as _streams, 56 | retries as _retries, 57 | }; 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-transform-postcss", 3 | "version": "0.2.1", 4 | "description": "PostCSS Babel Transform", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint . && flow check && BABEL_ENV=coverage nyc mocha", 8 | "build": "babel src --copy-files --out-dir dist", 9 | "prepublish": "npm run build" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/wbyoung/babel-plugin-transform-postcss" 14 | }, 15 | "nyc": { 16 | "include": [ 17 | "src/**/*.js" 18 | ], 19 | "exclude": [ 20 | "coverage/**", 21 | "dist/**" 22 | ], 23 | "reporter": [ 24 | "lcov", 25 | "html", 26 | "text" 27 | ], 28 | "all": true, 29 | "check-coverage": true, 30 | "lines": 100, 31 | "statements": 100, 32 | "functions": 100, 33 | "branches": 100, 34 | "sourceMap": false, 35 | "instrument": false, 36 | "temp-directory (pending-accepted-pr)": "coverage/nyc" 37 | }, 38 | "author": "Whitney Young", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/wbyoung/babel-plugin-transform-postcss/issues" 42 | }, 43 | "keywords": [ 44 | "postcss", 45 | "postcss-modules", 46 | "babel", 47 | "babel-transform", 48 | "css-modules" 49 | ], 50 | "homepage": "https://github.com/wbyoung/babel-plugin-transform-postcss", 51 | "devDependencies": { 52 | "babel-cli": "^6.18.0", 53 | "babel-core": "^6.18.0", 54 | "babel-eslint": "^8.0.0", 55 | "babel-plugin-istanbul": "^4.1.1", 56 | "babel-plugin-transform-flow-strip-types": "^6.21.0", 57 | "babel-preset-env": "^1.1.8", 58 | "babel-register": "^6.18.0", 59 | "chai": "^4.0.2", 60 | "chai-string": "^1.3.0", 61 | "eslint": "^4.1.0", 62 | "eslint-plugin-flowtype": "^2.29.2", 63 | "flow-bin": "^0.61.0", 64 | "mocha": "^5.0.0", 65 | "nyc": "^11.0.1", 66 | "postcss": "^6.0.7", 67 | "postcss-modules": "^1.0.0", 68 | "sinon": "^4.0.1", 69 | "sinon-chai": "^2.8.0", 70 | "string-hash": "^1.1.1" 71 | }, 72 | "peerDependencies": { 73 | "postcss": "^6.0.6 || ^7.0.0" 74 | }, 75 | "dependencies": { 76 | "debug": "^3.0.0", 77 | "postcss-load-config": "^1.1.0" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/postcss-client.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-sync */ 3 | 4 | import { 5 | describe, 6 | it, 7 | beforeEach, 8 | afterEach, 9 | } from 'mocha'; 10 | 11 | import { 12 | main, 13 | _streams as streams, 14 | _retries as retries, 15 | } from '../src/postcss-client'; 16 | 17 | import { 18 | spy, 19 | } from 'sinon'; 20 | 21 | import { expect } from 'chai'; 22 | 23 | import { join } from 'path'; 24 | import fs from 'fs'; 25 | import net from 'net'; 26 | import type { Socket } from 'net'; 27 | 28 | const testSocket = join(__dirname, 'tmp.sock'); 29 | const testOutput = join(__dirname, 'tmp.out'); 30 | 31 | describe('postcss-client', () => { 32 | let originalStdout, originalRetries; 33 | 34 | beforeEach(() => { 35 | originalStdout = streams.stdout; 36 | streams.stdout = fs.createWriteStream(testOutput); 37 | }); 38 | afterEach(() => { 39 | streams.stdout = originalStdout; 40 | fs.unlinkSync(testOutput); 41 | }); 42 | 43 | beforeEach(() => { 44 | originalRetries = [...retries]; 45 | retries.splice(0, retries.length, 1); 46 | }); 47 | afterEach(() => { 48 | retries.splice(0, retries.length, ...originalRetries); 49 | }); 50 | 51 | beforeEach(() => spy(net, 'connect')); 52 | afterEach(() => net.connect.restore()); 53 | 54 | describe('with a server to connect to', () => { 55 | let server, received; 56 | 57 | beforeEach(() => { 58 | received = ''; 59 | server = net.createServer({ allowHalfOpen: true }, (conn: Socket) => { 60 | conn.on('data', (chunk: Buffer) => { 61 | received += chunk.toString('utf8'); 62 | }); 63 | conn.on('end', () => { 64 | conn.end('server output'); 65 | }); 66 | }); 67 | 68 | return new Promise((resolve: () => void, reject: (Error) => void) => { 69 | server.listen(testSocket, (err: ?Error) => { 70 | if (err) { reject(err); } 71 | else { resolve(); } 72 | }); 73 | }); 74 | }); 75 | 76 | afterEach(async() => { 77 | await new Promise((resolve: () => void, reject: (Error) => void) => { 78 | fs.unlinkSync(testSocket); 79 | server.close((err: ?Error) => { 80 | if (err) { reject(err); } 81 | else { resolve(); } 82 | }); 83 | }); 84 | }); 85 | 86 | describe('main(...testArgs)', () => { 87 | beforeEach(async() => { 88 | const write = new Promise((resolve: () => void) => { 89 | streams.stdout.on('finish', () => resolve()); 90 | }); 91 | 92 | await Promise.all([main(testSocket, 'client message'), write]); 93 | }); 94 | 95 | it('sends client message to server', () => { 96 | expect(received).to.eql('client message'); 97 | }); 98 | 99 | it('writes server response to stdout', () => { 100 | expect(fs.readFileSync(testOutput, 'utf8')).to.eql('server output'); 101 | }); 102 | 103 | it('succeeds during first connect attempt', () => { 104 | expect(net.connect).to.have.been.calledOnce; 105 | }); 106 | }); 107 | }); 108 | 109 | describe('main(...testArgs)', () => { 110 | beforeEach(async() => { await main(testSocket, 'client message'); }); 111 | 112 | it('attempts to re-connect', () => { 113 | expect(net.connect).to.have.been.calledTwice; 114 | }); 115 | }); 116 | 117 | }); 118 | -------------------------------------------------------------------------------- /src/postcss-server.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import net from 'net'; 4 | import fs from 'fs'; 5 | import path from 'path'; 6 | import util from 'util'; 7 | import crypto from 'crypto'; 8 | import makeDebug from 'debug'; 9 | import postcss from 'postcss'; 10 | import loadConfig from 'postcss-load-config'; 11 | import type { Socket, Server } from 'net'; 12 | 13 | const debug = makeDebug('babel-plugin-transform-postcss'); 14 | const streams = { stderr: process.stderr }; // overwritable by tests 15 | const md5 = (data: string) => ( 16 | crypto.createHash('md5').update(data).digest('hex') 17 | ); 18 | const error = (...args: any) => { 19 | let prefix = 'babel-plugin-transform-postcss: '; 20 | const message = util.format(...args); 21 | 22 | if (streams.stderr.isTTY) { 23 | prefix = `\x1b[31m${prefix}\x1b[0m`; 24 | } 25 | 26 | streams.stderr.write(`${prefix}${message}\n`); 27 | }; 28 | 29 | const main = async function main( 30 | socketPath: string, 31 | tmpPath: string, 32 | ): Promise { 33 | 34 | try { fs.mkdirSync(tmpPath); } // eslint-disable-line no-sync 35 | catch (err) { 36 | if (err.code !== 'EEXIST') { 37 | throw err; 38 | } 39 | } 40 | 41 | const options = { allowHalfOpen: true }; 42 | const server = net.createServer(options, (connection: Socket) => { 43 | let data: string = ''; 44 | 45 | connection.on('data', (chunk: Buffer) => { 46 | data += chunk.toString('utf8'); 47 | }); 48 | 49 | connection.on('end', async(): Promise => { 50 | try { 51 | let tokens, cache; 52 | const { cssFile, config } = JSON.parse(data); 53 | const cachePath = 54 | `${path.join(tmpPath, cssFile.replace(/[^a-z]/ig, ''))}.cache`; 55 | const source = // eslint-disable-next-line no-sync 56 | fs.readFileSync(cssFile, 'utf8'); 57 | const hash = md5(source); 58 | 59 | // eslint-disable-next-line no-sync 60 | try { cache = JSON.parse(fs.readFileSync(cachePath, 'utf8')); } 61 | catch (err) { 62 | if (err.code !== 'ENOENT') { 63 | throw err; 64 | } 65 | } 66 | 67 | if (cache && cache.hash === hash) { 68 | connection.end(JSON.stringify(cache.tokens)); 69 | 70 | return; 71 | } 72 | 73 | const extractModules = (_, resultTokens: any) => { 74 | tokens = resultTokens; 75 | }; 76 | 77 | let configPath = path.dirname(cssFile); 78 | 79 | if (config) { 80 | configPath = path.resolve(config); 81 | } 82 | 83 | const { plugins, options: postcssOpts } = 84 | await loadConfig({ extractModules }, configPath); 85 | 86 | const runner = postcss(plugins); 87 | 88 | await runner.process(source, Object.assign({ 89 | from: cssFile, 90 | to: cssFile, // eslint-disable-line id-length 91 | }, postcssOpts)); 92 | 93 | cache = { 94 | hash, 95 | tokens, 96 | }; 97 | 98 | // eslint-disable-next-line no-sync 99 | fs.writeFileSync(cachePath, JSON.stringify(cache)); 100 | 101 | connection.end(JSON.stringify(tokens)); 102 | } 103 | catch (err) { 104 | error(err.stack); 105 | connection.end(); 106 | } 107 | }); 108 | }); 109 | 110 | if (fs.existsSync(socketPath)) { // eslint-disable-line no-sync 111 | error(`Server already running on socket ${socketPath}`); 112 | process.exit(1); 113 | 114 | return server; // tests can make it past process.exit 115 | } 116 | 117 | await new Promise((resolve: () => void, reject: (Error) => void) => { 118 | server.on('error', (err: Error) => reject(err)); 119 | server.on('listening', () => { 120 | const handler = () => { 121 | fs.unlinkSync(socketPath); // eslint-disable-line no-sync 122 | }; 123 | 124 | server.on('close', () => { 125 | process.removeListener('exit', handler); 126 | process.removeListener('SIGINT', handler); 127 | process.removeListener('SIGTERM', handler); 128 | }); 129 | 130 | process.on('exit', handler); 131 | process.on('SIGINT', handler); 132 | process.on('SIGTERM', handler); 133 | 134 | resolve(); 135 | }); 136 | 137 | server.listen(socketPath, () => { 138 | debug( 139 | `babel-plugin-transform-postcss server running on socket ${socketPath}` 140 | ); 141 | }); 142 | }); 143 | 144 | return server; 145 | }; 146 | 147 | /* istanbul ignore if */ 148 | if ((require: any).main === module) { 149 | (async(): Promise => { 150 | try { await main(...process.argv.slice(2)); } 151 | catch (err) { process.stderr.write(`${err.stack}\n`); process.exit(1); } 152 | })(); 153 | } 154 | 155 | export { 156 | main, 157 | streams as _streams, 158 | }; 159 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | dirname, 5 | extname, 6 | resolve, 7 | join, 8 | } from 'path'; 9 | 10 | import { 11 | execFileSync, 12 | spawn, 13 | } from 'child_process'; 14 | 15 | // note: socket path is important to keep short as it will be truncated if it 16 | // exceeds certain platform limits. for this reason, we're writing to /tmp 17 | // instead of using os.tmpdir (which can, on platforms like darwin, be quite 18 | // long & per-process). 19 | const projectId = process.cwd().toLowerCase().replace(/[^a-z]/ig, ''); 20 | const socketName = `bptp-${projectId}.sock`; 21 | const socketPath = join('/tmp', socketName); 22 | const tmpPath = join('/tmp', `bptp-${projectId}`); 23 | 24 | const nodeExecutable = process.argv[0]; 25 | const clientExcutable = join(__dirname, 'postcss-client.js'); 26 | const serverExcutable = join(__dirname, 'postcss-server.js'); 27 | 28 | let server; 29 | 30 | const startServer = () => { 31 | server = spawn(nodeExecutable, [serverExcutable, socketPath, tmpPath], { 32 | env: process.env, // eslint-disable-line no-process-env 33 | stdio: 'inherit', 34 | }); 35 | 36 | server.unref(); 37 | }; 38 | 39 | const stopServer = () => { 40 | if (!server) { return; } 41 | 42 | server.kill(); 43 | server = null; 44 | process.removeListener('exit', stopServer); 45 | }; 46 | 47 | const launchServer = () => { 48 | if (server) { return; } 49 | 50 | startServer(); 51 | 52 | process.on('exit', stopServer); 53 | }; 54 | 55 | const defaultExtensions = ['.css']; 56 | 57 | const getStylesFromStylesheet = ( 58 | stylesheetPath: string, 59 | file: any, 60 | config: any, 61 | configExtensions: string[], 62 | ): any => { 63 | const stylesheetExtension = extname(stylesheetPath); 64 | 65 | const extensions = Array.isArray(configExtensions) 66 | ? configExtensions 67 | : defaultExtensions; 68 | 69 | if (extensions.indexOf(stylesheetExtension) !== -1) { 70 | launchServer(); 71 | const requiringFile = file.opts.filename; 72 | const cssFile = resolve(dirname(requiringFile), stylesheetPath); 73 | const data = JSON.stringify({ cssFile, config }); 74 | const execArgs = [clientExcutable, socketPath, data]; 75 | const result = execFileSync(nodeExecutable, execArgs, { 76 | env: process.env, // eslint-disable-line no-process-env 77 | }).toString(); 78 | 79 | return JSON.parse(result || '{}'); 80 | } 81 | 82 | return undefined; 83 | }; 84 | 85 | export default function transformPostCSS({ types: t }: any): any { 86 | 87 | return { 88 | visitor: { 89 | CallExpression(path: any, { file }: any) { 90 | const { callee: { name: calleeName }, arguments: args } = path.node; 91 | 92 | if (calleeName !== 'require' || 93 | !args.length || 94 | !t.isStringLiteral(args[0])) { 95 | return; 96 | } 97 | 98 | const [{ value: stylesheetPath }] = args; 99 | const { config, extensions } = this.opts; 100 | const tokens = getStylesFromStylesheet( 101 | stylesheetPath, 102 | file, 103 | config, 104 | extensions 105 | ); 106 | 107 | if (tokens !== undefined) { 108 | const expression = path.findParent((test) => ( 109 | test.isVariableDeclaration() || 110 | test.isExpressionStatement() 111 | )); 112 | 113 | expression.addComment( 114 | 'trailing', ` @related-file ${stylesheetPath}`, true 115 | ); 116 | 117 | path.replaceWith(t.objectExpression( 118 | Object.keys(tokens).map( 119 | (token) => t.objectProperty( 120 | t.stringLiteral(token), 121 | t.stringLiteral(tokens[token]) 122 | ) 123 | ) 124 | )); 125 | } 126 | }, 127 | ImportDeclaration(path: any, { file }: any) { 128 | const stylesheetPath = path.node.source.value; 129 | 130 | if (path.node.specifiers.length !== 1) { 131 | return; 132 | } 133 | 134 | const { config, extensions } = this.opts; 135 | const tokens = getStylesFromStylesheet( 136 | stylesheetPath, 137 | file, 138 | config, 139 | extensions 140 | ); 141 | 142 | if (tokens) { 143 | const styles = t.objectExpression( 144 | Object.keys(tokens).map( 145 | (token) => t.objectProperty( 146 | t.stringLiteral(token), 147 | t.stringLiteral(tokens[token]) 148 | ) 149 | ) 150 | ); 151 | /* eslint-disable new-cap */ 152 | 153 | const variableDeclaration = t.VariableDeclaration('var', 154 | [t.VariableDeclarator(path.node.specifiers[0].local, styles)]); 155 | 156 | /* eslint-enable new-cap */ 157 | path.addComment('trailing', ` @related-file ${stylesheetPath}`, true); 158 | path.replaceWith(variableDeclaration); 159 | } 160 | }, 161 | }, 162 | }; 163 | } 164 | 165 | export { 166 | startServer, 167 | stopServer, 168 | }; 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Babel PostCSS Transform 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Coverage Status][coverage-image]][coverage-url] 6 | [![Dependencies][david-image]][david-url] 7 | [![devDependencies][david-dev-image]][david-dev-url] 8 | 9 | A [Babel][babel] plugin to process CSS files via [PostCSS][postcss]. 10 | 11 | Using [PostCSS Modules][postcss-modules], it can transform: 12 | 13 | ```js 14 | import styles from './styles'; 15 | ``` 16 | 17 | ```css 18 | .example { color: cyan; } 19 | ``` 20 | 21 | Into an object that has properties mirroring the style names: 22 | 23 | ```js 24 | var styles = {"example":"_example_amfqe_1"}; 25 | ``` 26 | 27 | ## Configuration 28 | 29 | Install the transform as well as `postcss` and any PostCSS plugins you want to 30 | use: 31 | 32 | ```bash 33 | npm install --save-dev \ 34 | babel-plugin-transform-postcss \ 35 | postcss \ 36 | postcss-modules 37 | ``` 38 | 39 | Add the transform to your babel configuration, i.e. `.babelrc`: 40 | 41 | ```json 42 | { 43 | "presets": [ 44 | ["env", { "targets": { "node": "current" }}] 45 | ], 46 | "plugins": [ 47 | "transform-postcss" 48 | ] 49 | } 50 | ``` 51 | 52 | Create a [`postcss.config.js`][postcss-load-config]: 53 | 54 | ```js 55 | module.exports = (ctx) => ({ 56 | plugins: [ 57 | require('postcss-modules')({ 58 | getJSON: ctx.extractModules || (() => {}), 59 | }), 60 | ], 61 | }); 62 | ``` 63 | 64 | You can also specify a location to load your `postcss.config.js` from in the options in your Babel configuration, i.e. `.babelrc`: 65 | ```json 66 | { 67 | "plugins": [ 68 | ["transform-postcss", { 69 | "config": "configuration/postcss.config.js" 70 | }] 71 | ] 72 | } 73 | ``` 74 | 75 | By default we look for `.css` files, but you can also specify the extensions we should look for: 76 | ```json 77 | { 78 | "plugins": [ 79 | ["transform-postcss", { 80 | "config": "configuration/postcss.config.js", 81 | "extensions": [".scss"] 82 | }] 83 | ] 84 | } 85 | ``` 86 | 87 | 88 | ## Details 89 | 90 | The transform will transform all imports & require statements that have a `.css` 91 | extension and run them through `postcss`. To determine the PostCSS config, it 92 | uses [`postcss-load-config`][postcss-load-config] with 93 | [additional context values](#postcss-load-config-context). One of those config 94 | values, [`extractModules`](#extractmodules_-any-modules-object) should be 95 | invoked in order to define the value of the resulting import. 96 | 97 | No CSS is actually included in the resulting JavaScript. It is expected that you 98 | transform your CSS using the same `postcss.config.js` file as the one used by 99 | this transform. We recommend: 100 | 101 | - [`postcss-cli`][postcss-cli] (v3 or later) 102 | - [`gulp-postcsssrc`][gulp-postcssrc] 103 | 104 | Finally, it's worth noting that this transform also adds a comment to the 105 | generated code indicating the related CSS file so that it can be processed by 106 | other tools, i.e. [`relateify`][relateify]. 107 | 108 | ### PostCSS Load Config Context 109 | 110 | #### `extractModules(_: any, modules: object)` 111 | 112 | This option is a function that may be passed directly on to 113 | [`postcss-modules`][postcss-modules] as the [`getJSON` 114 | argument][postcss-modules-get-json]. Other uses, while unlikely, are 115 | permittable, as well. 116 | 117 | The function accepts two arguments. The transform uses only the 118 | second value passed to the function. That value is the object value that 119 | replaces the `import`/`require`. 120 | 121 | ## Using with Browserify & Watchify 122 | 123 | This will work well with the [`babelify`][babelify] transform, but if you're 124 | using [`watchify`][watchify], you will want to add the [`relateify`][relateify] 125 | transform in order to ensure that changes to CSS files rebuild the appropriate 126 | JS files. 127 | 128 | ## Caching 129 | 130 | This module caches the results of the compilation of CSS files and stores the 131 | cache in a directory under `/tmp/bptp-UNIQUE_ID`. The cache is only invalidated 132 | when the CSS file contents change and not when the `postcss.config.js` file 133 | changes (due to limitations at the time of implementation). Try removing the 134 | cache if you're not seeing expected changes. 135 | 136 | ## Prior Art 137 | 138 | This plugin is based of the work of: 139 | 140 | - [`css-modules-transform`][css-modules-transform] 141 | - [`css-modules-require-hook`][css-modules-require-hook] 142 | 143 | Unlike the above, it supports both synchronous and asynchronous PostCSS plugins. 144 | 145 | ## License 146 | 147 | This project is distributed under the MIT license. 148 | 149 | [babel]: https://babeljs.io/ 150 | [postcss]: http://postcss.org/ 151 | [postcss-cli]: https://github.com/postcss/postcss-cli 152 | [postcss-modules]: https://github.com/css-modules/postcss-modules 153 | [postcss-modules-get-json]: https://github.com/css-modules/postcss-modules#saving-exported-classes 154 | [postcss-load-config]: https://github.com/michael-ciniawsky/postcss-load-config 155 | [css-modules-transform]: https://github.com/michalkvasnicak/babel-plugin-css-modules-transform 156 | [css-modules-require-hook]: https://github.com/css-modules/css-modules-require-hook 157 | [gulp-postcssrc]: https://github.com/michael-ciniawsky/gulp-postcssrc 158 | [babelify]: https://github.com/babel/babelify 159 | [watchify]: https://github.com/substack/watchify 160 | [relateify]: https://github.com/wbyoung/relateify 161 | 162 | [travis-image]: http://img.shields.io/travis/wbyoung/babel-plugin-transform-postcss.svg?style=flat 163 | [travis-url]: http://travis-ci.org/wbyoung/babel-plugin-transform-postcss 164 | [npm-image]: http://img.shields.io/npm/v/babel-plugin-transform-postcss.svg?style=flat 165 | [npm-url]: https://npmjs.org/package/babel-plugin-transform-postcss 166 | [coverage-image]: http://img.shields.io/coveralls/wbyoung/babel-plugin-transform-postcss.svg?style=flat 167 | [coverage-url]: https://coveralls.io/r/wbyoung/babel-plugin-transform-postcss 168 | [david-image]: http://img.shields.io/david/wbyoung/babel-plugin-transform-postcss.svg?style=flat 169 | [david-url]: https://david-dm.org/wbyoung/babel-plugin-transform-postcss 170 | [david-dev-image]: http://img.shields.io/david/dev/wbyoung/babel-plugin-transform-postcss.svg?style=flat 171 | [david-dev-url]: https://david-dm.org/wbyoung/babel-plugin-transform-postcss#info=devDependencies 172 | -------------------------------------------------------------------------------- /test/plugin.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-sync */ 3 | 4 | import { 5 | describe, 6 | it, 7 | beforeEach, 8 | afterEach, 9 | } from 'mocha'; 10 | 11 | import { 12 | read, 13 | transform, 14 | babelNoModules, 15 | } from './helpers'; 16 | 17 | import { 18 | startServer, 19 | stopServer, 20 | } from '../src/plugin'; 21 | 22 | import { expect } from 'chai'; 23 | import { stub } from 'sinon'; 24 | import childProcess from 'child_process'; 25 | 26 | describe('babel-plugin-transform-postcss', () => { 27 | beforeEach(() => { 28 | stub(childProcess, 'spawn').returns({ 29 | unref: stub(), 30 | kill: stub(), 31 | }); 32 | }); 33 | afterEach(() => childProcess.spawn.restore()); 34 | 35 | beforeEach(() => { 36 | stub(childProcess, 'execFileSync').returns( 37 | new Buffer('{"simple":"_simple_jvai8_1"}') 38 | ); 39 | }); 40 | afterEach(() => childProcess.execFileSync.restore()); 41 | 42 | afterEach(() => stopServer()); 43 | 44 | const testServerLaunched = () => { 45 | expect(childProcess.spawn).to.have.been.calledOnce; 46 | 47 | const [executable, args, opts] = childProcess.spawn.getCall(0).args; 48 | const [jsExecutable, socketPath, socketTmp] = args; 49 | 50 | expect(executable).to.match(/node$/); 51 | expect(args.length).to.eql(3); 52 | expect(jsExecutable).to.endWith('/postcss-server.js'); 53 | expect(socketPath).to.match(/^\/tmp.*\.sock$/); 54 | expect(socketTmp).to.match(/^\/tmp/); 55 | expect(opts).to.eql({ 56 | env: process.env, // eslint-disable-line no-process-env 57 | stdio: 'inherit', 58 | }); 59 | }; 60 | 61 | const testClientLaunched = (filename: string) => { 62 | expect(childProcess.execFileSync).to.have.been.calledOnce; 63 | 64 | const [executable, args, opts] = childProcess.execFileSync.getCall(0).args; 65 | const [jsExecutable, socketPath, jsonString] = args; 66 | const json = JSON.parse(jsonString); 67 | const { cssFile } = json; 68 | 69 | expect(executable).to.match(/node$/); 70 | expect(args.length).to.eql(3); 71 | expect(jsExecutable).to.endWith('/postcss-client.js'); 72 | expect(socketPath).to.match(/^\/tmp.*\.sock$/); 73 | expect(cssFile).to.endWith(`/${filename}`); 74 | expect(json).to.have.keys('cssFile', 'config'); 75 | expect(opts).to.eql({ 76 | env: process.env, // eslint-disable-line no-process-env 77 | }); 78 | }; 79 | 80 | const shouldBehaveLikeSeverIsRunning = () => { 81 | describe('when transforming require.js', () => { 82 | let initialSpwawnCount; 83 | 84 | beforeEach(() => { initialSpwawnCount = childProcess.spawn.callCount; }); 85 | beforeEach(() => transform('require.js')); 86 | 87 | it('does not launch the server again', () => { 88 | expect(childProcess.spawn.callCount).to.eql(initialSpwawnCount); 89 | }); 90 | }); 91 | }; 92 | 93 | describe('when transforming require.js', () => { 94 | let result; 95 | 96 | beforeEach(async() => { result = await transform('require.js'); }); 97 | 98 | it('launches the server', testServerLaunched); 99 | it('launches a client', () => testClientLaunched('simple.css')); 100 | it('compiles correctly', async() => { 101 | expect(result).to.eql((await read('require.expected.js')).trim()); 102 | }); 103 | 104 | shouldBehaveLikeSeverIsRunning(); 105 | }); 106 | 107 | describe('when transforming import.js', () => { 108 | let result; 109 | 110 | beforeEach(async() => { result = await transform('import.js'); }); 111 | 112 | it('launches the server', testServerLaunched); 113 | it('launches a client', () => testClientLaunched('simple.css')); 114 | it('compiles correctly', async() => { 115 | expect(result).to.eql((await read('import.expected.js')).trim()); 116 | }); 117 | 118 | shouldBehaveLikeSeverIsRunning(); 119 | }); 120 | 121 | describe('when transforming import.scss.js', () => { 122 | let result; 123 | 124 | beforeEach(async() => { 125 | result = await transform( 126 | 'import.scss.js', 127 | null, 128 | ['.scss'] 129 | ); 130 | }); 131 | 132 | it('launches the server', testServerLaunched); 133 | it('launches a client', () => testClientLaunched('simple.scss')); 134 | it('compiles correctly', async() => { 135 | expect(result).to.eql((await read('import.scss.expected.js')).trim()); 136 | }); 137 | 138 | shouldBehaveLikeSeverIsRunning(); 139 | }); 140 | 141 | describe('when transforming nocss.js', () => { 142 | beforeEach(() => transform('nocss.js')); 143 | 144 | it('does not launch the server', () => { 145 | expect(childProcess.spawn).to.not.have.been.called; 146 | }); 147 | 148 | it('does not launch a client', () => { 149 | expect(childProcess.execFileSync).to.not.have.been.called; 150 | }); 151 | }); 152 | 153 | describe('when transforming import.no.name.js', () => { 154 | beforeEach(() => transform('import.no.name.js', babelNoModules)); 155 | 156 | it('does not launch the server', () => { 157 | expect(childProcess.spawn).to.not.have.been.called; 158 | }); 159 | 160 | it('does not launch a client', () => { 161 | expect(childProcess.execFileSync).to.not.have.been.called; 162 | }); 163 | }); 164 | 165 | describe('when transforming import.nocss.js', () => { 166 | beforeEach(() => transform('import.nocss.js', babelNoModules)); 167 | 168 | it('does not launch the server', () => { 169 | expect(childProcess.spawn).to.not.have.been.called; 170 | }); 171 | 172 | it('does not launch a client', () => { 173 | expect(childProcess.execFileSync).to.not.have.been.called; 174 | }); 175 | }); 176 | 177 | describe('when transforming import.js without modules', () => { 178 | let result; 179 | 180 | beforeEach(async() => { 181 | result = await transform('import.js', babelNoModules); 182 | }); 183 | 184 | it('launches the server', testServerLaunched); 185 | it('launches a client', () => testClientLaunched('simple.css')); 186 | it('compiles correctly', async() => { 187 | expect(result).to.eql( 188 | (await read('import.no.modules.expected.js')).trim() 189 | ); 190 | }); 191 | 192 | shouldBehaveLikeSeverIsRunning(); 193 | }); 194 | 195 | describe('when the server has been started started', () => { 196 | beforeEach(() => startServer()); 197 | shouldBehaveLikeSeverIsRunning(); 198 | }); 199 | 200 | describe('when transforming require.js & the client returns no data', () => { 201 | let result; 202 | 203 | beforeEach(() => childProcess.execFileSync.returns(new Buffer(''))); 204 | beforeEach(async() => { result = await transform('require.js'); }); 205 | 206 | it('compiles correctly', async() => { 207 | expect(result).to.eql((await read('require.expected.empty.js')).trim()); 208 | }); 209 | }); 210 | 211 | }); 212 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | 4 | "parser": "babel-eslint", 5 | 6 | "plugins": [ 7 | "flowtype" 8 | ], 9 | 10 | "env": { 11 | "es6": true, 12 | "node": true 13 | }, 14 | 15 | "extends": "eslint:recommended", 16 | 17 | "rules": { 18 | 19 | /* Possible Errors */ 20 | 21 | "no-await-in-loop": "error", 22 | "no-empty": ["error", { "allowEmptyCatch": true }], 23 | "no-prototype-builtins": "error", 24 | "no-template-curly-in-string": "error", 25 | "no-unsafe-negation": "error", 26 | 27 | /* Best Practices */ 28 | 29 | "accessor-pairs": "error", 30 | "array-callback-return": "error", 31 | "block-scoped-var": "error", 32 | "consistent-return": "error", 33 | "curly": ["error", "all"], 34 | "dot-location": ["error", "property"], 35 | "dot-notation": ["error", { "allowPattern": "^[a-z]+(_[a-z]+)+$" }], 36 | "eqeqeq": "error", 37 | "guard-for-in": "error", 38 | "no-alert": "error", 39 | "no-caller": "error", 40 | "no-empty-function": "error", 41 | "no-eval": "error", 42 | "no-extend-native": "error", 43 | "no-extra-bind": "error", 44 | "no-floating-decimal": "error", 45 | "no-global-assign": "error", 46 | "no-implied-eval": "error", 47 | "no-iterator": "error", 48 | "no-labels": "error", 49 | "no-lone-blocks": "error", 50 | "no-loop-func": "error", 51 | "no-multi-spaces": "error", 52 | "no-multi-str": "error", 53 | "no-new-func": "error", 54 | "no-new-wrappers": "error", 55 | "no-new": "error", 56 | "no-octal-escape": "error", 57 | "no-proto": "error", 58 | "no-return-assign": "error", 59 | "no-return-await": "error", 60 | "no-script-url": "error", 61 | "no-self-compare": "error", 62 | "no-sequences": "error", 63 | "no-throw-literal": "error", 64 | "no-unmodified-loop-condition": "error", 65 | "no-unused-expressions": "error", 66 | "no-useless-call": "error", 67 | "no-useless-concat": "error", 68 | "no-useless-escape": "error", 69 | "no-useless-return": "error", 70 | "no-void": "error", 71 | "no-warning-comments": "error", 72 | "no-with": "error", 73 | "radix": ["error", "as-needed"], 74 | "require-await": "error", 75 | "wrap-iife": "error", 76 | "yoda": ["error", "never"], 77 | 78 | /* Variables */ 79 | 80 | "no-shadow-restricted-names": "error", 81 | "no-shadow": "error", 82 | "no-undef-init": "error", 83 | "no-use-before-define": "error", 84 | 85 | /* Node.js and CommonJS */ 86 | 87 | "global-require": "error", 88 | "handle-callback-err": "error", 89 | "no-path-concat": "error", 90 | "no-process-env": "error", 91 | "no-sync": "error", 92 | 93 | /* Stylistic Issues */ 94 | 95 | "array-bracket-spacing": ["error", "never", { 96 | "arraysInArrays": true, 97 | }], 98 | "block-spacing": "error", 99 | "brace-style": ["error", "stroustrup", { "allowSingleLine": true }], 100 | "camelcase": ["error", { "properties": "always" }], 101 | "comma-dangle": ["error", "always-multiline"], 102 | "comma-spacing": ["error", { "after": true, "before": false }], 103 | "comma-style": ["error", "last"], 104 | "computed-property-spacing": "error", 105 | "consistent-this": ["error", "self"], 106 | "eol-last": "error", 107 | "func-call-spacing": "error", 108 | "func-name-matching": "error", 109 | "func-names": ["error", "as-needed"], 110 | "func-style": ["error", "expression"], 111 | "id-length": ["error", { 112 | "min": 3, 113 | "exceptions": ["_", "fs", "cb", "i"], 114 | }], 115 | "indent": ["error", 2, { "SwitchCase": 1 }], 116 | "key-spacing": ["error", { "beforeColon": false, "afterColon": true }], 117 | "keyword-spacing": "error", 118 | "linebreak-style": ["error", "unix"], 119 | "lines-around-comment": ["error", { "beforeLineComment": true }], 120 | "max-len": ["error", 80], 121 | "max-lines": ["error", 1000], 122 | "new-cap": "error", 123 | "new-parens": "error", 124 | "newline-after-var": ["error", "always"], 125 | "newline-before-return": "error", 126 | "no-array-constructor": "error", 127 | "no-lonely-if": "error", 128 | "no-mixed-spaces-and-tabs": "error", 129 | "no-multiple-empty-lines": "error", 130 | "no-nested-ternary": "error", 131 | "no-new-object": "error", 132 | "no-trailing-spaces": "error", 133 | "no-unneeded-ternary": "error", 134 | "no-whitespace-before-property": "error", 135 | "object-curly-spacing": ["error", "always"], 136 | "one-var": ["error", { "uninitialized": "always", "initialized": "never" }], 137 | "operator-linebreak": "error", 138 | "quote-props": ["error", "consistent-as-needed"], 139 | "quotes": ["error", "single", { "avoidEscape": true }], 140 | "semi": ["error", "always"], 141 | "semi-spacing": ["error", { "before": false, "after": true }], 142 | "space-before-blocks": ["error", "always"], 143 | "space-before-function-paren": ["error", "never"], 144 | "space-in-parens": ["error", "never"], 145 | "space-infix-ops": "error", 146 | "space-unary-ops": ["error", { "words": false, "nonwords": false }], 147 | "spaced-comment": ["error", "always"], 148 | 149 | /* ECMAScript 6 */ 150 | 151 | "arrow-body-style": ["error", "as-needed"], 152 | "arrow-parens": "error", 153 | "arrow-spacing": "error", 154 | "generator-star-spacing": "error", 155 | "no-confusing-arrow": ["error", { "allowParens": true }], 156 | "no-useless-computed-key": "error", 157 | "no-useless-constructor": "error", 158 | "no-useless-rename": "error", 159 | "no-var": "error", 160 | "object-shorthand": "error", 161 | "prefer-arrow-callback": "error", 162 | "prefer-const": "error", 163 | "prefer-numeric-literals": "error", 164 | "prefer-rest-params": "error", 165 | "prefer-spread": "error", 166 | "prefer-template": "error", 167 | "rest-spread-spacing": "error", 168 | "symbol-description": "error", 169 | "template-curly-spacing": "error", 170 | "yield-star-spacing": "error", 171 | 172 | /* Flow */ 173 | 174 | "flowtype/boolean-style": ["error", "bool"], 175 | "flowtype/define-flow-type": "error", 176 | "flowtype/delimiter-dangle": ["error", "always-multiline"], 177 | "flowtype/generic-spacing": ["error", "never"], 178 | "flowtype/no-dupe-keys": "error", 179 | "flowtype/no-primitive-constructor-types": "error", 180 | "flowtype/no-weak-types": ["error", {"any": false}], 181 | "flowtype/object-type-delimiter": ["error", "comma"], 182 | "flowtype/require-parameter-type": ["error", { "excludeArrowFunctions": "expressionsOnly", "excludeParameterMatch": "^_" }], 183 | "flowtype/require-return-type": ["error", "always", { "excludeArrowFunctions": "expressionsOnly" }], 184 | "flowtype/require-valid-file-annotation": ["error", "always", {"annotationStyle": "block"}], 185 | "flowtype/semi": "error", 186 | "flowtype/space-after-type-colon": ["error", "always"], 187 | "flowtype/space-before-generic-bracket": ["error", "never"], 188 | "flowtype/space-before-type-colon": ["error", "never"], 189 | "flowtype/union-intersection-spacing": ["error", "always"], 190 | "flowtype/use-flow-type": "error", 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /test/postcss-server.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-sync */ 3 | 4 | import { 5 | describe, 6 | it, 7 | beforeEach, 8 | afterEach, 9 | } from 'mocha'; 10 | 11 | import { 12 | main, 13 | _streams as streams, 14 | } from '../src/postcss-server'; 15 | 16 | import { 17 | stub, 18 | } from 'sinon'; 19 | 20 | import { expect } from 'chai'; 21 | 22 | import { join } from 'path'; 23 | import path from 'path'; 24 | import fs from 'fs'; 25 | import net from 'net'; 26 | import { Server } from 'net'; 27 | 28 | const testSocket = join(__dirname, 'tmp.sock'); 29 | const testOutput = join(__dirname, 'tmp.out'); 30 | const testTmp = join(__dirname, 'tmp'); 31 | 32 | describe('postcss-server', () => { 33 | let server, originalStderr; 34 | const invokeMain = async() => { server = await main(testSocket, testTmp); }; 35 | const closeServer = async() => { 36 | await new Promise((resolve: () => void, reject: (Error) => void) => { 37 | server.close((err: ?Error) => { 38 | if (err) { reject(err); } 39 | else { resolve(); } 40 | }); 41 | }); 42 | }; 43 | const closeStderr = async() => { 44 | const write = new Promise((resolve: () => void) => { 45 | streams.stderr.on('finish', () => resolve()); 46 | }); 47 | 48 | streams.stderr.end(); 49 | await write; 50 | }; 51 | 52 | beforeEach(() => { 53 | originalStderr = streams.stderr; 54 | streams.stderr = fs.createWriteStream(testOutput); 55 | fs.mkdirSync(testTmp); 56 | }); 57 | afterEach(() => { 58 | streams.stderr = originalStderr; 59 | fs.unlinkSync(testOutput); 60 | 61 | for (const file of fs.readdirSync(testTmp)) { 62 | fs.unlinkSync(path.join(testTmp, file)); 63 | } 64 | 65 | fs.rmdirSync(testTmp); 66 | }); 67 | 68 | describe('main(...testArgs)', () => { 69 | let signintHandlers; 70 | 71 | beforeEach(() => { signintHandlers = process.listeners('SIGINT'); }); 72 | 73 | beforeEach(invokeMain); 74 | afterEach(closeServer); 75 | 76 | it('starts a server', () => { 77 | expect(server.address()).to.eql(testSocket); 78 | }); 79 | 80 | it('observes SIGINT to cleanup server socket', () => { 81 | const newHandlers = process.listeners('SIGINT') 82 | .slice(signintHandlers.length); 83 | 84 | expect(newHandlers.length).to.eql(1); 85 | newHandlers[0](); 86 | 87 | expect(fs.existsSync(testSocket)).to.be.false; 88 | }); 89 | 90 | const simpleCSSFile = join(__dirname, 'fixtures', 'simple.css'); 91 | const sendMessage = async( 92 | json: { 93 | cssFile: string, 94 | } 95 | ): Promise => { 96 | let response = ''; 97 | 98 | await new Promise((resolve: () => void, reject: (Error) => void) => { 99 | const client = net.connect(testSocket, () => { 100 | client.end(JSON.stringify(json)); 101 | client.on('data', (chunk: Buffer) => { 102 | response += chunk.toString('utf8'); 103 | }); 104 | }); 105 | 106 | client.on('error', (err: ErrnoError) => reject(err)); 107 | client.on('close', (err: ?Error) => { 108 | if (err) { reject(err); } 109 | else { resolve(); } 110 | }); 111 | }); 112 | 113 | return response; 114 | }; 115 | 116 | it('accepts JSON details and extracts PostCSS modules', async() => { 117 | const response = await sendMessage({ 118 | cssFile: simpleCSSFile, 119 | }); 120 | 121 | expect(JSON.parse(response)).to.eql({ simple: '_simple_jvai8_1' }); 122 | }); 123 | 124 | it('fails gracefully for invalid CSS', async() => { 125 | const response = await sendMessage({ 126 | cssFile: join(__dirname, 'fixtures', 'invalid.css'), 127 | }); 128 | 129 | expect(response).to.eql(''); 130 | }); 131 | 132 | describe('with a cached result', () => { 133 | beforeEach(() => { 134 | const name = simpleCSSFile.replace(/[^a-z]/ig, ''); 135 | 136 | fs.writeFileSync(path.join(testTmp, `${name}.cache`), JSON.stringify({ 137 | hash: 'e773de66362a5c384076b75ac292038b', 138 | tokens: { simple: '_simple_cached' }, 139 | })); 140 | }); 141 | 142 | it('accepts JSON details and extracts PostCSS modules', async() => { 143 | const response = await sendMessage({ 144 | cssFile: simpleCSSFile, 145 | }); 146 | 147 | expect(JSON.parse(response)).to.eql({ simple: '_simple_cached' }); 148 | }); 149 | }); 150 | 151 | describe('with an invalid cache', () => { 152 | let response; 153 | 154 | beforeEach(() => { 155 | const name = simpleCSSFile.replace(/[^a-z]/ig, ''); 156 | 157 | fs.writeFileSync(path.join(testTmp, `${name}.cache`), 'not-json'); 158 | }); 159 | beforeEach(async() => { 160 | response = await sendMessage({ 161 | cssFile: simpleCSSFile, 162 | }); 163 | }); 164 | beforeEach(closeStderr); 165 | 166 | it('does not contain a response', () => { 167 | expect(response).to.eql(''); 168 | }); 169 | 170 | it('logs a useful message', () => { 171 | expect(fs.readFileSync(testOutput, 'utf8')) 172 | .to.match(/JSON/i); 173 | }); 174 | }); 175 | 176 | describe('with a missing CSS file', () => { 177 | let response; 178 | 179 | beforeEach(async() => { 180 | response = await sendMessage({ 181 | cssFile: join(__dirname, 'fixtures', 'nofile'), 182 | }); 183 | }); 184 | beforeEach(closeStderr); 185 | 186 | it('does not contain a response', () => { 187 | expect(response).to.eql(''); 188 | }); 189 | 190 | it('logs a useful message', () => { 191 | expect(fs.readFileSync(testOutput, 'utf8')) 192 | .to.match(/no such file/i); 193 | }); 194 | }); 195 | 196 | describe('with a missing config file', () => { 197 | let response; 198 | 199 | beforeEach(() => stub(path, 'dirname').callsFake(() => process.cwd())); 200 | afterEach(() => path.dirname.restore()); 201 | 202 | beforeEach(async() => { 203 | response = await sendMessage({ 204 | cssFile: join(__dirname, 'fixtures', 'simple.css'), 205 | config: join('fixtures', 'nofile'), 206 | }); 207 | }); 208 | beforeEach(closeStderr); 209 | 210 | it('does not contain a response', () => { 211 | expect(response).to.eql(''); 212 | }); 213 | 214 | it('logs a useful message', () => { 215 | expect(fs.readFileSync(testOutput, 'utf8')) 216 | .to.match(/No PostCSS Config/i); 217 | }); 218 | }); 219 | }); 220 | 221 | describe('when listen fails', () => { 222 | beforeEach(() => { 223 | stub(Server.prototype, 'listen').callsFake(function errorHandler() { 224 | this.emit('error', new Error('test failure')); 225 | }); 226 | }); 227 | afterEach(() => { Server.prototype.listen.restore(); }); 228 | 229 | it('fails to complete main(...testArgs)', async() => { 230 | let error; 231 | 232 | try { await invokeMain(); } 233 | catch (err) { error = err; } 234 | expect(error).to.match(/test failure/); 235 | }); 236 | }); 237 | 238 | describe('when the server socket already exists', () => { 239 | beforeEach(() => { stub(process, 'exit'); }); 240 | afterEach(() => { process.exit.restore(); }); 241 | 242 | beforeEach(() => { fs.writeFileSync(testSocket, ''); }); 243 | afterEach(() => { fs.unlinkSync(testSocket); }); 244 | 245 | describe('main(...testArgs)', () => { 246 | beforeEach(invokeMain); 247 | beforeEach(closeStderr); 248 | 249 | it('exits', () => { 250 | expect(process.exit).to.have.been.calledOnce; 251 | expect(process.exit).to.have.been.calledWith(1); 252 | }); 253 | 254 | it('logs a useful message', () => { 255 | expect(fs.readFileSync(testOutput, 'utf8')) 256 | .to.match(/already running/i); 257 | }); 258 | }); 259 | 260 | describe('when stderr is a TTY', () => { 261 | beforeEach(() => { (streams.stderr: any).isTTY = true; }); 262 | 263 | describe('main(...testArgs)', () => { 264 | beforeEach(invokeMain); 265 | beforeEach(closeStderr); 266 | 267 | it('logs with color', () => { 268 | expect(fs.readFileSync(testOutput, 'utf8')).to.startWith('\x1b[31m'); 269 | }); 270 | }); 271 | }); 272 | 273 | }); 274 | 275 | describe('when making a directory fails', () => { 276 | beforeEach(() => { stub(fs, 'mkdirSync').throws('Error with no code'); }); 277 | afterEach(() => { fs.mkdirSync.restore(); }); 278 | 279 | it('errors when invoking main', async() => { 280 | let error; 281 | 282 | try { await invokeMain(); } 283 | catch (err) { error = err; } 284 | 285 | expect(error).to.match(/Error with no code/); 286 | }); 287 | }); 288 | 289 | }); 290 | --------------------------------------------------------------------------------