diff --git a/.eslintrc.yaml b/.eslintrc.yaml
deleted file mode 100644
index 0473b0f..0000000
--- a/.eslintrc.yaml
+++ /dev/null
@@ -1,5 +0,0 @@
-env:
- node: true
-extends: problems
-rules:
- no-console: off
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..a7a22ea
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,22 @@
+name: Node.js CI
+on:
+ push:
+ branches: master
+ pull_request:
+
+jobs:
+ test:
+ strategy:
+ matrix:
+ node: [18, 20]
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - run: git config --global core.autocrlf input
+ - uses: actions/checkout@v4
+ - name: Use Node.js ${{ matrix.node }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node }}
+ - run: npm install
+ - run: npm run ci
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 410129e..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,14 +0,0 @@
-language: node_js
-git:
- autocrlf: input
-os:
- - linux
- - windows
-node_js:
- - node
- - 12
- - 10
-script:
- - npm run ci
-after_success:
- - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1e0ce1..4fed6ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,67 @@
+# 11.0.1 / 2025-03-12
+
+- Update and minimize dependencies
+
+# 11.0.0 / 2023-12-05
+
+- **BREAKING:** Require Node.js v18+ ([#464](https://github.com/postcss/postcss-cli/pull/464))
+- Upgrade to [`postcss-load-config@5`](https://github.com/postcss/postcss-load-config/blob/main/CHANGELOG.md#50-2023-11-20) for improved ESM & TS config support ([#461](https://github.com/postcss/postcss-cli/issues/461), [#462](https://github.com/postcss/postcss-cli/pull/462))
+
+# 10.1.0 / 2022-11-29
+
+- Allow running `--watch` mode in non-TTY contexts, like Docker ([#448](https://github.com/postcss/postcss-cli/pull/448))
+- Update dependencies
+
+# 10.0.0 / 2022-06-29
+
+- **BREAKING:** Drop Node 12 support ([#438](https://github.com/postcss/postcss-cli/pull/438))
+- Add support for ESM config files ([#437](https://github.com/postcss/postcss-cli/pull/437))
+
+# 9.1.0 / 2021-12-10
+
+- Don't write to files if they're unchanged ([#320](https://github.com/postcss/postcss-cli/issues/320), [#417](https://github.com/postcss/postcss-cli/pull/417))
+
+# 9.0.2 / 2021-11-04
+
+- Switch to picocolors ([#409](https://github.com/postcss/postcss-cli/pull/409))
+- Remove test files from npm package
+
+# 9.0.1 / 2021-09-28
+
+- Actually exit with error when attempting to stdout in watch mode
+- Remove `bin/` from `files` in package.json
+
+# 9.0.0 / 2021-09-24
+
+- **BREAKING:** Require Node.js v12+
+- **BREAKING:** Must specify full file path, including `.js` extension, when loading local plugins with `--use` ([#401](https://github.com/postcss/postcss-cli/pull/401))
+- **BREAKING:** Officially remove support for watching postcss config (was already broken in previous releases)
+- Add support for `dir-dependency` messages ([#383](https://github.com/postcss/postcss-cli/pull/383), [#391](https://github.com/postcss/postcss-cli/pull/391))
+- Update deps
+
+# 8.3.1 / 2020-12-12
+
+- Ensure paths are not interpreted as numbers ([#360](https://github.com/postcss/postcss-cli/issues/360))
+- Better errors for incorrect postcss version ([#361](https://github.com/postcss/postcss-cli/issues/361), [#362](https://github.com/postcss/postcss-cli/pull/362))
+
+# 8.3.0 / 2020-11-17
+
+- Exit on EOF/`^D` ([#358](https://github.com/postcss/postcss-cli/pull/358))
+
+# 8.2.0 / 2020-10-29
+
+- Allow backslashes in paths for better Windows experience ([#355](https://github.com/postcss/postcss-cli/pull/355))
+
+# 8.1.0 / 2020-10-08
+
+- Add support for `postcss.config.cjs` files ([#351](https://github.com/postcss/postcss-cli/pull/351))
+
+# 8.0.0 / 2020-09-21
+
+- **BREAKING:** Support postcss v8 ([#344](https://github.com/postcss/postcss-cli/issues/344), [#349](https://github.com/postcss/postcss-cli/pull/349))
+- **BREAKING:** postcss is now a `peerDependency`, you must install it seperately ([#344](https://github.com/postcss/postcss-cli/issues/344), [#349](https://github.com/postcss/postcss-cli/pull/349))
+- Upgrade dependencies ([#340](https://github.com/postcss/postcss-cli/pull/340))
+
# 7.1.2 / 2020-08-31
- Make `--version` machine-readable ([#334](https://github.com/postcss/postcss-cli/issues/334), [#335](https://github.com/postcss/postcss-cli/pull/335))
@@ -21,7 +85,7 @@
# 6.1.3 / 2019-07-08
-- Fix external sorcemap filenames when the directory name contains `.css` ([#283](https://github.com/postcss/postcss-cli/issues/283), [#286](https://github.com/postcss/postcss-cli/pull/286))
+- Fix external sourcemap filenames when the directory name contains `.css` ([#283](https://github.com/postcss/postcss-cli/issues/283), [#286](https://github.com/postcss/postcss-cli/pull/286))
# 6.1.2 / 2019-02-22
@@ -276,4 +340,4 @@ Migration guide: https://github.com/postcss/postcss-cli/wiki/Migrating-from-v2-t
# 0.1.0 / 2015-03-11
-- initial implementaion
+- initial implementation
diff --git a/README.md b/README.md
index 50bade6..a390e3e 100644
--- a/README.md
+++ b/README.md
@@ -1,22 +1,23 @@
[![npm][npm]][npm-url]
[![node][node]][node-url]
-[](https://greenkeeper.io/)
[![tests][tests]][tests-url]
[![cover][cover]][cover-url]
[![chat][chat]][chat-url]
+PostCSS CLI is a command line interface for [PostCSS](https://postcss.org/)
+
Install
```bash
-npm i -g|-D postcss-cli
+npm i -D postcss postcss-cli
```
Usage
@@ -98,7 +99,7 @@ Note that you **can not** set the `from` or `to` options for postcss in the conf
### Context
-For more advanced usage it's recommend to to use a function in `postcss.config.js`, this gives you access to the CLI context to dynamically apply options and plugins **per file**
+For more advanced usage, it's recommended to use a function in `postcss.config.js`; this gives you access to the CLI context to dynamically apply options and plugins **per file**
| Name | Type | Default | Description |
| :-------: | :--------: | :--------------------------------: | :------------------- |
@@ -142,8 +143,8 @@ module.exports = (ctx) => ({
[npm-url]: https://npmjs.com/package/postcss-cli
[node]: https://img.shields.io/node/v/postcss-cli.svg
[node-url]: https://nodejs.org/
-[tests]: http://img.shields.io/travis/postcss/postcss-cli/master.svg
-[tests-url]: https://travis-ci.org/postcss/postcss-cli
+[tests]: https://img.shields.io/github/workflow/status/postcss/postcss-cli/Node.js%20CI/master
+[tests-url]: https://github.com/postcss/postcss-cli/actions?query=branch%3Amaster
[cover]: https://img.shields.io/coveralls/postcss/postcss-cli/master.svg
[cover-url]: https://coveralls.io/github/postcss/postcss-cli
[chat]: https://img.shields.io/gitter/room/postcss/postcss.svg
diff --git a/bin/postcss b/bin/postcss
deleted file mode 100755
index d40abef..0000000
--- a/bin/postcss
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/usr/bin/env node
-
-require('../')
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..26e500f
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,16 @@
+import problems from 'eslint-config-problems'
+import globals from 'globals'
+
+export default [
+ problems,
+ {
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ },
+ rules: {
+ 'no-console': 'off',
+ },
+ },
+]
diff --git a/index.js b/index.js
old mode 100644
new mode 100755
index 187b9bc..266c912
--- a/index.js
+++ b/index.js
@@ -1,47 +1,59 @@
-'use strict'
+#!/usr/bin/env node
-const fs = require('fs-extra')
-const path = require('path')
+import fs from 'fs-extra'
+import path from 'path'
-const prettyHrtime = require('pretty-hrtime')
-const stdin = require('get-stdin')
-const read = require('read-cache')
-const chalk = require('chalk')
-const globber = require('globby')
-const chokidar = require('chokidar')
+import prettyHrtime from 'pretty-hrtime'
+import { text } from 'stream/consumers'
+import read from 'read-cache'
+import pc from 'picocolors'
+import { glob } from 'tinyglobby'
+import slash from 'slash'
+import chokidar from 'chokidar'
-const postcss = require('postcss')
-const postcssrc = require('postcss-load-config')
-const reporter = require('postcss-reporter/lib/formatter')()
+import postcss from 'postcss'
+import postcssrc from 'postcss-load-config'
+import postcssReporter from 'postcss-reporter/lib/formatter.js'
-const argv = require('./lib/args')
-const depGraph = require('./lib/depGraph')
-const getMapfile = require('./lib/getMapfile')
+import argv from './lib/args.js'
+import createDependencyGraph from './lib/DependencyGraph.js'
+import getMapfile from './lib/getMapfile.js'
+
+const reporter = postcssReporter()
+const depGraph = createDependencyGraph()
let input = argv._
const { dir, output } = argv
if (argv.map) argv.map = { inline: false }
-const cliConfig = {
- options: {
- map: argv.map !== undefined ? argv.map : { inline: true },
- parser: argv.parser ? require(argv.parser) : undefined,
- syntax: argv.syntax ? require(argv.syntax) : undefined,
- stringifier: argv.stringifier ? require(argv.stringifier) : undefined,
- },
- plugins: argv.use
- ? argv.use.map((plugin) => {
- try {
- return require(plugin)()
- } catch (e) {
- const msg = e.message || `Cannot find module '${plugin}'`
- let prefix = msg.includes(plugin) ? '' : ` (${plugin})`
- if (e.name && e.name !== 'Error') prefix += `: ${e.name}`
- return error(`Plugin Error${prefix}: ${msg}'`)
- }
- })
- : [],
+let cliConfig
+
+async function buildCliConfig() {
+ cliConfig = {
+ options: {
+ map: argv.map !== undefined ? argv.map : { inline: true },
+ parser: argv.parser ? await import(argv.parser) : undefined,
+ syntax: argv.syntax ? await import(argv.syntax) : undefined,
+ stringifier: argv.stringifier
+ ? await import(argv.stringifier)
+ : undefined,
+ },
+ plugins: argv.use
+ ? await Promise.all(
+ argv.use.map(async (plugin) => {
+ try {
+ return (await import(plugin)).default()
+ } catch (e) {
+ const msg = e.message || `Cannot find module '${plugin}'`
+ let prefix = msg.includes(plugin) ? '' : ` (${plugin})`
+ if (e.name && e.name !== 'Error') prefix += `: ${e.name}`
+ return error(`Plugin Error${prefix}: ${msg}'`)
+ }
+ }),
+ )
+ : [],
+ }
}
let configFile
@@ -49,19 +61,40 @@ let configFile
if (argv.env) process.env.NODE_ENV = argv.env
if (argv.config) argv.config = path.resolve(argv.config)
-Promise.resolve()
+let { isTTY } = process.stdin
+
+if (process.env.FORCE_IS_TTY === 'true') {
+ isTTY = true
+}
+
+if (argv.watch && isTTY) {
+ process.stdin.on('end', () => process.exit(0))
+ process.stdin.resume()
+}
+
+/* istanbul ignore next */
+if (parseInt(postcss().version) < 8) {
+ error('Please install PostCSS 8 or above')
+}
+
+buildCliConfig()
.then(() => {
if (argv.watch && !(argv.output || argv.replace || argv.dir)) {
error('Cannot write to stdout in watch mode')
+ // Need to explicitly exit here, since error() doesn't exit in watch mode
+ process.exit(1)
}
if (input && input.length) {
- return globber(input, { dot: argv.includeDotfiles })
+ return glob(
+ input.map((i) => slash(String(i))),
+ { dot: argv.includeDotfiles },
+ )
}
if (argv.replace || argv.dir) {
error(
- 'Input Error: Cannot use --dir or --replace when reading from stdin'
+ 'Input Error: Cannot use --dir or --replace when reading from stdin',
)
}
@@ -78,7 +111,7 @@ Promise.resolve()
if (i.length > 1 && !argv.dir && !argv.replace) {
error(
- 'Input Error: Must use --dir or --replace with multiple input files'
+ 'Input Error: Must use --dir or --replace with multiple input files',
)
}
@@ -91,7 +124,7 @@ Promise.resolve()
.then((results) => {
if (argv.watch) {
const printMessage = () =>
- printVerbose(chalk.dim('\nWaiting for file changes...'))
+ printVerbose(pc.dim('\nWaiting for file changes...'))
const watcher = chokidar.watch(input.concat(dependencies(results)), {
usePolling: argv.poll,
interval: argv.poll && typeof argv.poll === 'number' ? argv.poll : 100,
@@ -108,13 +141,17 @@ Promise.resolve()
if (input.includes(file)) recompile.push(file)
+ const dependants = depGraph
+ .dependantsOf(file)
+ .concat(getAncestorDirs(file).flatMap(depGraph.dependantsOf))
+
recompile = recompile.concat(
- depGraph.dependantsOf(file).filter((file) => input.includes(file))
+ dependants.filter((file) => input.includes(file)),
)
if (!recompile.length) recompile = input
- return files(recompile)
+ return files([...new Set(recompile)])
.then((results) => watcher.add(dependencies(results)))
.then(printMessage)
.catch(error)
@@ -134,7 +171,7 @@ function rc(ctx, path) {
.then((rc) => {
if (rc.options.from || rc.options.to) {
error(
- 'Config Error: Can not set from or to options in config file, use CLI arguments instead'
+ 'Config Error: Can not set from or to options in config file, use CLI arguments instead',
)
}
configFile = rc.file
@@ -151,14 +188,14 @@ function files(files) {
return Promise.all(
files.map((file) => {
if (file === 'stdin') {
- return stdin().then((content) => {
+ return text(process.stdin).then((content) => {
if (!content) return error('Input Error: Did not receive any STDIN')
return css(content, 'stdin')
})
}
return read(file).then((content) => css(content, file))
- })
+ }),
)
}
@@ -182,7 +219,7 @@ function css(css, file) {
const time = process.hrtime()
- printVerbose(chalk`{cyan Processing {bold ${relativePath}}...}`)
+ printVerbose(pc.cyan(`Processing ${pc.bold(relativePath)}...`))
return rc(ctx, argv.config)
.then((config) => {
@@ -209,7 +246,7 @@ function css(css, file) {
if (!options.to && config.options.map && !config.options.map.inline) {
error(
- 'Output Error: Cannot output external sourcemaps when writing to STDOUT'
+ 'Output Error: Cannot output external sourcemaps when writing to STDOUT',
)
}
@@ -219,18 +256,20 @@ function css(css, file) {
const tasks = []
if (options.to) {
- tasks.push(fs.outputFile(options.to, result.css))
+ tasks.push(outputFile(options.to, result.css))
if (result.map) {
const mapfile = getMapfile(options)
- tasks.push(fs.outputFile(mapfile, result.map.toString()))
+ tasks.push(outputFile(mapfile, result.map.toString()))
}
} else process.stdout.write(result.css, 'utf8')
return Promise.all(tasks).then(() => {
const prettyTime = prettyHrtime(process.hrtime(time))
printVerbose(
- chalk`{green Finished {bold ${relativePath}} in {bold ${prettyTime}}}`
+ pc.green(
+ `Finished ${pc.bold(relativePath)} in ${pc.bold(prettyTime)}`,
+ ),
)
const messages = result.warnings()
@@ -245,6 +284,13 @@ function css(css, file) {
.catch((err) => {
throw err
})
+
+ async function outputFile(file, string) {
+ const fileExists = await fs.pathExists(file)
+ const currentValue = fileExists ? await fs.readFile(file, 'utf8') : null
+ if (currentValue === string) return
+ return fs.outputFile(file, string)
+ }
}
function dependencies(results) {
@@ -256,9 +302,21 @@ function dependencies(results) {
if (result.messages <= 0) return
result.messages
- .filter((msg) => (msg.type === 'dependency' ? msg : ''))
+ .filter((msg) =>
+ msg.type === 'dependency' || msg.type === 'dir-dependency' ? msg : '',
+ )
.map(depGraph.add)
- .forEach((dependency) => messages.push(dependency.file))
+ .forEach((dependency) => {
+ if (dependency.type === 'dir-dependency') {
+ messages.push(
+ dependency.glob
+ ? path.join(dependency.dir, dependency.glob)
+ : dependency.dir,
+ )
+ } else {
+ messages.push(dependency.file)
+ }
+ })
})
return messages
@@ -273,7 +331,7 @@ function error(err) {
if (argv.verbose) console.error()
if (typeof err === 'string') {
- console.error(chalk.red(err))
+ console.error(pc.red(err))
} else if (err.name === 'CssSyntaxError') {
console.error(err.toString())
} else {
@@ -283,3 +341,14 @@ function error(err) {
if (argv.watch) return
process.exit(1)
}
+
+// Input: '/imports/components/button.css'
+// Output: ['/imports/components', '/imports', '/']
+function getAncestorDirs(fileOrDir) {
+ const { root } = path.parse(fileOrDir)
+ if (fileOrDir === root) {
+ return []
+ }
+ const parentDir = path.dirname(fileOrDir)
+ return [parentDir, ...getAncestorDirs(parentDir)]
+}
diff --git a/lib/DependencyGraph.js b/lib/DependencyGraph.js
new file mode 100644
index 0000000..efb7ac3
--- /dev/null
+++ b/lib/DependencyGraph.js
@@ -0,0 +1,30 @@
+import path from 'path'
+import { DepGraph } from 'dependency-graph'
+
+export default function createDependencyGraph() {
+ const graph = new DepGraph()
+ return {
+ add(message) {
+ message.parent = path.resolve(message.parent)
+ graph.addNode(message.parent)
+
+ if (message.type === 'dir-dependency') {
+ message.dir = path.resolve(message.dir)
+ graph.addNode(message.dir)
+ graph.addDependency(message.parent, message.dir)
+ } else {
+ message.file = path.resolve(message.file)
+ graph.addNode(message.file)
+ graph.addDependency(message.parent, message.file)
+ }
+
+ return message
+ },
+ dependantsOf(node) {
+ node = path.resolve(node)
+
+ if (graph.hasNode(node)) return graph.dependantsOf(node)
+ return []
+ },
+ }
+}
diff --git a/lib/DependencyGraph.test.js b/lib/DependencyGraph.test.js
new file mode 100644
index 0000000..d54393c
--- /dev/null
+++ b/lib/DependencyGraph.test.js
@@ -0,0 +1,19 @@
+import test from 'ava'
+import path from 'path'
+import createDependencyGraph from './DependencyGraph.js'
+
+function resolveArray(arr) {
+ return arr.map((p) => path.resolve(p))
+}
+
+test('tracks dependencies', (t) => {
+ const graph = createDependencyGraph()
+ graph.add({ file: 'aa', parent: 'a' })
+ graph.add({ file: 'bb', parent: 'b' })
+ graph.add({ file: 'ab', parent: 'a' })
+ graph.add({ file: 'ab', parent: 'b' })
+ t.deepEqual(graph.dependantsOf('aa'), resolveArray(['a']))
+ t.deepEqual(graph.dependantsOf('bb'), resolveArray(['b']))
+ t.deepEqual(graph.dependantsOf('ab'), resolveArray(['a', 'b']))
+ t.deepEqual(graph.dependantsOf('nonexistent'), [])
+})
diff --git a/lib/args.js b/lib/args.js
index b027bb4..12d14fa 100644
--- a/lib/args.js
+++ b/lib/args.js
@@ -1,31 +1,17 @@
-'use strict'
-const chalk = require('chalk')
+import yargs from 'yargs'
-const logo = `
- /|\\
- // //
- // //
- //___*___*___//
- //--*---------*--//
- /|| * * ||/
- // ||* *|| //
- // || * * || //
- //_____||___*_________*___||_____//
-`
-
-module.exports = require('yargs')
+const { argv } = yargs(process.argv.slice(2))
.usage(
- `${chalk.bold.red(logo)}
-Usage:
+ `Usage:
$0 [input.css] [OPTIONS] [-o|--output output.css] [--watch|-w]
$0 ... [OPTIONS] --dir [--watch|-w]
$0 [OPTIONS] --dir [--watch|-w]
$0 [OPTIONS] --dir [--watch|-w]
- $0 ... [OPTIONS] --replace`
+ $0 ... [OPTIONS] --replace`,
)
.group(
['o', 'd', 'r', 'map', 'no-map', 'watch', 'verbose', 'env'],
- 'Basic options:'
+ 'Basic options:',
)
.option('o', {
alias: 'output',
@@ -64,7 +50,7 @@ Usage:
})
.group(
['u', 'parser', 'stringifier', 'syntax'],
- 'Options for use without a config file:'
+ 'Options for use without a config file:',
)
.option('u', {
alias: 'use',
@@ -88,14 +74,9 @@ Usage:
desc: 'Override the output file extension; for use with --dir',
type: 'string',
implies: 'dir',
- coerce(ext) {
- if (ext.indexOf('.') !== 0) return `.${ext}`
- return ext
- },
})
.option('base', {
- desc:
- 'Mirror the directory structure relative to this path in the output directory, for use with --dir',
+ desc: 'Mirror the directory structure relative to this path in the output directory, for use with --dir',
type: 'string',
implies: 'dir',
})
@@ -105,8 +86,7 @@ Usage:
type: 'boolean',
})
.option('poll', {
- desc:
- 'Use polling for file watching. Can optionally pass polling interval; default 100 ms',
+ desc: 'Use polling for file watching. Can optionally pass polling interval; default 100 ms',
implies: 'watch',
})
.option('config', {
@@ -118,7 +98,7 @@ Usage:
.example('$0 src/**/*.css --base src --dir build', 'Glob Pattern & output')
.example(
'cat input.css | $0 -u autoprefixer > output.css',
- 'Piping input & output'
+ 'Piping input & output',
)
.epilog(
`If no input files are passed, it reads from stdin. If neither -o, --dir, or --replace is passed, it writes to stdout.
@@ -127,5 +107,9 @@ If there are multiple input files, the --dir or --replace option must be passed.
Input files may contain globs (e.g. src/**/*.css). If you pass an input directory, it will process all files in the directory and any subdirectories, respecting the glob pattern.
-For more details, please see https://github.com/postcss/postcss-cli`
- ).argv
+For more details, please see https://github.com/postcss/postcss-cli`,
+ )
+
+if (argv.ext && argv.ext.indexOf('.') !== 0) argv.ext = `.${argv.ext}`
+
+export default argv
diff --git a/lib/depGraph.js b/lib/depGraph.js
deleted file mode 100644
index 783d76e..0000000
--- a/lib/depGraph.js
+++ /dev/null
@@ -1,22 +0,0 @@
-'use strict'
-const path = require('path')
-const DepGraph = require('dependency-graph').DepGraph
-
-const graph = new DepGraph()
-
-exports.add = (message) => {
- message.parent = path.resolve(message.parent)
- message.file = path.resolve(message.file)
-
- graph.addNode(message.parent)
- graph.addNode(message.file)
- graph.addDependency(message.parent, message.file)
- return message
-}
-
-exports.dependantsOf = (node) => {
- node = path.resolve(node)
-
- if (graph.hasNode(node)) return graph.dependantsOf(node)
- return []
-}
diff --git a/lib/getMapfile.js b/lib/getMapfile.js
index 346fea7..e4d0c93 100644
--- a/lib/getMapfile.js
+++ b/lib/getMapfile.js
@@ -1,6 +1,5 @@
-'use strict'
-const path = require('path')
-module.exports = function getMapfile(options) {
+import path from 'path'
+export default function getMapfile(options) {
if (options.map && typeof options.map.annotation === 'string') {
return `${path.dirname(options.to)}/${options.map.annotation}`
}
diff --git a/lib/getMapfile.test.js b/lib/getMapfile.test.js
new file mode 100644
index 0000000..becc460
--- /dev/null
+++ b/lib/getMapfile.test.js
@@ -0,0 +1,27 @@
+import test from 'ava'
+import getMapfile from './getMapfile.js'
+
+test('mapFile path is properly resolved', async (t) => {
+ const paths = [
+ {
+ input: { to: '/foo/bar.css/baz/index.css' },
+ want: '/foo/bar.css/baz/index.css.map',
+ },
+ {
+ input: { to: '/foo/bar.sss/baz/index.sss' },
+ want: '/foo/bar.sss/baz/index.sss.map',
+ },
+ {
+ input: { to: '/foo/bar.css/baz/bar.css' },
+ want: '/foo/bar.css/baz/bar.css.map',
+ },
+ {
+ input: { map: { annotation: 'foo.map' }, to: '/foo/bar.css/baz/bar.css' },
+ want: '/foo/bar.css/baz/foo.map',
+ },
+ ]
+
+ for (const p of paths) {
+ t.is(getMapfile(p.input), p.want)
+ }
+})
diff --git a/package.json b/package.json
index 0588ea6..dcb8866 100644
--- a/package.json
+++ b/package.json
@@ -1,51 +1,55 @@
{
"name": "postcss-cli",
- "version": "7.1.2",
+ "version": "11.0.1",
"description": "CLI for PostCSS",
- "main": "index.js",
+ "type": "module",
"engines": {
- "node": ">=10"
+ "node": ">=18"
},
"bin": {
- "postcss": "./bin/postcss"
+ "postcss": "./index.js"
},
"scripts": {
- "ci": "eslint . && nyc ava -v && npm run prettier -- --list-different",
+ "ci": "eslint . && c8 ava -v && npm run prettier -- --list-different",
"clean": "node test/helpers/clean.js",
"prettier": "prettier --single-quote --no-semi \"**/*.{js,md}\"",
"format": "npm run prettier -- --write && eslint . --fix",
"pretest": "npm run clean && npm run format",
- "test": "nyc ava -v"
+ "test": "c8 ava -v"
},
"dependencies": {
- "chalk": "^4.0.0",
"chokidar": "^3.3.0",
- "dependency-graph": "^0.9.0",
- "fs-extra": "^9.0.0",
- "get-stdin": "^8.0.0",
- "globby": "^11.0.0",
- "postcss": "^7.0.0",
- "postcss-load-config": "^2.0.0",
- "postcss-reporter": "^6.0.0",
+ "dependency-graph": "^1.0.0",
+ "fs-extra": "^11.0.0",
+ "picocolors": "^1.0.0",
+ "postcss-load-config": "^5.0.0",
+ "postcss-reporter": "^7.0.0",
"pretty-hrtime": "^1.0.3",
"read-cache": "^1.0.0",
- "yargs": "^15.0.2"
+ "slash": "^5.0.0",
+ "tinyglobby": "^0.2.12",
+ "yargs": "^17.0.0"
},
"devDependencies": {
"ava": "^3.1.0",
+ "c8": "^10.0.0",
"coveralls": "^3.0.0",
- "eslint": "^6.8.0",
- "eslint-config-problems": "4.0.0",
- "nyc": "^15.0.0",
- "postcss-import": "^12.0.0",
- "prettier": "~2.0.0",
- "sugarss": "^2.0.0",
- "uuid": "^8.0.0"
+ "eslint": "^9.22.0",
+ "eslint-config-problems": "9.0.0",
+ "globals": "^16.0.0",
+ "postcss": "^8.0.4",
+ "postcss-import": "^16.0.0",
+ "prettier": "~3.5.0",
+ "sugarss": "^5.0.0",
+ "uuid": "^11.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
},
"files": [
- "bin",
"index.js",
- "lib"
+ "lib",
+ "!**/*.test.js"
],
"keywords": [
"cli",
diff --git a/test/.eslintrc.yaml b/test/.eslintrc.yaml
deleted file mode 100644
index add5a78..0000000
--- a/test/.eslintrc.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-parserOptions:
- sourceType: module
diff --git a/test/base.js b/test/base.js
index 1bef3e3..a0f965c 100644
--- a/test/base.js
+++ b/test/base.js
@@ -1,9 +1,9 @@
-const test = require('ava')
-const path = require('path')
+import test from 'ava'
+import path from 'path'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--base --dir works', async (t) => {
const dir = tmp()
@@ -21,11 +21,11 @@ test('--base --dir works', async (t) => {
t.is(
await read(path.join(dir, 'level-1/level-2/a.css')),
- await read('test/fixtures/base/level-1/level-2/a.css')
+ await read('test/fixtures/base/level-1/level-2/a.css'),
)
t.is(
await read(path.join(dir, 'level-1/b.css')),
- await read('test/fixtures/base/level-1/b.css')
+ await read('test/fixtures/base/level-1/b.css'),
)
})
diff --git a/test/cli.js b/test/cli.js
index d2e449c..3adab9b 100644
--- a/test/cli.js
+++ b/test/cli.js
@@ -1,8 +1,8 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('works with defaults', async (t) => {
const output = tmp('output.css')
diff --git a/test/config.js b/test/config.js
index 774e2f5..b1d7c2c 100644
--- a/test/config.js
+++ b/test/config.js
@@ -1,10 +1,10 @@
-const test = require('ava')
-const path = require('path')
+import test from 'ava'
+import path from 'path'
-const ENV = require('./helpers/env.js')
+import ENV from './helpers/env.js'
-const cli = require('./helpers/cli.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import read from './helpers/read.js'
test('supports common config', async (t) => {
const env = `module.exports = {
@@ -17,14 +17,39 @@ test('supports common config', async (t) => {
const { error, stderr } = await cli(
['a.css', '-o', 'output.css', '--no-map'],
- dir
+ dir,
)
t.falsy(error, stderr)
t.is(
await read(path.join(dir, 'output.css')),
- await read('test/fixtures/a.css')
+ await read('test/fixtures/a.css'),
+ )
+})
+
+test('supports ESM config', async (t) => {
+ const env = `import postcssImport from 'postcss-import'
+ export default function () {
+ return {
+ plugins: [
+ postcssImport()
+ ]
+ }
+ }`
+
+ const dir = await ENV(env, ['a.css'], 'mjs')
+
+ const { error, stderr } = await cli(
+ ['a.css', '-o', 'output.css', '--no-map'],
+ dir,
+ )
+
+ t.falsy(error, stderr)
+
+ t.is(
+ await read(path.join(dir, 'output.css')),
+ await read('test/fixtures/a.css'),
)
})
@@ -35,14 +60,14 @@ test("doesn't error on empty config", async (t) => {
const { error, stderr } = await cli(
['a.css', '-o', 'output.css', '--no-map'],
- dir
+ dir,
)
t.falsy(error, stderr)
t.is(
await read(path.join(dir, 'output.css')),
- await read('test/fixtures/a.css')
+ await read('test/fixtures/a.css'),
)
})
@@ -57,7 +82,7 @@ test('errors if `to` is set', async (t) => {
t.regex(
stderr,
- /Config Error: Can not set from or to options in config file, use CLI arguments instead/
+ /Config Error: Can not set from or to options in config file, use CLI arguments instead/,
)
})
@@ -72,6 +97,6 @@ test('errors if `from` is set', async (t) => {
t.regex(
stderr,
- /Config Error: Can not set from or to options in config file, use CLI arguments instead/
+ /Config Error: Can not set from or to options in config file, use CLI arguments instead/,
)
})
diff --git a/test/dir.js b/test/dir.js
index 51c0b6e..695f198 100644
--- a/test/dir.js
+++ b/test/dir.js
@@ -1,9 +1,9 @@
-const test = require('ava')
-const path = require('path')
+import test from 'ava'
+import path from 'path'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--dir works', async (t) => {
const dir = tmp()
diff --git a/test/error.js b/test/error.js
index e53ce05..5ff67ef 100644
--- a/test/error.js
+++ b/test/error.js
@@ -1,7 +1,7 @@
-const test = require('ava')
+import test from 'ava'
-const tmp = require('./helpers/tmp.js')
-const cli = require('./helpers/cli.js')
+import tmp from './helpers/tmp.js'
+import cli from './helpers/cli.js'
test('multiple input files && --output', (t) => {
return cli(['test/fixtures/*.css', '-o', tmp()]).then(({ error, code }) => {
@@ -22,33 +22,20 @@ test('--map && writing to stdout', (t) => {
t.is(code, 1, 'expected non-zero error code')
t.regex(
error.toString(),
- /Output Error: Cannot output external sourcemaps when writing to STDOUT/
+ /Output Error: Cannot output external sourcemaps when writing to STDOUT/,
)
})
})
-test.failing('invalid --config', (t) => {
- return cli([
- 'test/fixtures/*.css',
- '-c',
- 'test/postcss.config.js',
- '-d',
- tmp(),
- ]).then(({ error, code }) => {
- t.is(code, 1, 'expected non-zero error code')
- t.regex(error.toString(), /ENOENT: no such file or directory/)
- })
-})
-
test('plugin not found', (t) => {
return cli(['test/fixtures/a.css', '-u', 'postcss-plugin', '-o', tmp()]).then(
({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
t.regex(
error.toString(),
- /Plugin Error: Cannot find module 'postcss-plugin'/
+ /Plugin Error: Cannot find package 'postcss-plugin'/,
)
- }
+ },
)
})
@@ -56,12 +43,12 @@ test('plugin throws on require', (t) => {
return cli([
'test/fixtures/a.css',
'-u',
- './test/fixtures/_bad-plugin',
+ './test/fixtures/_bad-plugin.js',
'-o',
tmp(),
]).then(({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
- t.regex(error.toString(), /Plugin Error \(.*bad-plugin\): This fails/)
+ t.regex(error.toString(), /Plugin Error \(.*bad-plugin.js\): This fails/)
})
})
@@ -71,8 +58,8 @@ test('CssSyntaxError', (t) => {
t.is(code, 1, 'expected non-zero error code')
t.regex(
error.toString(),
- /CssSyntaxError: .*a.css:1:4: Unnecessary curly bracket/
+ /CssSyntaxError: .*a.css:1:4: Unnecessary curly bracket/,
)
- }
+ },
)
})
diff --git a/test/ext.js b/test/ext.js
index 3f6e883..329f2c4 100644
--- a/test/ext.js
+++ b/test/ext.js
@@ -1,10 +1,10 @@
-const test = require('ava')
+import test from 'ava'
-const fs = require('fs-extra')
-const path = require('path')
+import fs from 'fs-extra'
+import path from 'path'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
test('--ext works', async (t) => {
const dir = tmp()
@@ -22,3 +22,20 @@ test('--ext works', async (t) => {
t.truthy(await fs.pathExists(path.join(dir, 'a.css')))
})
+
+test('--ext works with no leading dot', async (t) => {
+ const dir = tmp()
+
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.sss',
+ '--parser',
+ 'sugarss',
+ '-d',
+ dir,
+ '--ext',
+ 'css',
+ ])
+ t.falsy(error, stderr)
+
+ t.truthy(await fs.pathExists(path.join(dir, 'a.css')))
+})
diff --git a/test/fixtures/base/level-1/level-2/unrelated.md b/test/fixtures/base/level-1/level-2/unrelated.md
new file mode 100644
index 0000000..e48c0e0
--- /dev/null
+++ b/test/fixtures/base/level-1/level-2/unrelated.md
@@ -0,0 +1 @@
+Editing this file should not trigger a rebuild.
diff --git a/test/fixtures/unchanged-input.css b/test/fixtures/unchanged-input.css
new file mode 100644
index 0000000..60f1eab
--- /dev/null
+++ b/test/fixtures/unchanged-input.css
@@ -0,0 +1,3 @@
+body {
+ color: red;
+}
diff --git a/test/fixtures/unchanged-output.css b/test/fixtures/unchanged-output.css
new file mode 100644
index 0000000..6521159
--- /dev/null
+++ b/test/fixtures/unchanged-output.css
@@ -0,0 +1,5 @@
+body {
+ color: red;
+}
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInVuY2hhbmdlZC1pbnB1dC5jc3MiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEiLCJmaWxlIjoidW5jaGFuZ2VkLW91dHB1dC5jc3MiLCJzb3VyY2VzQ29udGVudCI6WyJib2R5IHtcbiAgY29sb3I6IHJlZDtcbn1cbiJdfQ== */
\ No newline at end of file
diff --git a/test/glob.js b/test/glob.js
index 574cebb..863fc39 100644
--- a/test/glob.js
+++ b/test/glob.js
@@ -1,9 +1,9 @@
-const test = require('ava')
-const path = require('path')
+import test from 'ava'
+import path from 'path'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('works with glob patterns', async (t) => {
const output = tmp()
@@ -19,14 +19,14 @@ test('works with glob patterns', async (t) => {
t.is(
await read(path.join(output, 'a.css')),
- await read('test/fixtures/glob/a.css')
+ await read('test/fixtures/glob/a.css'),
)
t.is(
await read(path.join(output, 'b.css')),
- await read('test/fixtures/glob/b.css')
+ await read('test/fixtures/glob/b.css'),
)
t.is(
await read(path.join(output, 's.css')),
- await read('test/fixtures/glob/s.css')
+ await read('test/fixtures/glob/s.css'),
)
})
diff --git a/test/helpers/clean.js b/test/helpers/clean.js
index 526bedf..73bb191 100644
--- a/test/helpers/clean.js
+++ b/test/helpers/clean.js
@@ -1,10 +1,8 @@
-'use strict' // eslint-disable-line
-const fs = require('fs-extra')
+import fs from 'fs-extra'
Promise.all([
fs.emptyDir('./test/fixtures/.tmp/'),
fs.remove('./coverage'),
- fs.remove('./.nyc_output'),
]).catch((err) => {
console.error(err)
process.exit(1)
diff --git a/test/helpers/cli.js b/test/helpers/cli.js
index 7512439..838d5a6 100644
--- a/test/helpers/cli.js
+++ b/test/helpers/cli.js
@@ -1,10 +1,10 @@
-const path = require('path')
-const { exec } = require('child_process')
+import path from 'path'
+import { exec } from 'child_process'
-module.exports = function (args, cwd) {
+export default function (args, cwd) {
return new Promise((resolve) => {
exec(
- `node ${path.resolve('bin/postcss')} ${args.join(' ')}`,
+ `node ${path.resolve('index.js')} ${args.join(' ')}`,
{ cwd },
(error, stdout, stderr) => {
resolve({
@@ -13,7 +13,7 @@ module.exports = function (args, cwd) {
stdout,
stderr,
})
- }
+ },
)
})
}
diff --git a/test/helpers/env.js b/test/helpers/env.js
index 2fad200..017df87 100644
--- a/test/helpers/env.js
+++ b/test/helpers/env.js
@@ -1,19 +1,18 @@
-const fs = require('fs-extra')
-const path = require('path')
-const globby = require('globby')
+import fs from 'fs-extra'
+import path from 'path'
+import { glob } from 'tinyglobby'
-const tmp = require('./tmp.js')
+import tmp from './tmp.js'
-module.exports = function (config, fixtures) {
- fixtures = fixtures || '**/*'
+export default function (config, fixtures = '**/*', extension = 'cjs') {
const dir = tmp()
return Promise.all([
- globby(fixtures, { cwd: 'test/fixtures' }).then((list) => {
+ glob(fixtures, { cwd: 'test/fixtures' }).then((list) => {
return list.map((item) => {
return fs.copy(path.join('test/fixtures', item), path.join(dir, item))
})
}),
- fs.outputFile(path.join(dir, 'postcss.config.js'), config),
+ fs.outputFile(path.join(dir, `postcss.config.${extension}`), config),
]).then(() => dir)
}
diff --git a/test/helpers/read.js b/test/helpers/read.js
index 3122ee1..e9939aa 100644
--- a/test/helpers/read.js
+++ b/test/helpers/read.js
@@ -1,7 +1,7 @@
-const { readFile } = require('fs-extra')
+import fs from 'fs-extra'
-module.exports = function (path) {
- return readFile(path, 'utf8').then(
- (content) => content.replace(/\r\n/g, '\n') // normalize line endings on Windows
+export default function (path) {
+ return fs.readFile(path, 'utf8').then(
+ (content) => content.replace(/\r\n/g, '\n'), // normalize line endings on Windows
)
}
diff --git a/test/helpers/tmp.js b/test/helpers/tmp.js
index f084dea..bfa1369 100644
--- a/test/helpers/tmp.js
+++ b/test/helpers/tmp.js
@@ -1,7 +1,7 @@
-const path = require('path')
-const { v4: uuid } = require('uuid')
+import path from 'path'
+import { v4 as uuid } from 'uuid'
-module.exports = function (ext) {
+export default function (ext) {
ext = ext || ''
return path.join('test/fixtures/.tmp', uuid(), ext)
diff --git a/test/map.js b/test/map.js
index ac64a78..4783010 100644
--- a/test/map.js
+++ b/test/map.js
@@ -1,11 +1,9 @@
-const test = require('ava')
-const fs = require('fs-extra')
+import test from 'ava'
+import fs from 'fs-extra'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
-
-const getMapfile = require('../lib/getMapfile')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('inline maps are generated by default', async (t) => {
const output = tmp('output.css')
@@ -55,28 +53,3 @@ test('--no-map disables internal sourcemaps', async (t) => {
t.notRegex(await read(output), /\/*# sourceMappingURL=/)
})
-
-test('mapFile path is property resolved', async (t) => {
- const paths = [
- {
- input: { to: '/foo/bar.css/baz/index.css' },
- want: '/foo/bar.css/baz/index.css.map',
- },
- {
- input: { to: '/foo/bar.sss/baz/index.sss' },
- want: '/foo/bar.sss/baz/index.sss.map',
- },
- {
- input: { to: '/foo/bar.css/baz/bar.css' },
- want: '/foo/bar.css/baz/bar.css.map',
- },
- {
- input: { map: { annotation: 'foo.map' }, to: '/foo/bar.css/baz/bar.css' },
- want: '/foo/bar.css/baz/foo.map',
- },
- ]
-
- for (const p of paths) {
- t.is(getMapfile(p.input), p.want)
- }
-})
diff --git a/test/misc.js b/test/misc.js
index 29a736c..09bde3f 100644
--- a/test/misc.js
+++ b/test/misc.js
@@ -1,6 +1,6 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
+import cli from './helpers/cli.js'
test('--help', async (t) => {
const help = await cli(['--help'])
@@ -17,6 +17,6 @@ test('--version', async (t) => {
t.truthy(
version.stdout.length > 5,
- 'expected --version to output version info'
+ 'expected --version to output version info',
)
})
diff --git a/test/parser.js b/test/parser.js
index 4c56abb..689d9b7 100644
--- a/test/parser.js
+++ b/test/parser.js
@@ -1,8 +1,8 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--parser works', async (t) => {
const output = tmp('output.css')
diff --git a/test/replace.js b/test/replace.js
index 5b48ad4..dd24b98 100644
--- a/test/replace.js
+++ b/test/replace.js
@@ -1,11 +1,11 @@
-const test = require('ava')
+import test from 'ava'
-const fs = require('fs-extra')
-const path = require('path')
+import fs from 'fs-extra'
+import path from 'path'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--replace works', async (t) => {
const dir = tmp()
diff --git a/test/stdin.js b/test/stdin.js
index 867145c..a682436 100644
--- a/test/stdin.js
+++ b/test/stdin.js
@@ -1,17 +1,17 @@
-const test = require('ava')
+import test from 'ava'
-const fs = require('fs-extra')
-const path = require('path')
-const { exec } = require('child_process')
+import fs from 'fs-extra'
+import path from 'path'
+import { exec } from 'child_process'
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test.cb('reads from stdin', (t) => {
const output = tmp('output.css')
const cp = exec(
- `node ${path.resolve('bin/postcss')} -o ${output} --no-map`,
+ `node ${path.resolve('index.js')} -o ${output} --no-map`,
(error, stdout, stderr) => {
if (error) t.end(error, stderr)
@@ -21,7 +21,7 @@ test.cb('reads from stdin', (t) => {
t.end()
})
.catch(t.end)
- }
+ },
)
fs.createReadStream('test/fixtures/a.css').pipe(cp.stdin)
diff --git a/test/stdout.js b/test/stdout.js
index 9b26ad4..260180f 100644
--- a/test/stdout.js
+++ b/test/stdout.js
@@ -1,15 +1,15 @@
-const test = require('ava')
+import test from 'ava'
-const fs = require('fs-extra')
-const path = require('path')
-const { exec } = require('child_process')
+import fs from 'fs-extra'
+import path from 'path'
+import { exec } from 'child_process'
-const read = require('./helpers/read.js')
+import read from './helpers/read.js'
test.cb('writes to stdout', (t) => {
const cp = exec(
`node ${path.resolve(
- 'bin/postcss'
+ 'index.js',
)} --parser sugarss -u postcss-import --no-map`,
(error, stdout, stderr) => {
if (error) t.end(error, stderr)
@@ -20,7 +20,7 @@ test.cb('writes to stdout', (t) => {
t.end()
})
.catch(t.end)
- }
+ },
)
fs.createReadStream('./test/fixtures/a.sss').pipe(cp.stdin)
diff --git a/test/stringifier.js b/test/stringifier.js
index 4256c79..69adaff 100644
--- a/test/stringifier.js
+++ b/test/stringifier.js
@@ -1,8 +1,8 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--stringifier works', async (t) => {
const output = tmp('output.sss')
diff --git a/test/syntax.js b/test/syntax.js
index 4658d69..b17e1fd 100644
--- a/test/syntax.js
+++ b/test/syntax.js
@@ -1,8 +1,8 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--syntax works', async (t) => {
const output = tmp('output.sss')
diff --git a/test/unchanged.js b/test/unchanged.js
new file mode 100644
index 0000000..4598530
--- /dev/null
+++ b/test/unchanged.js
@@ -0,0 +1,17 @@
+import fs from 'fs-extra'
+import test from 'ava'
+
+import cli from './helpers/cli.js'
+
+test('files are not saved if the contents are the same', async (t) => {
+ const input = 'test/fixtures/unchanged-input.css'
+ const output = 'test/fixtures/unchanged-output.css'
+ const intialStat = await fs.stat(output)
+
+ const { error, stderr } = await cli([input, '-o', output])
+
+ t.falsy(error, stderr)
+
+ const finalStat = await fs.stat(output)
+ t.is(finalStat.mtimeMs, intialStat.mtimeMs)
+})
diff --git a/test/use.js b/test/use.js
index 07901f6..c35d6cd 100644
--- a/test/use.js
+++ b/test/use.js
@@ -1,8 +1,8 @@
-const test = require('ava')
+import test from 'ava'
-const cli = require('./helpers/cli.js')
-const tmp = require('./helpers/tmp.js')
-const read = require('./helpers/read.js')
+import cli from './helpers/cli.js'
+import tmp from './helpers/tmp.js'
+import read from './helpers/read.js'
test('--use works', async (t) => {
const output = tmp('i.css')
diff --git a/test/watch.js b/test/watch.js
index 6012bf7..357174a 100644
--- a/test/watch.js
+++ b/test/watch.js
@@ -1,15 +1,17 @@
-const test = require('ava')
+import test from 'ava'
-const fs = require('fs-extra')
-const path = require('path')
-const { exec } = require('child_process')
-const chokidar = require('chokidar')
+import fs from 'fs-extra'
+import path from 'path'
+import { exec, spawn } from 'child_process'
+import chokidar from 'chokidar'
-const ENV = require('./helpers/env.js')
-const read = require('./helpers/read.js')
+import ENV from './helpers/env.js'
+import read from './helpers/read.js'
+import tmp from './helpers/tmp.js'
-// XXX: All the tests in this file are skipped on the CI; too flacky there
-const testCb = process.env.CI ? test.cb.skip : test.cb
+// XXX: All the tests in this file are skipped on the Windows CI; too flacky there
+const testCb =
+ process.env.CI && process.platform === 'win32' ? test.cb.skip : test.cb
testCb('--watch works', (t) => {
let cp
@@ -49,10 +51,8 @@ testCb('--watch works', (t) => {
watcher.on('ready', () => {
// Using exec() and quoting "*.css" to test watch's glob handling:
cp = exec(
- `node ${path.resolve(
- 'bin/postcss'
- )} "*.css" -o output.css --no-map -w`,
- { cwd: dir }
+ `node ${path.resolve('index.js')} "*.css" -o output.css --no-map -w`,
+ { cwd: dir },
)
cp.on('error', t.end)
cp.on('exit', (code) => {
@@ -62,10 +62,9 @@ testCb('--watch works', (t) => {
// Helper functions:
function isEqual(p, expected) {
- return Promise.all([
- read(path.join(dir, p)),
- read(expected),
- ]).then(([a, e]) => t.is(a, e))
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
function done(err) {
@@ -77,17 +76,14 @@ testCb('--watch works', (t) => {
}
})
.catch(t.end)
-
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
})
-testCb('--watch postcss.config.js', (t) => {
+testCb('--watch dependencies', (t) => {
let cp
t.plan(2)
- ENV('module.exports = {}', ['import.css', 'a.css'])
+ ENV('', ['import.css', 'a.css'])
.then((dir) => {
// Init watcher:
const watcher = chokidar.watch('.', {
@@ -100,19 +96,9 @@ testCb('--watch postcss.config.js', (t) => {
watcher.on('add', (p) => {
// Assert, then change the source file
if (p === 'output.css') {
- read(path.join(dir, p))
- .then((css) => {
- t.is(css, '@import "./a.css";\n')
-
- return fs.writeFile(
- path.join(dir, 'postcss.config.js'),
- `module.exports = {
- plugins: [
- require('postcss-import')()
- ]
- }`
- )
- })
+ isEqual(p, 'test/fixtures/a.css')
+ .then(() => read('test/fixtures/b.css'))
+ .then((css) => fs.writeFile(path.join(dir, 'a.css'), css))
.catch(done)
}
})
@@ -120,7 +106,7 @@ testCb('--watch postcss.config.js', (t) => {
// When the change is picked up:
watcher.on('change', (p) => {
if (p === 'output.css') {
- isEqual(p, 'test/fixtures/a.css')
+ isEqual(p, 'test/fixtures/b.css')
.then(() => done())
.catch(done)
}
@@ -130,9 +116,9 @@ testCb('--watch postcss.config.js', (t) => {
watcher.on('ready', () => {
cp = exec(
`node ${path.resolve(
- 'bin/postcss'
- )} import.css -o output.css -w --no-map`,
- { cwd: dir }
+ 'index.js',
+ )} import.css -o output.css -u postcss-import -w --no-map`,
+ { cwd: dir },
)
cp.on('error', t.end)
@@ -143,144 +129,396 @@ testCb('--watch postcss.config.js', (t) => {
// Helper functions:
function isEqual(p, expected) {
- return Promise.all([
- read(path.join(dir, p)),
- read(expected),
- ]).then(([a, e]) => t.is(a, e))
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
function done(err) {
try {
cp.kill()
} catch {}
-
t.end(err)
}
})
.catch(t.end)
-
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
})
-testCb('--watch dependencies', (t) => {
- let cp
+// Doesn't work on CI for some reason
+;(process.env.CI ? test.cb.skip : test.cb)(
+ "--watch doesn't exit on CssSyntaxError",
+ (t) => {
+ t.plan(0)
+
+ ENV('', ['a.css'])
+ .then((dir) => {
+ // Init watcher:
+ const watcher = chokidar.watch('.', {
+ cwd: dir,
+ ignoreInitial: true,
+ awaitWriteFinish: true,
+ })
+ watcher.on('add', (p) => {
+ if (p === 'output.css') {
+ // Change to invalid CSS
+ fs.writeFile(path.join(dir, 'a.css'), '.a { color: red').catch(done)
+ }
+ })
- t.plan(2)
+ let killed = false
+ const cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} a.css -o output.css -u postcss-import -w --no-map`,
+ { cwd: dir },
+ )
+ cp.on('error', t.end)
+ cp.stderr.on('data', (chunk) => {
+ // When error message is printed, kill the process after a timeout
+ if (~chunk.indexOf('Unclosed block')) {
+ setTimeout(() => {
+ killed = true
+ cp.kill()
+ }, 1000)
+ }
+ })
+ cp.on('exit', (code) => {
+ if (!killed)
+ return t.end(`Should not exit (exited with code ${code})`)
+ done()
+ })
- ENV('', ['import.css', 'a.css'])
- .then((dir) => {
- // Init watcher:
- const watcher = chokidar.watch('.', {
- cwd: dir,
- ignoreInitial: true,
- awaitWriteFinish: true,
- })
+ function done(err) {
+ try {
+ cp.kill()
+ } catch {}
- // On the first output:
- watcher.on('add', (p) => {
- // Assert, then change the source file
- if (p === 'output.css') {
- isEqual(p, 'test/fixtures/a.css')
- .then(() => read('test/fixtures/b.css'))
- .then((css) => fs.writeFile(path.join(dir, 'a.css'), css))
- .catch(done)
+ t.end(err)
}
})
+ .catch(t.end)
+ },
+)
+
+testCb('--watch does exit on closing stdin (Ctrl-D/EOF)', (t) => {
+ t.plan(1)
+
+ const cp = spawn(`./index.js test/fixtures/a.css -o ${tmp()} -w --no-map`, {
+ shell: true,
+ env: {
+ ...process.env,
+ FORCE_IS_TTY: true,
+ },
+ })
+
+ cp.on('error', t.end)
+ cp.on('exit', (code) => {
+ t.is(code, 0)
+ t.end()
+ })
+
+ cp.stdin.end()
+})
- // When the change is picked up:
- watcher.on('change', (p) => {
- if (p === 'output.css') {
- isEqual(p, 'test/fixtures/b.css')
- .then(() => done())
- .catch(done)
- }
- })
+testCb('--watch watches dependencies', (t) => {
+ let cp
- // Start postcss-cli:
- watcher.on('ready', () => {
- cp = exec(
- `node ${path.resolve(
- 'bin/postcss'
- )} import.css -o output.css -u postcss-import -w --no-map`,
- { cwd: dir }
- )
+ t.plan(2)
- cp.on('error', t.end)
- cp.on('exit', (code) => {
- if (code) t.end(code)
+ ENV('', ['s.css', 'a.css', 'b.css']).then((dir) => {
+ fs.writeFile(
+ path.join(dir, 'postcss.config.cjs'),
+ `
+ const fs = require('fs')
+ module.exports = {
+ plugins: [
+ (root, result) => {
+ const file = '${path.resolve(dir, 'a.css')}'
+ result.messages.push({
+ plugin: 'test',
+ type: 'dependency',
+ file,
+ parent: result.opts.from,
+ })
+ root.nodes = []
+ root.append(fs.readFileSync(file, 'utf8'))
+ return root
+ }
+ ]
+ }
+ `,
+ )
+ .then(() => {
+ // Init watcher:
+ const watcher = chokidar.watch('.', {
+ cwd: dir,
+ ignoreInitial: true,
+ awaitWriteFinish: true,
})
- })
- // Helper functions:
- function isEqual(p, expected) {
- return Promise.all([
- read(path.join(dir, p)),
- read(expected),
- ]).then(([a, e]) => t.is(a, e))
- }
-
- function done(err) {
- try {
- cp.kill()
- } catch {}
- t.end(err)
- }
- })
- .catch(t.end)
+ // On the first output:
+ watcher.on('add', (p) => {
+ // Assert, then change the source file
+ if (p === 'output.css') {
+ isEqual(p, 'test/fixtures/a.css')
+ .then(() => read('test/fixtures/b.css'))
+ .then((css) => fs.writeFile(path.join(dir, 'a.css'), css))
+ .catch(done)
+ }
+ })
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
-})
+ // When the change is picked up:
+ watcher.on('change', (p) => {
+ if (p === 'output.css') {
+ isEqual(p, 'test/fixtures/b.css')
+ .then(() => done())
+ .catch(done)
+ }
+ })
-testCb("--watch doesn't exit on CssSyntaxError", (t) => {
- t.plan(0)
+ // Start postcss-cli:
+ watcher.on('ready', () => {
+ // Using exec() and quoting "*.css" to test watch's glob handling:
+ cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} "s.css" -o output.css --no-map -w`,
+ { cwd: dir },
+ )
+ cp.on('error', t.end)
+ cp.on('exit', (code) => {
+ if (code) t.end(code)
+ })
+ })
- ENV('', ['a.css'])
- .then((dir) => {
- // Init watcher:
- const watcher = chokidar.watch('.', {
- cwd: dir,
- ignoreInitial: true,
- awaitWriteFinish: true,
- })
- watcher.on('add', (p) => {
- if (p === 'output.css') {
- // Change to invalid CSS
- fs.writeFile(path.join(dir, 'a.css'), '.a { color: red').catch(done)
+ // Helper functions:
+ function isEqual(p, expected) {
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
- })
- let killed = false
- const cp = exec(
- `node ${path.resolve('bin/postcss')} a.css -o output.css -w --no-map`,
- { cwd: dir }
- )
- cp.on('error', t.end)
- cp.stderr.on('data', (chunk) => {
- // When error message is printed, kill the process after a timeout
- if (~chunk.indexOf('Unclosed block')) {
- setTimeout(() => {
- killed = true
+ function done(err) {
+ try {
cp.kill()
- }, 1000)
+ } catch {}
+
+ t.end(err)
}
})
- cp.on('exit', (code) => {
- if (!killed) return t.end(`Should not exit (exited with code ${code})`)
- done()
- })
+ .catch(t.end)
+ })
+})
- function done(err) {
- try {
- cp.kill()
- } catch {}
+testCb('--watch watches directory dependencies', (t) => {
+ let cp
- t.end(err)
- }
- })
- .catch(t.end)
+ t.plan(2)
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
+ ENV('', ['s.css', 'base/level-1/b.css', 'base/level-1/level-2/a.css']).then(
+ (dir) => {
+ fs.writeFile(
+ path.join(dir, 'postcss.config.cjs'),
+ `
+ const fs = require('fs')
+ module.exports = {
+ plugins: [
+ (root, result) => {
+ result.messages.push({
+ plugin: 'test',
+ type: 'dir-dependency',
+ dir: '${path.resolve(dir, 'base')}',
+ parent: result.opts.from,
+ })
+ root.nodes = []
+ root.append(fs.readFileSync('${path.resolve(
+ dir,
+ 'base/level-1/level-2/a.css',
+ )}', 'utf8'))
+ return root
+ }
+ ]
+ }
+ `,
+ )
+ .then(() => {
+ // Init watcher:
+ const watcher = chokidar.watch('.', {
+ cwd: dir,
+ ignoreInitial: true,
+ awaitWriteFinish: true,
+ })
+
+ // On the first output:
+ watcher.on('add', (p) => {
+ // Assert, then change the source file
+ if (p === 'output.css') {
+ isEqual(p, 'test/fixtures/base/level-1/level-2/a.css')
+ .then(() => read('test/fixtures/base/level-1/b.css'))
+ .then((css) =>
+ fs.writeFile(
+ path.join(dir, 'base/level-1/level-2/a.css'),
+ css,
+ ),
+ )
+ .catch(done)
+ }
+ })
+
+ // When the change is picked up:
+ watcher.on('change', (p) => {
+ if (p === 'output.css') {
+ isEqual(p, 'test/fixtures/base/level-1/b.css')
+ .then(() => done())
+ .catch(done)
+ }
+ })
+
+ // Start postcss-cli:
+ watcher.on('ready', () => {
+ // Using exec() and quoting "*.css" to test watch's glob handling:
+ cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} "s.css" -o output.css --no-map -w`,
+ { cwd: dir },
+ )
+ cp.on('error', t.end)
+ cp.on('exit', (code) => {
+ if (code) t.end(code)
+ })
+ })
+
+ // Helper functions:
+ function isEqual(p, expected) {
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
+ }
+
+ function done(err) {
+ try {
+ cp.kill()
+ } catch {}
+
+ t.end(err)
+ }
+ })
+ .catch(t.end)
+ },
+ )
})
+
+testCb(
+ '--watch applies glob on dir-dependency (and excludes non matching files)',
+ (t) => {
+ let cp
+ let modifying = null // one of "unrelated.md", "a.css"
+
+ t.plan(1)
+
+ ENV('', [
+ 's.css',
+ 'base/level-1/b.css',
+ 'base/level-1/level-2/a.css',
+ 'base/level-1/level-2/unrelated.md',
+ ]).then((dir) => {
+ fs.writeFile(
+ path.join(dir, 'postcss.config.cjs'),
+ `
+ const fs = require('fs')
+ module.exports = {
+ plugins: [
+ (root, result) => {
+ result.messages.push({
+ plugin: 'test',
+ type: 'dir-dependency',
+ dir: '${path.resolve(dir, 'base')}',
+ glob: '**/*.css',
+ parent: result.opts.from,
+ })
+ root.nodes = []
+ root.append(fs.readFileSync('${path.resolve(
+ dir,
+ 'base/level-1/level-2/a.css',
+ )}', 'utf8'))
+ return root
+ }
+ ]
+ }
+ `,
+ )
+ .then(() => {
+ // Init watcher:
+ const watcher = chokidar.watch('.', {
+ cwd: dir,
+ ignoreInitial: true,
+ awaitWriteFinish: true,
+ })
+
+ // On the first output:
+ watcher.on('add', (p) => {
+ if (p === 'output.css') {
+ // Modify unwatched file, shouldn't trigger output
+ modifyUnwatched()
+ }
+ })
+
+ // When the change is picked up:
+ watcher.on('change', (p) => {
+ if (p === 'output.css') {
+ // Assert that change to output.css happened only after modifying the watched a.css
+ t.is(
+ modifying,
+ 'a.css',
+ `Unexpected change to ${p} after modifying ${modifying}`,
+ )
+ done()
+ } else if (p === 'base/level-1/level-2/unrelated.md') {
+ // Modify watched file next, should trigger output
+ setTimeout(modifyWatched, 250)
+ }
+ })
+
+ // Start postcss-cli:
+ watcher.on('ready', () => {
+ cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} "s.css" -o output.css --no-map -w`,
+ { cwd: dir },
+ )
+ cp.on('error', t.end)
+ cp.on('exit', (code) => {
+ if (code) t.end(code)
+ })
+ })
+
+ function modifyUnwatched() {
+ modifying = 'unrelated.md'
+ fs.writeFile(
+ path.join(dir, 'base/level-1/level-2/unrelated.md'),
+ 'Some modification',
+ ).catch(done)
+ }
+
+ function modifyWatched() {
+ modifying = 'a.css'
+ fs.writeFile(
+ path.join(dir, 'base/level-1/level-2/a.css'),
+ 'a { color: hotpink }',
+ ).catch(done)
+ }
+
+ function done(err) {
+ try {
+ cp.kill()
+ } catch {}
+
+ t.end(err)
+ }
+ })
+ .catch(t.end)
+ })
+ },
+)