Skip to content

Instantly share code, notes, and snippets.

@cullylarson
Last active January 20, 2021 22:24
Show Gist options
  • Save cullylarson/9913a31d145e2d263a009d2edd41762b to your computer and use it in GitHub Desktop.
Save cullylarson/9913a31d145e2d263a009d2edd41762b to your computer and use it in GitHub Desktop.
A solution to slow transforms in styled-jsx-plugin-postcss.
// Most of this is copied from: https://github.com/wbyoung/babel-plugin-transform-postcss
const {spawnSync, spawn} = require('child_process')
const path = require('path')
const socketPath = path.join('/tmp', 'styled-jsx-plugin-postcss--' + process.pid + '.sock')
const nodeExecutable = process.argv[0]
const clientExcutable = path.join(__dirname, 'client.js')
const serverExcutable = path.join(__dirname, 'server.js')
let server
const startServer = () => {
server = spawn(nodeExecutable, [serverExcutable, socketPath], {
env: process.env,
stdio: 'inherit',
})
server.unref()
}
const stopServer = () => {
if(!server) return
server.kill()
server = null
process.removeListener('exit', stopServer)
}
const launchServer = () => {
if(server) {
return
}
startServer()
process.on('exit', stopServer)
}
module.exports = (css, options) => {
launchServer()
const result = spawnSync(nodeExecutable, [clientExcutable], {
input: JSON.stringify({
socketPath,
css,
options,
}),
encoding: 'utf8',
})
if(result.status !== 0) {
throw new Error(`PostCSS failed with: ${result.stderr}`)
}
return result.stdout
}
const net = require('net')
// exponential backoff, roughly 100ms-6s
const retries = [1, 2, 3, 4, 5].map((num) => Math.exp(num) * 40)
const streams = {stdout: process.stdout} // overwritable by tests
// writes result to stdout
const communicate = async (socketPath, css, options) => {
return new Promise((resolve, reject) => {
const client = net.connect(socketPath, () => {
client.end(JSON.stringify({css, options}))
client.pipe(streams.stdout)
})
client.on('error', (err) => reject(err))
client.on('close', (err) => {
if(err) reject(err)
else resolve()
})
})
}
const processCss = async (socketPath, css, options) => {
try {
await communicate(socketPath, css, options)
}
catch(err) {
const recoverable = err.code === 'ECONNREFUSED' || err.code === 'ENOENT'
if(recoverable && retries.length) {
await new Promise((resolve, reject) => {
setTimeout(() => {
processCss(socketPath, css, options)
.then(resolve, reject)
}, retries.shift())
})
}
else {
throw err
}
}
}
let input = ''
process.stdin.on('data', (data) => {
input += data.toString()
})
process.stdin.on('end', () => {
const inputData = JSON.parse(input)
processCss(inputData.socketPath, inputData.css, inputData.options)
.catch((err) => {
// NOTE: we console.erorr(err) and then process.exit(1) instead of throwing the error
// to avoid the UnhandledPromiseRejectionWarning message.
console.error(err)
process.exit(1)
})
})
const fs = require('fs')
const net = require('net')
const postcss = require('postcss')
const loadConfig = require('postcss-load-config')
let plugins
let processor
const processCss = async (css, options) => {
const cssWithPlaceholders = css.replace(
/%%styled-jsx-placeholder-(\d+)%%/g,
(_, id) => `/*%%styled-jsx-placeholder-${id}%%*/`,
)
if(!plugins) {
plugins = loadConfig.sync(options.env || process.env, options.path, {argv: false}).plugins
}
if(!processor) {
processor = postcss(plugins)
}
const resultFull = await processor.process(cssWithPlaceholders, {from: false})
const result = resultFull.css
return result.replace(
/\/\*%%styled-jsx-placeholder-(\d+)%%\*\//g,
(_, id) => `%%styled-jsx-placeholder-${id}%%`,
)
}
const startServer = async (socketPath) => {
const serverOptions = {allowHalfOpen: true}
const server = net.createServer(serverOptions, (connection) => {
let data = ''
connection.on('data', (chunk) => {
data += chunk.toString('utf8')
})
connection.on('end', async () => {
try {
const {css, options} = JSON.parse(data)
const result = await processCss(css, options)
connection.end(result)
}
catch (err) {
console.error(err)
connection.end()
}
})
})
if(fs.existsSync(socketPath)) {
console.error(`Server already running on socket ${socketPath}`)
process.exit(1)
}
await new Promise((resolve, reject) => {
server.on('error', (err) => reject(err))
server.on('listening', () => {
const exitHandler = ({
cleanup = false,
exit = false,
}) => {
if(cleanup) {
try {
fs.unlinkSync(socketPath)
}
catch(err) {
// this handler may be called multiple times, so
// if the file was already removed, don't throw
// an error
if(err.code !== 'ENOENT') {
throw err
}
}
}
if(exit) {
process.exit()
}
}
server.on('close', () => {
process.removeListener('exit', exitHandler)
process.removeListener('SIGINT', exitHandler)
process.removeListener('SIGTERM', exitHandler)
process.removeListener('SIGUSR1', exitHandler)
process.removeListener('SIGUSR2', exitHandler)
process.removeListener('uncaughtException', exitHandler)
})
process.on('exit', exitHandler.bind(null, {cleanup: true}))
process.on('SIGINT', exitHandler.bind(null, {exit: true}))
process.on('SIGTERM', exitHandler.bind(null, {exit: true}))
// catches "kill pid" (for example: nodemon restart)
process.on('SIGUSR1', exitHandler.bind(null, {exit: true}))
process.on('SIGUSR2', exitHandler.bind(null, {exit: true}))
process.on('uncaughtException', exitHandler.bind(null, {exit: true}))
resolve()
})
server.listen(socketPath)
})
return server
}
(async () => {
try {
await startServer(...process.argv.slice(2))
}
catch (err) {
console.error(err)
process.exit(1)
}
})()
@cullylarson
Copy link
Author

Most of this is copied from: https://github.com/wbyoung/babel-plugin-transform-postcss

This issue is being tracked here: giuseppeg/styled-jsx-plugin-postcss#39. It also has an explanation of how this works and why it's necessary.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment