diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..eb92fc9
--- /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, 22, 24]
+ os: [ubuntu-latest, windows-latest]
+ runs-on: ${{ matrix.os }}
+ steps:
+ - run: git config --global core.autocrlf input
+ - uses: actions/checkout@v7
+ - name: Use Node.js ${{ matrix.node }}
+ uses: actions/setup-node@v7
+ with:
+ node-version: ${{ matrix.node }}
+ - run: npm install
+ - run: npm run ci
diff --git a/.npmrc b/.npmrc
new file mode 100644
index 0000000..43c97e7
--- /dev/null
+++ b/.npmrc
@@ -0,0 +1 @@
+package-lock=false
diff --git a/.prettierignore b/.prettierignore
new file mode 100644
index 0000000..3018b3a
--- /dev/null
+++ b/.prettierignore
@@ -0,0 +1 @@
+.tmp/
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 58d5476..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-sudo: false
-language: node_js
-node_js:
- - node
- - 6
- - 4
-after_success:
-- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4a7199e..4fed6ca 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,28 +1,161 @@
-4.0.0 / 2017-05-09
-==================
+# 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))
+- Organize and clarify `--help` text ([#336](https://github.com/postcss/postcss-cli/pull/336))
+- Update dependencies
+
+# 7.1.1 / 2020-04-27
+
+- Fix bug where external source maps were not properly generated ([#324](https://github.com/postcss/postcss-cli/pull/324))
+- Update dependencies
+
+# 7.1.0 / 2020-01-09
+
+- Respect `map.annotation` option when set to a string ([#307](https://github.com/postcss/postcss-cli/pull/307))
+
+# 7.0.0 / 2020-01-07
+
+- **BREAKING:** Drop support for Node 6 & 8
+- Update `globby`, which may be a breaking change for a small number of users using certain glob patterns, see https://github.com/mrmlnc/fast-glob#pattern-syntax for details ([#303](https://github.com/postcss/postcss-cli/pull/303))
+- Update dependencies
+
+# 6.1.3 / 2019-07-08
+
+- 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
+
+- Upgrade dependency globby to fix bug in sub-dependency ([#265](https://github.com/postcss/postcss-cli/issues/265), [#270](https://github.com/postcss/postcss-cli/pull/270))
+
+# 6.1.1 / 2019-01-04
+
+- Fix empty lines being printed in some cases ([#256](https://github.com/postcss/postcss-cli/issues/256), [#257](https://github.com/postcss/postcss-cli/pull/257))
+
+# 6.1.0 / 2018-12-19
+
+- Add `--include-dotfiles` flag to allow globs to match dotfiles. ([#254](https://github.com/postcss/postcss-cli/pull/254))
+- Only log warning messages. This has always been intended behavior, but it wasn't working as expected. ([#244](https://github.com/postcss/postcss-cli/pull/244))
+
+# 6.0.1 / 2018-10-17
+
+- Better error handling for errors thrown by plugins ([#242](https://github.com/postcss/postcss-cli/issues/242), [#243](https://github.com/postcss/postcss-cli/pull/243))
+- Update dependencies
+- Clarify docs ([#233](https://github.com/postcss/postcss-cli/issues/233))
+
+# 6.0.0 / 2018-07-18
+
+- Drop support for Node 4
+- Upgrade to postcss v7 ([release notes](https://github.com/postcss/postcss/blob/master/CHANGELOG.md#70-president-amy))
+- Upgrade to postcss-load-config v2 ([release notes](https://github.com/michael-ciniawsky/postcss-load-config/blob/master/CHANGELOG.md#200-2018-07-10))
+
+# 5.0.1 / 2018-06-18
+
+- Shallow copy options object; fixes a few edge cases
+- Adjust options for file watching to play better with some editors
+
+# 5.0.0 / 2018-02-06
+
+- Now allows passing a directory as the input (all files in the directory will be processed)
+- The CLI is now silent by default; added `--verbose` flag for if you want noisy logs
+- Doesn't exit watch mode when there's an error in the plugin chain
+- Removed non-obvious shorthand arguments (`-x`, `-p`, `-s`, `-t`, `-e`, `-b`, & `-c`). Also removed `-v` as an alias for `--version`.
+- Prevent stupid option combinations like `--dir` & `-o`, and `--watch` & `--replace`
+- Doesn't allow `--watch` when writing to STDOUT
+
+# 4.1.1 / 2017-08-17
+
+- Fixed bug with `--config`
+- Upgraded dependencies
+
+# 4.1.0 / 2017-06-10
+
+- Can now pass a number to `--poll` to set poll interval
+- Updated `postcss-reporter` dependency to v4.0.0
+
+# 4.0.0 / 2017-05-09
- **BREAKING:** Upgrade postcss to v6.x
-3.2.0 / 2017-04-21
-==================
+# 3.2.0 / 2017-04-21
- Added `--base` CLI option for keeping directory structure
-3.1.1 / 2017-04-04
-==================
+# 3.1.1 / 2017-04-04
- Fixed `files` property in `package.json`; `lib/` folder wasn't included in v3.1.0
-3.1.0 / 2017-04-04
-==================
+# 3.1.0 / 2017-04-04
- Improved incremental rebuilds for better performance in watch mode.
- Switched to `read-cache` for file reading for better performance.
- Set a dummy filename when reading from stdin to help plugins like autoprefixer find config files.
- Updated `fs-promise` dependency.
-3.0.0 / 2017-03-15
-==========
+# 3.0.0 / 2017-03-15
## Changes since 3.0.0-beta
@@ -41,10 +174,10 @@
- Fix `--watch`'s glob handling
- Fix error handling
-
## Changes since v2.6.0
### Breaking Changes
+
- Uses https://github.com/michael-ciniawsky/postcss-load-config for config files. Dropped support for the v2 config file format.
- Can't set input files in config file; pass input files on the command line instead.
- `--use` accepts a list of plugins. This may cause issues if you have your list of css files at the end of your command.
@@ -56,15 +189,16 @@
- Changed behavior of the `--poll` option; no longer accepts an integer `interval`.
### New Features
+
- `--ext` (`-x`) option allows you to set the file extensions for your output files when using `--dir`.
- `--env` allows you to set `NODE_ENV` in a cross-platform manner.
Migration guide for upgrading from v2: https://github.com/postcss/postcss-cli/wiki/Migrating-from-v2-to-v3
-3.0.0-beta / 2017-03-17
-==========
+# 3.0.0-beta / 2017-03-17
## Breaking Changes
+
- Uses https://github.com/michael-ciniawsky/postcss-load-config for config files. Dropped support for the v2 config file format.
- Can't set input files in config file; pass input files on the command line instead.
- `--use` accepts a list of plugins. This may cause issues if you have your list of css files at the end of your command.
@@ -75,159 +209,135 @@ Migration guide for upgrading from v2: https://github.com/postcss/postcss-cli/wi
- Removed the global `watchCSS` handler, plugins that import/rely on other files should use a `dependency` message instead.
## New Features
+
- `--ext` (`-x`) option allows you to set the file extensions for your output files when using `--dir`.
- `--env` allows you to set `NODE_ENV` in a cross-platform manner.
Migration guide: https://github.com/postcss/postcss-cli/wiki/Migrating-from-v2-to-v3
-2.6.0 / 2016-08-30
-==================
+# 2.6.0 / 2016-08-30
- * Add log option
- * Update postcss-import to v8.1.2 from v7.1.0
- * Update globby to v4.1.0 from v3.0.1
- * Update postcss-url to v5.1.2 from v4.0.0
- * Update jshint to v2.9.2 from v2.6.3
- * Update chokidar to v1.5.1 from v1.0.3
- * Update yargs to v4.7.1 from v3.32.0
- * Support es6 export
- * Allow running without plugins
- * Add test for --poll
- * Add --poll flag
+- Add log option
+- Update postcss-import to v8.1.2 from v7.1.0
+- Update globby to v4.1.0 from v3.0.1
+- Update postcss-url to v5.1.2 from v4.0.0
+- Update jshint to v2.9.2 from v2.6.3
+- Update chokidar to v1.5.1 from v1.0.3
+- Update yargs to v4.7.1 from v3.32.0
+- Support es6 export
+- Allow running without plugins
+- Add test for --poll
+- Add --poll flag
-2.5.2 / 2016-04-18
-==================
+# 2.5.2 / 2016-04-18
- * Fix typo in help message: -use => [--use|-u]
- * npm install --save mkdirp
- * Support mkdirp to create dest path if it doesn't exists
- * Fix booleans in config file
+- Fix typo in help message: -use => [--use|-u]
+- npm install --save mkdirp
+- Support mkdirp to create dest path if it doesn't exists
+- Fix booleans in config file
-2.5.1 / 2016-02-11
-==================
+# 2.5.1 / 2016-02-11
- * fix `input` argument
+- fix `input` argument
-2.5.0 / 2016-01-30
-==================
+# 2.5.0 / 2016-01-30
- * move to postcss/postcss-cli repository
- * Update Readme.md
+- move to postcss/postcss-cli repository
+- Update Readme.md
-2.4.1 / 2016-01-27
-==================
+# 2.4.1 / 2016-01-27
- * improve warning disply format
+- improve warning disply format
-2.4.0 / 2016-01-15
-==================
+# 2.4.0 / 2016-01-15
- * add support for source maps
+- add support for source maps
-2.3.3 / 2015-12-28
-==================
+# 2.3.3 / 2015-12-28
- * add usage example for `local-plugins` option in config file
+- add usage example for `local-plugins` option in config file
-2.3.2 / 2015-10-27
-==================
+# 2.3.2 / 2015-10-27
- * auto-configure postcss-import support
- * add support for watching multiple entry points
+- auto-configure postcss-import support
+- add support for watching multiple entry points
-2.3.1 / 2015-10-25
-==================
+# 2.3.1 / 2015-10-25
- * update Travis config
- * upgrade postcss-import dependency - fix deprecation warnings during make test-watch
+- update Travis config
+- upgrade postcss-import dependency - fix deprecation warnings during make test-watch
-2.3.0 / 2015-10-24
-==================
+# 2.3.0 / 2015-10-24
- * add --local-plugins option that lets postcss-cli to look for plugins in current directory
+- add --local-plugins option that lets postcss-cli to look for plugins in current directory
-2.2.0 / 2015-10-09
-==================
+# 2.2.0 / 2015-10-09
- * add support for --replace|-r - if used input files are replaced with generated output
- * refactor support for custom syntax options
+- add support for --replace|-r - if used input files are replaced with generated output
+- refactor support for custom syntax options
-2.1.1 / 2015-10-08
-==================
+# 2.1.1 / 2015-10-08
- * add globby to support wildcards in Windows
- * remove obsolete note on postcss-import compatibility
+- add globby to support wildcards in Windows
+- remove obsolete note on postcss-import compatibility
-2.1.0 / 2015-09-01
-==================
+# 2.1.0 / 2015-09-01
- * add support for PostCSS 5.0 custom syntax options
+- add support for PostCSS 5.0 custom syntax options
-2.0.0 / 2015-08-24
-==================
+# 2.0.0 / 2015-08-24
- * remove support for --safe option
- * switch to using postcss 5.x
+- remove support for --safe option
+- switch to using postcss 5.x
-1.5.0 / 2015-07-20
-==================
+# 1.5.0 / 2015-07-20
- * add watch mode (-w|--watch) in which postcss-cli observes and recompiles inputs whenever they change
- * update neo-async dependency to released version
- * update postcss-url dependency (used in tests only)
+- add watch mode (-w|--watch) in which postcss-cli observes and recompiles inputs whenever they change
+- update neo-async dependency to released version
+- update postcss-url dependency (used in tests only)
-1.4.0 / 2015-07-12
-==================
+# 1.4.0 / 2015-07-12
- * allow specifying input file via config file
- * allow specifying -u|--use via config file
+- allow specifying input file via config file
+- allow specifying -u|--use via config file
-1.3.1 / 2015-05-03
-==================
+# 1.3.1 / 2015-05-03
- * update npm keyword: postcssrunner -> postcss-runner
+- update npm keyword: postcssrunner -> postcss-runner
-1.3.0 / 2015-04-28
-==================
+# 1.3.0 / 2015-04-28
- * add support for stdin/stdout if no input/output file specified
+- add support for stdin/stdout if no input/output file specified
-1.2.1 / 2015-04-20
-==================
+# 1.2.1 / 2015-04-20
- * fix typo in readme
+- fix typo in readme
-1.2.0 / 2015-04-02
-==================
+# 1.2.0 / 2015-04-02
- * display warnings and errors
- * stop testing on node 0.10
+- display warnings and errors
+- stop testing on node 0.10
-1.1.0 / 2015-03-28
-==================
+# 1.1.0 / 2015-03-28
- * prefer postcss async API if available
+- prefer postcss async API if available
-1.0.0 / 2015-03-22
-==================
+# 1.0.0 / 2015-03-22
- * use official yargs version
- * add support for multiple input files
+- use official yargs version
+- add support for multiple input files
-0.3.0 / 2015-03-19
-==================
+# 0.3.0 / 2015-03-19
- * support JS format as plugins config
+- support JS format as plugins config
-0.2.0 / 2015-03-13
-==================
+# 0.2.0 / 2015-03-13
- * use autoprefixer instead of autoprefixer-core
- * change short options for --use from `p` to `u`
- * add -v|--version support
- * add --safe option to enable postcss safe mode
+- use autoprefixer instead of autoprefixer-core
+- change short options for --use from `p` to `u`
+- add -v|--version support
+- add --safe option to enable postcss safe mode
-0.1.0 / 2015-03-11
-==================
+# 0.1.0 / 2015-03-11
- * initial implementaion
+- initial implementation
diff --git a/README.md b/README.md
index 63b052f..a390e3e 100644
--- a/README.md
+++ b/README.md
@@ -1,89 +1,97 @@
[![npm][npm]][npm-url]
[![node][node]][node-url]
-[![deps][deps]][deps-url]
[![tests][tests]][tests-url]
[![cover][cover]][cover-url]
-[![code style][style]][style-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
-```bash
-postcss [input.css] [OPTIONS] [-o|--output output.css] [-w|--watch]
-```
-
-The input may also be a glob:
-
-```bash
-postcss src/*.css [OPTIONS]
-```
-
-Recursively read a directory:
-
-```bash
-postcss src/**/*.css [OPTIONS]
```
+Usage:
+ postcss [input.css] [OPTIONS] [-o|--output output.css] [--watch|-w]
+ postcss ... [OPTIONS] --dir [--watch|-w]
+ postcss [OPTIONS] --dir [--watch|-w]
+ postcss [OPTIONS] --dir [--watch|-w]
+ postcss ... [OPTIONS] --replace
+
+Basic options:
+ -o, --output Output file [string]
+ -d, --dir Output directory [string]
+ -r, --replace Replace (overwrite) the input file [boolean]
+ -m, --map Create an external sourcemap
+ --no-map Disable the default inline sourcemaps
+ -w, --watch Watch files for changes and recompile as needed [boolean]
+ --verbose Be verbose [boolean]
+ --env A shortcut for setting NODE_ENV [string]
+
+Options for use without a config file:
+ -u, --use List of postcss plugins to use [array]
+ --parser Custom postcss parser [string]
+ --stringifier Custom postcss stringifier [string]
+ --syntax Custom postcss syntax [string]
+
+Options for use with --dir:
+ --ext Override the output file extension; for use with --dir [string]
+ --base Mirror the directory structure relative to this path in the output
+ directory, for use with --dir [string]
+
+Advanced options:
+ --include-dotfiles Enable glob to match files/dirs that begin with "."
+ [boolean]
+ --poll Use polling for file watching. Can optionally pass polling
+ interval; default 100 ms
+ --config Set a custom directory to look for a config file [string]
+
+Options:
+ --version Show version number [boolean]
+ -h, --help Show help [boolean]
+
+Examples:
+ postcss input.css -o output.css Basic usage
+ postcss src/**/*.css --base src --dir build Glob Pattern & output
+ cat input.css | postcss -u autoprefixer > output.css Piping input & output
+
+If no input files are passed, it reads from stdin. If neither -o, --dir, or
+--replace is passed, it writes to stdout.
-> ⚠️ If there are multiple input files, the --dir or --replace option must be passed.
+If there are multiple input files, the --dir or --replace option must be passed.
-```bash
-cat input.css | postcss [OPTIONS] > output.css
+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.
```
-> ⚠️ If no input files are passed, it reads from stdin. If neither -o, --dir, or
---replace is passed, it writes to stdout.
-
-Options
-
-|Name|Type|Default|Description|
-|:---|:--:|:-----:|:----------|
-|`-d, --dir`|`{String}`|`undefined`|Output Directory|
-|`-b, --base`|`{String}`|`undefined`|Use together with `--dir` for keeping directory structure.|
-|`-x, --ext`|`{String}`|`extname(output)`|Output File Extension|
-|`-o, --output`|`{String}`|`undefined`|Output File|
-|`-r, --replace`|`{String}`|`undefined`|Replace Input <=> Output|
-|`-p, --parser`|`{String}`|`undefined`|Custom PostCSS Parser|
-|`-s, --syntax`|`{String}`|`undefined`|Custom PostCSS Syntax|
-|`-t, --stringifier`|`{String}`|`undefined`|Custom PostCSS Stringifier|
-|`-w, --watch`|`{Boolean}`|`false`|Enable Watch Mode|
-|`--poll`|`{Boolean}`|`false`|Use polling for file watching|
-|`-u, --use`|`{Array}`|`[]`|PostCSS Plugins|
-|`-m, --map`|`{Boolean}`|`{ inline: true }`|External Sourcemaps|
-|`--no-map`|`{Boolean}`|`false`|Disable Sourcemaps|
-|`-e, --env`|`{String}`|`process.env.NODE_ENV`|Sets `$NODE_ENV`|
-|`-c, --config`|`{String}`|`dirname(file)`|PostCSS Config Path `postcss.config.js`|
-|`-h, --help`|`{Boolean}`|`false`|CLI Help|
-|`-v, --version`|`{Boolean}`|`false`|CLI Version|
-
-
-> ℹ️ More details on custom parsers, stringifiers and syntaxes, can be found [here](https://github.com/postcss/postcss#syntaxes).
+> ℹ️ More details on custom parsers, stringifiers and syntaxes, can be found [here](https://github.com/postcss/postcss#syntaxes).
### [Config](https://github.com/michael-ciniawsky/postcss-load-config)
If you need to pass options to your plugins, or have a long plugin chain, you'll want to use a configuration file.
**postcss.config.js**
+
```js
module.exports = {
parser: 'sugarss',
plugins: [
- require('postcss-import')({...options}),
- require('postcss-url')({ url: 'copy', useHash: true })
- ]
+ require('postcss-import')({ ...options }),
+ require('postcss-url')({ url: 'copy', useHash: true }),
+ ],
}
```
@@ -91,63 +99,53 @@ 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|
-|:--:|:--:|:-----:|:----------|
-|`env`|`{String}`|`'development'`|process.env.NODE_ENV|
-|`file`|`{Object}`|`dirname, basename, extname`|File|
-|`options`|`{Object}`|`map, parser, syntax, stringifier`|PostCSS Options|
+| Name | Type | Default | Description |
+| :-------: | :--------: | :--------------------------------: | :------------------- |
+| `env` | `{String}` | `'development'` | process.env.NODE_ENV |
+| `file` | `{Object}` | `dirname, basename, extname` | File |
+| `options` | `{Object}` | `map, parser, syntax, stringifier` | PostCSS Options |
**postcss.config.js**
+
```js
module.exports = (ctx) => ({
map: ctx.options.map,
parser: ctx.file.extname === '.sss' ? 'sugarss' : false,
plugins: {
- 'postcss-import': { root: ctx.file.dirname }),
- 'cssnano': ctx.env === 'production' ? {} : false
- }
+ 'postcss-import': { root: ctx.file.dirname },
+ cssnano: ctx.env === 'production' ? {} : false,
+ },
})
```
-> ⚠️ If you want to set options via CLI, it's mandatory to reference `ctx.options` in `postcss.config.js`
-
+> ⚠️ If you want to set options via CLI, it's mandatory to reference `ctx.options` in `postcss.config.js`
```bash
postcss input.sss -p sugarss -o output.css -m
```
**postcss.config.js**
+
```js
module.exports = (ctx) => ({
map: ctx.options.map,
parser: ctx.options.parser,
plugins: {
'postcss-import': { root: ctx.file.dirname },
- 'cssnano': ctx.env === 'production' ? {} : false
- }
+ cssnano: ctx.env === 'production' ? {} : false,
+ },
})
```
-
[npm]: https://img.shields.io/npm/v/postcss-cli.svg
[npm-url]: https://npmjs.com/package/postcss-cli
-
[node]: https://img.shields.io/node/v/postcss-cli.svg
[node-url]: https://nodejs.org/
-
-[deps]: https://img.shields.io/gemnasium/postcss/postcss-cli.svg
-[deps-url]: https://gemnasium.com/postcss/postcss-cli
-
-[tests]: http://img.shields.io/travis/postcss/postcss-cli/master.svg
-[tests-url]: https://travis-ci.org/postcss/postcss-cli
-
-[style]: https://img.shields.io/badge/code%20style-standard-yellow.svg
-[style-url]: http://standardjs.com/
-
+[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
[chat-url]: https://gitter.im/postcss/postcss
diff --git a/appveyor.yml b/appveyor.yml
deleted file mode 100644
index 2f9467a..0000000
--- a/appveyor.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-environment:
- matrix:
- - nodejs_version: "7"
- - nodejs_version: "6"
- - nodejs_version: "4"
-
-version: "{build}"
-build: off
-deploy: off
-
-install:
- - ps: Install-Product node $env:nodejs_version
- - npm install
-
-test_script:
- - node --version
- - npm --version
- - npm test
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 aa9d5ed..7cca0e5
--- a/index.js
+++ b/index.js
@@ -1,175 +1,102 @@
-'use strict'
-
-const fs = require('fs-extra')
-const path = require('path')
-
-const ora = require('ora')
-const stdin = require('get-stdin')
-const read = require('read-cache')
-const chalk = require('chalk')
-const globber = require('globby')
-const chokidar = require('chokidar')
-
-const postcss = require('postcss')
-const postcssrc = require('postcss-load-config')
-const reporter = require('postcss-reporter/lib/formatter')()
-
-const depGraph = require('./lib/depGraph')
-
-const logo = `
- /|\\
- // //
- // //
- //___*___*___//
- //--*---------*--//
- /|| * * ||/
- // ||* *|| //
- // || * * || //
- //_____||___*_________*___||_____//
-`
-
-const version = () => {
- const cli = require('./package.json').version
-
- return chalk.bold.red(`
- /|\\
- // //
- // //
- //___*___*___//
- //--*---------*--//
- /|| * * ||/
- // ||* v${cli} *|| //
- // || * * || //
- //_____||___*_________*___||_____//
- `)
-}
+#!/usr/bin/env node
-const argv = require('yargs')
- .usage(
-`${chalk.bold.red(logo)}
-Usage:
-
- $0 [input.css] [OPTIONS] [--output|-o output.css] [--watch]`
-)
- .option('o', {
- alias: 'output',
- desc: 'Output file',
- type: 'string'
- })
- .option('d', {
- alias: 'dir',
- desc: 'Output directory',
- type: 'string'
- })
- .option('r', {
- alias: 'replace',
- desc: 'Replace (overwrite) the input file',
- type: 'boolean'
- })
- .option('u', {
- alias: 'use',
- desc: 'List of postcss plugins to use',
- type: 'array'
- })
- .option('p', {
- alias: 'parser',
- desc: 'Custom postcss parser',
- type: 'string'
- })
- .option('t', {
- alias: 'stringifier',
- desc: 'Custom postcss stringifier',
- type: 'string'
- })
- .option('s', {
- alias: 'syntax',
- desc: 'Custom postcss syntax',
- type: 'string'
- })
- .option('w', {
- alias: 'watch',
- desc: 'Watch files for changes and recompile as needed',
- type: 'boolean'
- })
- .option('poll', {
- desc: 'Use polling for file watching',
- type: 'boolean'
- })
- .option('x', {
- alias: 'ext',
- desc: 'Override the output file extension',
- type: 'string',
- coerce (ext) {
- if (ext.indexOf('.') !== 0) return '.' + ext
- return ext
- }
- })
- .option('e', {
- alias: 'env',
- desc: 'A shortcut for setting NODE_ENV',
- type: 'string'
- })
- .option('b', {
- alias: 'base',
- desc: 'Mirror the directory structure relative to this path in the output directory, this only works together with --dir',
- type: 'string'
- })
- .option('c', {
- alias: 'config',
- desc: 'Set a custom path to look for a config file',
- type: 'string'
- })
- .alias('m', 'map')
- .describe('m', 'Create an external sourcemap')
- .describe('no-map', 'Disable the default inline sourcemaps')
- .version(version).alias('v', 'version')
- .help('h').alias('h', 'help')
- .example('$0 input.css -o output.css', 'Basic usage')
- .example('cat input.css | $0 -u autoprefixer > output.css', '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.
-
-If there are multiple input files, the --dir or --replace option must be passed.
-
-For more details, please see https://github.com/postcss/postcss-cli`
- )
- .argv
+import fs from 'node:fs/promises'
+import path from 'path'
+
+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'
+
+import postcss from 'postcss'
+import postcssrc from 'postcss-load-config'
+import postcssReporter from 'postcss-reporter/lib/formatter.js'
+
+import argv from './lib/args.js'
+import createDependencyGraph from './lib/DependencyGraph.js'
+import getMapfile from './lib/getMapfile.js'
-let dir = argv.dir
+const reporter = postcssReporter()
+const depGraph = createDependencyGraph()
let input = argv._
-let output = argv.output
+const { dir, output } = argv
if (argv.map) argv.map = { inline: false }
-const spinner = ora()
-
-let config = {
- 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) {
- error(`Plugin Error: Cannot find module '${plugin}'`)
- }
- })
- : []
+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
+
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 (input && input.length) return globber(input)
+ 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 (argv.replace || argv.dir) error('Input Error: Cannot use --dir or --replace when reading from stdin')
+ if (input && input.length) {
+ 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',
+ )
+ }
if (argv.watch) {
error('Input Error: Cannot run in watch mode when reading from stdin')
@@ -183,10 +110,12 @@ Promise.resolve()
}
if (i.length > 1 && !argv.dir && !argv.replace) {
- error('Input Error: Must use --dir or --replace with multiple input files')
+ error(
+ 'Input Error: Must use --dir or --replace with multiple input files',
+ )
}
- if (i[0] !== 'stdin') i = i.map(i => path.resolve(i))
+ if (i[0] !== 'stdin') i = i.map((i) => path.resolve(i))
input = i
@@ -194,92 +123,108 @@ Promise.resolve()
})
.then((results) => {
if (argv.watch) {
- const watcher = chokidar.watch(
- input.concat(dependencies(results)),
- { usePolling: argv.poll }
- )
-
- if (config.file) watcher.add(config.file)
-
- watcher
- .on('ready', (file) => console.warn(chalk.bold.cyan('Waiting for file changes...')))
- .on('change', (file) => {
- let recompile = []
-
- if (~input.indexOf(file)) recompile.push(file)
-
- recompile = recompile.concat(
- depGraph.dependantsOf(file).filter(file => ~input.indexOf(file))
- )
-
- if (!recompile.length) recompile = input
-
- return files(recompile)
- .then((results) => watcher.add(dependencies(results)))
- .then(() => console.warn(chalk.bold.cyan('Waiting for file changes...')))
- .catch(error)
- })
+ const printMessage = () =>
+ 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,
+ awaitWriteFinish: {
+ stabilityThreshold: 50,
+ pollInterval: 10,
+ },
+ })
+
+ if (configFile) watcher.add(configFile)
+
+ watcher.on('ready', printMessage).on('change', (file) => {
+ let recompile = []
+
+ if (input.includes(file)) recompile.push(file)
+
+ const dependants = depGraph
+ .dependantsOf(file)
+ .concat(getAncestorDirs(file).flatMap(depGraph.dependantsOf))
+
+ recompile = recompile.concat(
+ dependants.filter((file) => input.includes(file)),
+ )
+
+ if (!recompile.length) recompile = input
+
+ return files([...new Set(recompile)])
+ .then((results) => watcher.add(dependencies(results)))
+ .then(printMessage)
+ .catch(error)
+ })
}
})
- .catch(error)
+ .catch((err) => {
+ error(err)
-function rc (ctx, path) {
- if (argv.use) return Promise.resolve()
+ process.exit(1)
+ })
+
+function rc(ctx, path) {
+ if (argv.use) return Promise.resolve(cliConfig)
return postcssrc(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')
+ error(
+ 'Config Error: Can not set from or to options in config file, use CLI arguments instead',
+ )
}
- config = rc
+ configFile = rc.file
+ return rc
})
.catch((err) => {
- if (err.message.indexOf('No PostCSS Config found') === -1) throw err
+ if (!err.message.includes('No PostCSS Config found')) throw err
})
}
-function files (files) {
- if (typeof files === 'string') files = [ files ]
+function files(files) {
+ if (typeof files === 'string') files = [files]
- return Promise.all(files.map((file) => {
- if (file === 'stdin') {
- return stdin()
- .then((content) => {
+ return Promise.all(
+ files.map((file) => {
+ if (file === 'stdin') {
+ 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))
- }))
+ return read(file).then((content) => css(content, file))
+ }),
+ )
}
-function css (css, file) {
- const ctx = { options: config.options }
+function css(css, file) {
+ const ctx = { options: cliConfig.options }
if (file !== 'stdin') {
ctx.file = {
dirname: path.dirname(file),
basename: path.basename(file),
- extname: path.extname(file)
+ extname: path.extname(file),
}
if (!argv.config) argv.config = path.dirname(file)
}
- const relativePath = file !== 'stdin' ? path.relative(path.resolve(), file) : file
+ const relativePath =
+ file !== 'stdin' ? path.relative(path.resolve(), file) : file
if (!argv.config) argv.config = process.cwd()
const time = process.hrtime()
- spinner.text = `Processing ${relativePath}`
- spinner.start()
+ printVerbose(pc.cyan(`Processing ${pc.bold(relativePath)}...`))
return rc(ctx, argv.config)
- .then(() => {
- let options = config.options
+ .then((config) => {
+ config = config || cliConfig
+ const options = { ...config.options }
if (file === 'stdin' && output) file = output
@@ -287,19 +232,22 @@ function css (css, file) {
options.from = file === 'stdin' ? path.join(process.cwd(), 'stdin') : file
if (output || dir || argv.replace) {
- options.to = output || (argv.replace ? file : path.join(dir, argv.base ? file.replace(path.resolve(argv.base), '') : path.basename(file)))
+ const base = argv.base
+ ? file.replace(path.resolve(argv.base), '')
+ : path.basename(file)
+ options.to = output || (argv.replace ? file : path.join(dir, base))
if (argv.ext) {
- options.to = options.to
- .replace(path.extname(options.to), argv.ext)
+ options.to = options.to.replace(path.extname(options.to), argv.ext)
}
options.to = path.resolve(options.to)
}
if (!options.to && config.options.map && !config.options.map.inline) {
- spinner.fail()
- error('Output Error: Cannot output external sourcemaps when writing to STDOUT')
+ error(
+ 'Output Error: Cannot output external sourcemaps when writing to STDOUT',
+ )
}
return postcss(config.plugins)
@@ -308,87 +256,97 @@ 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) {
- tasks.push(
- fs.outputFile(
- options.to
- .replace(
- path.extname(options.to),
- path.extname(options.to) + '.map'
- ),
- result.map
- )
- )
+ const mapfile = getMapfile(options)
+ tasks.push(outputFile(mapfile, result.map.toString()))
}
- } else {
- spinner.text = chalk.bold.green(
- `Finished ${relativePath} (${Math.round(process.hrtime(time)[1] / 1e6)}ms)`
+ } else process.stdout.write(result.css, 'utf8')
+
+ return Promise.all(tasks).then(() => {
+ const prettyTime = prettyHrtime(process.hrtime(time))
+ printVerbose(
+ pc.green(
+ `Finished ${pc.bold(relativePath)} in ${pc.bold(prettyTime)}`,
+ ),
)
- spinner.succeed()
- return process.stdout.write(result.css, 'utf8')
- }
-
- return Promise.all(tasks)
- .then(() => {
- spinner.text = chalk.bold.green(
- `Finished ${relativePath} (${Math.round(process.hrtime(time)[1] / 1e6)}ms)`
- )
- if (result.warnings().length) {
- spinner.fail()
- console.warn(reporter(result))
- } else spinner.succeed()
-
- return result
- })
+
+ const messages = result.warnings()
+ if (messages.length) {
+ console.warn(reporter({ ...result, messages }))
+ }
+
+ return result
+ })
})
- }).catch((err) => {
- spinner.fail()
+ })
+ .catch((err) => {
throw err
})
-}
-function dependencies (results) {
- if (!Array.isArray(results)) results = [ results ]
+ async function outputFile(file, string) {
+ const fileExists = await fs.access(file).then(
+ () => true,
+ () => false,
+ )
+ const currentValue = fileExists ? await fs.readFile(file, 'utf8') : null
+ if (currentValue === string) return
+ await fs.mkdir(path.dirname(file), { recursive: true })
+ return fs.writeFile(file, string)
+ }
+}
- const messages = []
+function dependencies(results) {
+ if (!Array.isArray(results)) results = [results]
- results.forEach((result) => {
- if (result.messages <= 0) return
+ return results.flatMap((result) => {
+ if (result.messages.length <= 0) return []
- result.messages
- .filter((msg) => msg.type === 'dependency' ? msg : '')
+ return result.messages
+ .filter(
+ (msg) => msg.type === 'dependency' || msg.type === 'dir-dependency',
+ )
.map(depGraph.add)
- .forEach((dependency) => messages.push(dependency.file))
+ .map((dependency) => {
+ if (dependency.type === 'dir-dependency') {
+ return dependency.glob
+ ? path.join(dependency.dir, dependency.glob)
+ : dependency.dir
+ }
+
+ return dependency.file
+ })
})
+}
- return messages
+function printVerbose(message) {
+ if (argv.verbose) console.warn(message)
}
-function error (err) {
+function error(err) {
+ // Seperate error from logging output
+ if (argv.verbose) console.error()
+
if (typeof err === 'string') {
- spinner.fail(chalk.bold.red(err))
+ console.error(pc.red(err))
} else if (err.name === 'CssSyntaxError') {
- console.error('\n')
-
- spinner.text = spinner.text.replace('Processing ', '')
- spinner.fail(chalk.bold.red(`Syntax Error: ${spinner.text}`))
-
- if (err.file) {
- err.message = err.message.substr(err.file.length + 1)
- } else {
- err.message = err.message.replace(':', '')
- }
-
- err.message = err.message.replace(/:\s/, '] ')
-
- console.error('\n', chalk.bold.red(`[${err.message}`))
- console.error('\n', err.showSourceCode(), '\n\n')
-
- if (argv.watch) return
+ console.error(err.toString())
} else {
console.error(err)
}
+ // Watch mode shouldn't exit on error
+ 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
new file mode 100644
index 0000000..12d14fa
--- /dev/null
+++ b/lib/args.js
@@ -0,0 +1,115 @@
+import yargs from 'yargs'
+
+const { argv } = yargs(process.argv.slice(2))
+ .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`,
+ )
+ .group(
+ ['o', 'd', 'r', 'map', 'no-map', 'watch', 'verbose', 'env'],
+ 'Basic options:',
+ )
+ .option('o', {
+ alias: 'output',
+ desc: 'Output file',
+ type: 'string',
+ conflicts: ['dir', 'replace'],
+ })
+ .option('d', {
+ alias: 'dir',
+ desc: 'Output directory',
+ type: 'string',
+ conflicts: ['output', 'replace'],
+ })
+ .option('r', {
+ alias: 'replace',
+ desc: 'Replace (overwrite) the input file',
+ type: 'boolean',
+ conflicts: ['output', 'dir'],
+ })
+ .alias('m', 'map')
+ .describe('map', 'Create an external sourcemap')
+ .describe('no-map', 'Disable the default inline sourcemaps')
+ .option('w', {
+ alias: 'watch',
+ desc: 'Watch files for changes and recompile as needed',
+ type: 'boolean',
+ conflicts: 'replace',
+ })
+ .option('verbose', {
+ desc: 'Be verbose',
+ type: 'boolean',
+ })
+ .option('env', {
+ desc: 'A shortcut for setting NODE_ENV',
+ type: 'string',
+ })
+ .group(
+ ['u', 'parser', 'stringifier', 'syntax'],
+ 'Options for use without a config file:',
+ )
+ .option('u', {
+ alias: 'use',
+ desc: 'List of postcss plugins to use',
+ type: 'array',
+ })
+ .option('parser', {
+ desc: 'Custom postcss parser',
+ type: 'string',
+ })
+ .option('stringifier', {
+ desc: 'Custom postcss stringifier',
+ type: 'string',
+ })
+ .option('syntax', {
+ desc: 'Custom postcss syntax',
+ type: 'string',
+ })
+ .group(['ext', 'base'], 'Options for use with --dir:')
+ .option('ext', {
+ desc: 'Override the output file extension; for use with --dir',
+ type: 'string',
+ implies: 'dir',
+ })
+ .option('base', {
+ desc: 'Mirror the directory structure relative to this path in the output directory, for use with --dir',
+ type: 'string',
+ implies: 'dir',
+ })
+ .group(['include-dotfiles', 'poll', 'config'], 'Advanced options:')
+ .option('include-dotfiles', {
+ desc: 'Enable glob to match files/dirs that begin with "."',
+ type: 'boolean',
+ })
+ .option('poll', {
+ desc: 'Use polling for file watching. Can optionally pass polling interval; default 100 ms',
+ implies: 'watch',
+ })
+ .option('config', {
+ desc: 'Set a custom directory to look for a config file',
+ type: 'string',
+ })
+ .alias('h', 'help')
+ .example('$0 input.css -o output.css', 'Basic usage')
+ .example('$0 src/**/*.css --base src --dir build', 'Glob Pattern & output')
+ .example(
+ 'cat input.css | $0 -u autoprefixer > output.css',
+ '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.
+
+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`,
+ )
+
+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 2b54a70..0000000
--- a/lib/depGraph.js
+++ /dev/null
@@ -1,21 +0,0 @@
-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
new file mode 100644
index 0000000..e4d0c93
--- /dev/null
+++ b/lib/getMapfile.js
@@ -0,0 +1,7 @@
+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}`
+ }
+ return `${options.to}.map`
+}
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 04cd8d4..70194cd 100644
--- a/package.json
+++ b/package.json
@@ -1,54 +1,61 @@
{
"name": "postcss-cli",
- "version": "4.0.0",
+ "version": "11.0.1",
"description": "CLI for PostCSS",
- "main": "index.js",
+ "type": "module",
"engines": {
- "node": ">=4"
+ "node": ">=18"
},
"bin": {
- "postcss": "./bin/postcss"
+ "postcss": "./index.js"
},
"scripts": {
+ "ci": "eslint . && c8 ava -v && npm run prettier -- --list-different",
"clean": "node test/helpers/clean.js",
- "lint": "standard",
- "pretest": "npm run clean && npm run lint",
- "test": "nyc ava -v"
+ "prettier": "prettier --single-quote --no-semi \"**/*.{js,md}\"",
+ "format": "npm run prettier -- --write && eslint . --fix",
+ "pretest": "npm run clean && npm run format",
+ "test": "c8 ava -v"
},
"dependencies": {
- "chalk": "^1.1.3",
- "chokidar": "^1.6.1",
- "dependency-graph": "^0.5.0",
- "fs-extra": "^3.0.1",
- "get-stdin": "^5.0.1",
- "globby": "^6.1.0",
- "ora": "^1.1.0",
- "postcss": "^6.0.1",
- "postcss-load-config": "^1.1.0",
- "postcss-reporter": "^3.0.0",
+ "chokidar": "^3.3.0",
+ "dependency-graph": "^1.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": "^8.0.1"
+ "slash": "^5.0.0",
+ "tinyglobby": "^0.2.12",
+ "yargs": "^17.0.0"
},
"devDependencies": {
- "ava": "^0.19.1",
- "coveralls": "^2.12.0",
- "nyc": "^10.1.2",
- "postcss-import": "^9.1.0",
- "standard": "^10.0.2",
- "sugarss": "^1.0.0",
- "uuid": "^3.0.1"
+ "ava": "^3.1.0",
+ "c8": "^10.0.0",
+ "coveralls": "^3.0.0",
+ "eslint": "^9.22.0",
+ "eslint-config-problems": "9.0.0",
+ "globals": "^17.0.0",
+ "postcss": "^8.0.4",
+ "postcss-import": "^16.0.0",
+ "prettier": "~3.9.0",
+ "sugarss": "^5.0.0",
+ "uuid": "^13.0.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.0.0"
},
"files": [
- "bin",
"index.js",
- "lib"
+ "lib",
+ "!**/*.test.js"
],
"keywords": [
"cli",
"postcss",
"postcss-runner"
],
- "authors": [
+ "contributors": [
{
"name": "Michael Ciniawky",
"email": "michael.ciniawsky@gmail.com"
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..a513283
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,7 @@
+{
+ "extends": [
+ "config:base",
+ ":preserveSemverRanges",
+ ":label(deps)"
+ ]
+}
diff --git a/test/base.js b/test/base.js
index e55063b..a0f965c 100644
--- a/test/base.js
+++ b/test/base.js
@@ -5,27 +5,27 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--base --dir works', async function (t) {
+test('--base --dir works', async (t) => {
const dir = tmp()
- const { error, stderr } = await cli(
- [
- 'test/fixtures/base/**/*.css',
- '--dir', dir,
- '--base', 'test/fixtures/base',
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ '"test/fixtures/base/**/*.css"',
+ '--dir',
+ dir,
+ '--base',
+ 'test/fixtures/base',
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
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 b97ec6b..3adab9b 100644
--- a/test/cli.js
+++ b/test/cli.js
@@ -4,21 +4,17 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('works with defaults', async function (t) {
+test('works with defaults', async (t) => {
const output = tmp('output.css')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.css',
- '-o', output,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.css',
+ '-o',
+ output,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(output),
- await read('test/fixtures/a.css')
- )
+ t.is(await read(output), await read('test/fixtures/a.css'))
})
diff --git a/test/config.js b/test/config.js
index e5ccd00..b1d7c2c 100644
--- a/test/config.js
+++ b/test/config.js
@@ -6,7 +6,7 @@ import ENV from './helpers/env.js'
import cli from './helpers/cli.js'
import read from './helpers/read.js'
-test('supports common config', async function (t) {
+test('supports common config', async (t) => {
const env = `module.exports = {
plugins: [
require('postcss-import')()
@@ -17,61 +17,86 @@ test('supports common config', async function (t) {
const { error, stderr } = await cli(
['a.css', '-o', 'output.css', '--no-map'],
- dir
+ dir,
)
- t.ifError(error, stderr)
+ 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("doesn't error on empty config", async function (t) {
+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'),
+ )
+})
+
+test("doesn't error on empty config", async (t) => {
const env = `module.exports = {}`
const dir = await ENV(env, ['a.css'])
const { error, stderr } = await cli(
['a.css', '-o', 'output.css', '--no-map'],
- dir
+ dir,
)
- t.ifError(error, stderr)
+ 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('errors if `to` is set', async function (t) {
+test('errors if `to` is set', async (t) => {
const env = `module.exports = {
to: 'out.css'
}`
const dir = await ENV(env, ['a.css'])
- const { stderr } = await cli(
- ['a.css', '-o', 'output.css', '--no-map'],
- dir
- )
+ const { stderr } = await cli(['a.css', '-o', 'output.css', '--no-map'], dir)
- t.regex(stderr, /Config Error: Can not set from or to options in config file, use CLI arguments instead/)
+ t.regex(
+ stderr,
+ /Config Error: Can not set from or to options in config file, use CLI arguments instead/,
+ )
})
-test('errors if `from` is set', async function (t) {
+test('errors if `from` is set', async (t) => {
const env = `module.exports = {
from: 'in.css'
}`
const dir = await ENV(env, ['a.css'])
- const { stderr } = await cli(
- ['a.css', '-o', 'output.css', '--no-map'],
- dir
- )
+ const { stderr } = await cli(['a.css', '-o', 'output.css', '--no-map'], dir)
- t.regex(stderr, /Config Error: Can not set from or to options in config file, use CLI arguments instead/)
+ t.regex(
+ stderr,
+ /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 81d9d01..695f198 100644
--- a/test/dir.js
+++ b/test/dir.js
@@ -5,27 +5,20 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--dir works', async function (t) {
+test('--dir works', async (t) => {
const dir = tmp()
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.css',
- 'test/fixtures/b.css',
- '--dir', dir,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.css',
+ 'test/fixtures/b.css',
+ '--dir',
+ dir,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(path.join(dir, 'a.css')),
- await read('test/fixtures/a.css')
- )
+ t.is(await read(path.join(dir, 'a.css')), await read('test/fixtures/a.css'))
- t.is(
- await read(path.join(dir, 'b.css')),
- await read('test/fixtures/b.css')
- )
+ t.is(await read(path.join(dir, 'b.css')), await read('test/fixtures/b.css'))
})
diff --git a/test/error.js b/test/error.js
index 72929a2..5ff67ef 100644
--- a/test/error.js
+++ b/test/error.js
@@ -4,72 +4,62 @@ 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(({ err, code }) => {
+ return cli(['test/fixtures/*.css', '-o', tmp()]).then(({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /Input Error: Must use --dir or --replace/)
+ t.regex(error.toString(), /Input Error: Must use --dir or --replace/)
})
})
test('multiple input files && writing to stdout', (t) => {
- return cli(['test/fixtures/*.css'])
- .then(({ err, code }) => {
+ return cli(['test/fixtures/*.css']).then(({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /Input Error: Must use --dir or --replace/)
+ t.regex(error.toString(), /Input Error: Must use --dir or --replace/)
})
})
test('--map && writing to stdout', (t) => {
- return cli(['test/fixtures/a.css', '--map'])
- .then(({ err, code }) => {
+ return cli(['test/fixtures/a.css', '--map']).then(({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /Output Error: Cannot output external sourcemaps when writing to STDOUT/)
+ t.regex(
+ error.toString(),
+ /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()
- ]
+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 package 'postcss-plugin'/,
+ )
+ },
)
- .then(({ err, code }) => {
- t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /ENOENT: no such file or directory/)
- })
})
-test('PluginError', (t) => {
- return cli(
- [
- 'test/fixtures/a.css',
- '-u', 'postcss-plugin',
- '-o', tmp()
- ]
- )
- .then(({ err, code }) => {
+test('plugin throws on require', (t) => {
+ return cli([
+ 'test/fixtures/a.css',
+ '-u',
+ './test/fixtures/_bad-plugin.js',
+ '-o',
+ tmp(),
+ ]).then(({ error, code }) => {
t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /Plugin Error: Cannot find module 'postcss-plugin'/)
+ t.regex(error.toString(), /Plugin Error \(.*bad-plugin.js\): This fails/)
})
})
test('CssSyntaxError', (t) => {
- return cli(
- [
- 'test/fixtures/a.css',
- '-p', 'sugarss',
- '-o', tmp()
- ]
+ return cli(['test/fixtures/a.css', '--parser', 'sugarss', '-o', tmp()]).then(
+ ({ error, code }) => {
+ t.is(code, 1, 'expected non-zero error code')
+ t.regex(
+ error.toString(),
+ /CssSyntaxError: .*a.css:1:4: Unnecessary curly bracket/,
+ )
+ },
)
- .then(({ err, code }) => {
- t.is(code, 1, 'expected non-zero error code')
- t.regex(err.toString(), /\[1:4] Unnecessary curly bracket/)
- })
})
diff --git a/test/ext.js b/test/ext.js
index 10578cc..675939d 100644
--- a/test/ext.js
+++ b/test/ext.js
@@ -1,23 +1,51 @@
import test from 'ava'
-import fs from 'fs-extra'
+import fs from 'node:fs/promises'
import path from 'path'
import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
-test('--ext works', async function (t) {
+test('--ext works', async (t) => {
const dir = tmp()
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.sss',
- '-p', 'sugarss',
- '-d', dir,
- '--ext', '.css'
- ]
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.sss',
+ '--parser',
+ 'sugarss',
+ '-d',
+ dir,
+ '--ext',
+ '.css',
+ ])
+ t.falsy(error, stderr)
+
+ t.truthy(
+ await fs.access(path.join(dir, 'a.css')).then(
+ () => true,
+ () => false,
+ ),
)
- t.ifError(error, stderr)
+})
+
+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')))
+ t.truthy(
+ await fs.access(path.join(dir, 'a.css')).then(
+ () => true,
+ () => false,
+ ),
+ )
})
diff --git a/test/fixtures/_bad-plugin.js b/test/fixtures/_bad-plugin.js
new file mode 100644
index 0000000..2911e95
--- /dev/null
+++ b/test/fixtures/_bad-plugin.js
@@ -0,0 +1 @@
+throw new Error('This fails')
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/glob/a.css b/test/fixtures/glob/a.css
new file mode 100644
index 0000000..5451a33
--- /dev/null
+++ b/test/fixtures/glob/a.css
@@ -0,0 +1,3 @@
+.a {
+ color: red;
+}
diff --git a/test/fixtures/glob/b.css b/test/fixtures/glob/b.css
new file mode 100644
index 0000000..14aa1f6
--- /dev/null
+++ b/test/fixtures/glob/b.css
@@ -0,0 +1,3 @@
+.b {
+ color: blue;
+}
diff --git a/test/fixtures/glob/s.css b/test/fixtures/glob/s.css
new file mode 100644
index 0000000..e88d515
--- /dev/null
+++ b/test/fixtures/glob/s.css
@@ -0,0 +1,3 @@
+.a {
+ color: red
+}
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
new file mode 100644
index 0000000..38743f7
--- /dev/null
+++ b/test/glob.js
@@ -0,0 +1,58 @@
+import test from 'ava'
+import path from 'path'
+
+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()
+
+ const { error, stderr } = await cli([
+ 'test/fixtures/glob/*.css',
+ '-d',
+ output,
+ '--no-map',
+ ])
+
+ t.falsy(error, stderr)
+
+ t.is(
+ await read(path.join(output, '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'),
+ )
+ t.is(
+ await read(path.join(output, 's.css')),
+ await read('test/fixtures/glob/s.css'),
+ )
+})
+
+test('works with directory input', async (t) => {
+ const output = tmp()
+
+ const { error, stderr } = await cli([
+ 'test/fixtures/glob',
+ '-d',
+ output,
+ '--no-map',
+ ])
+
+ t.falsy(error, stderr)
+
+ t.is(
+ await read(path.join(output, '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'),
+ )
+ t.is(
+ await read(path.join(output, 's.css')),
+ await read('test/fixtures/glob/s.css'),
+ )
+})
diff --git a/test/helpers/clean.js b/test/helpers/clean.js
index 4a9c929..7591d7b 100644
--- a/test/helpers/clean.js
+++ b/test/helpers/clean.js
@@ -1,13 +1,11 @@
-'use strict'
-
-const fs = require('fs-extra')
+import fs from 'node:fs/promises'
Promise.all([
- fs.emptyDir('./test/fixtures/.tmp/'),
- fs.remove('./coverage'),
- fs.remove('./.nyc_output')
-])
- .catch(err => {
- console.error(err)
- process.exit(1)
- })
+ fs
+ .rm('./test/fixtures/.tmp/', { recursive: true, force: true })
+ .then(() => fs.mkdir('./test/fixtures/.tmp/', { recursive: true })),
+ fs.rm('./coverage', { recursive: true, force: true }),
+]).catch((err) => {
+ console.error(err)
+ process.exit(1)
+})
diff --git a/test/helpers/cli.js b/test/helpers/cli.js
index 4e55c2a..838d5a6 100644
--- a/test/helpers/cli.js
+++ b/test/helpers/cli.js
@@ -1,20 +1,19 @@
-'use strict'
-
import path from 'path'
-import { execFile } from 'child_process'
+import { exec } from 'child_process'
export default function (args, cwd) {
return new Promise((resolve) => {
- execFile(
- path.resolve('bin/postcss'),
- args, { cwd },
- (err, stdout, stderr) => {
+ exec(
+ `node ${path.resolve('index.js')} ${args.join(' ')}`,
+ { cwd },
+ (error, stdout, stderr) => {
resolve({
- code: err && err.code ? err.code : 0,
- err,
+ code: error && error.code ? error.code : 0,
+ error,
stdout,
- stderr
+ stderr,
})
- })
+ },
+ )
})
}
diff --git a/test/helpers/env.js b/test/helpers/env.js
index d655628..0fd9549 100644
--- a/test/helpers/env.js
+++ b/test/helpers/env.js
@@ -1,26 +1,24 @@
-'use strict'
-
-import fs from 'fs-extra'
+import fs from 'node:fs/promises'
import path from 'path'
-import globby from 'globby'
+import { glob } from 'tinyglobby'
import tmp from './tmp.js'
-export default function (config, fixtures) {
- fixtures = fixtures || '**/*'
- // fixtures = fixtures.map(p => path.join('test/fixtures', p))
+export default async function (config, fixtures = '**/*', extension = 'cjs') {
const dir = tmp()
- // Save promise in a const
- const fixture = globby(fixtures, { cwd: 'test/fixtures' })
- .then((list) => {
- return list.map((item) => {
- return fs.copy(path.join('test/fixtures', item), path.join(dir, item))
- })
- })
- // Save promise in a const
- const rc = fs.outputFile(
- path.join(dir, 'postcss.config.js'), config
- )
- // Return a promise for dir when both tasks are done:
- return Promise.all([ fixture, rc ]).then(() => dir)
+
+ await fs.mkdir(dir, { recursive: true })
+
+ const list = await glob(fixtures, { cwd: 'test/fixtures' })
+
+ await Promise.all([
+ ...list.map(async (item) => {
+ const dest = path.join(dir, item)
+ await fs.mkdir(path.dirname(dest), { recursive: true })
+ await fs.copyFile(path.join('test/fixtures', item), dest)
+ }),
+ fs.writeFile(path.join(dir, `postcss.config.${extension}`), config),
+ ])
+
+ return dir
}
diff --git a/test/helpers/read.js b/test/helpers/read.js
index 7526a61..17d969f 100644
--- a/test/helpers/read.js
+++ b/test/helpers/read.js
@@ -1,7 +1,7 @@
-'use strict'
-
-import { readFile } from 'fs-extra'
+import fs from 'node:fs/promises'
export default function (path) {
- return readFile(path, 'utf8')
+ 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 139d185..bfa1369 100644
--- a/test/helpers/tmp.js
+++ b/test/helpers/tmp.js
@@ -1,7 +1,5 @@
-'use strict'
-
import path from 'path'
-import uuid from 'uuid'
+import { v4 as uuid } from 'uuid'
export default function (ext) {
ext = ext || ''
diff --git a/test/map.js b/test/map.js
index e7862c0..a3febb9 100644
--- a/test/map.js
+++ b/test/map.js
@@ -1,46 +1,60 @@
import test from 'ava'
-import fs from 'fs-extra'
+import fs from 'node:fs/promises'
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 function (t) {
+test('inline maps are generated by default', async (t) => {
const output = tmp('output.css')
- const { error, stderr } = await cli(
- [ 'test/fixtures/import.css', '-u', 'postcss-import', '-o', output ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/import.css',
+ '-u',
+ 'postcss-import',
+ '-o',
+ output,
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
t.regex(await read(output), /\/*# sourceMappingURL=/)
})
-test('--map generates external sourcemaps', async function (t) {
+test('--map generates external sourcemaps', async (t) => {
const output = tmp('output.css')
- const { error, stderr } = await cli(
- [ 'test/fixtures/import.css', '-u', 'postcss-import', '-o', output, '--map' ]
+ const { error, stderr } = await cli([
+ 'test/fixtures/import.css',
+ '-u',
+ 'postcss-import',
+ '-o',
+ output,
+ '--map',
+ ])
+
+ t.falsy(error, stderr)
+
+ t.truthy(
+ await fs.access(output.replace('.css', '.css.map')).then(
+ () => true,
+ () => false,
+ ),
)
-
- t.ifError(error, stderr)
-
- t.truthy(await fs.pathExists(output.replace('.css', '.css.map')))
})
-test('--no-map disables internal sourcemaps', async function (t) {
+test('--no-map disables internal sourcemaps', async (t) => {
const output = tmp('output.css')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/import.css',
- '-u', 'postcss-import',
- '-o', output,
- '--no-map'
- ]
- )
- t.ifError(error, stderr)
+ const { error, stderr } = await cli([
+ 'test/fixtures/import.css',
+ '-u',
+ 'postcss-import',
+ '-o',
+ output,
+ '--no-map',
+ ])
+ t.falsy(error, stderr)
t.notRegex(await read(output), /\/*# sourceMappingURL=/)
})
diff --git a/test/misc.js b/test/misc.js
index 3d02dde..09bde3f 100644
--- a/test/misc.js
+++ b/test/misc.js
@@ -2,23 +2,21 @@ import test from 'ava'
import cli from './helpers/cli.js'
-test('--help', async function (t) {
+test('--help', async (t) => {
const help = await cli(['--help'])
- t.ifError(help.error)
+ t.falsy(help.error)
- t.truthy(
- help.stdout.length > 10,
- 'expected --help to output a help message'
- )
+ t.truthy(help.stdout.length > 10, 'expected --help to output a help message')
})
-test('--version', async function (t) {
+
+test('--version', async (t) => {
const version = await cli(['--version'])
- t.ifError(version.error)
+ t.falsy(version.error)
t.truthy(
- version.stdout.length > 10,
- 'expected --version to output version info'
+ version.stdout.length > 5,
+ 'expected --version to output version info',
)
})
diff --git a/test/parser.js b/test/parser.js
index be3cad7..689d9b7 100644
--- a/test/parser.js
+++ b/test/parser.js
@@ -4,22 +4,19 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--parser works', async function (t) {
+test('--parser works', async (t) => {
const output = tmp('output.css')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.sss',
- '-p', 'sugarss',
- '-o', output,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.sss',
+ '--parser',
+ 'sugarss',
+ '-o',
+ output,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(output),
- await read('test/fixtures/s.css')
- )
+ t.is(await read(output), await read('test/fixtures/s.css'))
})
diff --git a/test/replace.js b/test/replace.js
index c2a1c7e..7032493 100644
--- a/test/replace.js
+++ b/test/replace.js
@@ -1,35 +1,32 @@
import test from 'ava'
-import fs from 'fs-extra'
+import fs from 'node:fs/promises'
import path from 'path'
import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--replace works', async function (t) {
+test('--replace works', async (t) => {
const dir = tmp()
const output = path.join(dir, 'output.css')
+ await fs.mkdir(dir, { recursive: true })
await Promise.all([
- fs.copy('test/fixtures/import.css', output),
- fs.copy('test/fixtures/a.css', path.join(dir, 'a.css'))
+ fs.copyFile('test/fixtures/import.css', output),
+ fs.copyFile('test/fixtures/a.css', path.join(dir, 'a.css')),
])
- const { error, stderr } = await cli(
- [
- output,
- '--replace',
- '-u', 'postcss-import',
- '--no-map'
- ]
- )
-
- t.ifError(error, stderr)
-
- t.is(
- await read(output),
- await read('test/fixtures/a.css')
- )
+ const { error, stderr } = await cli([
+ output.replace(/\\/g, '/'), // gotta keep globby happy on Windows
+ '--replace',
+ '-u',
+ 'postcss-import',
+ '--no-map',
+ ])
+
+ t.falsy(error, stderr)
+
+ t.is(await read(output), await read('test/fixtures/a.css'))
})
diff --git a/test/stdin.js b/test/stdin.js
index 78dd855..9cdf928 100644
--- a/test/stdin.js
+++ b/test/stdin.js
@@ -1,8 +1,8 @@
import test from 'ava'
-import fs from 'fs-extra'
+import { createReadStream } from 'node:fs'
import path from 'path'
-import { execFile } from 'child_process'
+import { exec } from 'child_process'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
@@ -10,20 +10,19 @@ import read from './helpers/read.js'
test.cb('reads from stdin', (t) => {
const output = tmp('output.css')
- let cp = execFile(
- path.resolve('bin/postcss'),
- ['-o', output, '--no-map'],
+ const cp = exec(
+ `node ${path.resolve('index.js')} -o ${output} --no-map`,
(error, stdout, stderr) => {
if (error) t.end(error, stderr)
- Promise.all([ read(output), read('test/fixtures/a.css') ])
- .then(([ a, e ]) => {
+ Promise.all([read(output), read('test/fixtures/a.css')])
+ .then(([a, e]) => {
t.is(a, e)
t.end()
})
.catch(t.end)
- }
+ },
)
- fs.createReadStream('test/fixtures/a.css').pipe(cp.stdin)
+ createReadStream('test/fixtures/a.css').pipe(cp.stdin)
})
diff --git a/test/stdout.js b/test/stdout.js
index 31433c2..b53de43 100644
--- a/test/stdout.js
+++ b/test/stdout.js
@@ -1,30 +1,27 @@
import test from 'ava'
-import fs from 'fs-extra'
+import { createReadStream } from 'node:fs'
import path from 'path'
-import { execFile } from 'child_process'
+import { exec } from 'child_process'
import read from './helpers/read.js'
-test.cb('writes to stdout', function (t) {
- const cp = execFile(
- path.resolve('bin/postcss'),
- [
- '-p', 'sugarss',
- '-u', 'postcss-import',
- '--no-map'
- ],
+test.cb('writes to stdout', (t) => {
+ const cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} --parser sugarss -u postcss-import --no-map`,
(error, stdout, stderr) => {
if (error) t.end(error, stderr)
- Promise.all([ stdout, read('test/fixtures/s.css') ])
- .then(([ a, e ]) => {
+ Promise.all([stdout.replace(/\r\n/g, '\n'), read('test/fixtures/s.css')])
+ .then(([a, e]) => {
t.is(a, e)
t.end()
})
.catch(t.end)
- }
+ },
)
- fs.createReadStream('./test/fixtures/a.sss').pipe(cp.stdin)
+ createReadStream('./test/fixtures/a.sss').pipe(cp.stdin)
})
diff --git a/test/stringifier.js b/test/stringifier.js
index 55e7d68..69adaff 100644
--- a/test/stringifier.js
+++ b/test/stringifier.js
@@ -4,22 +4,19 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--stringifier works', async function (t) {
+test('--stringifier works', async (t) => {
const output = tmp('output.sss')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.css',
- '-t', 'sugarss',
- '-o', output,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.css',
+ '--stringifier',
+ 'sugarss',
+ '-o',
+ output,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(output),
- await read('test/fixtures/a.sss')
- )
+ t.is(await read(output), await read('test/fixtures/a.sss'))
})
diff --git a/test/syntax.js b/test/syntax.js
index 272b932..b17e1fd 100644
--- a/test/syntax.js
+++ b/test/syntax.js
@@ -4,22 +4,19 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--syntax works', async function (t) {
+test('--syntax works', async (t) => {
const output = tmp('output.sss')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/a.sss',
- '-s', 'sugarss',
- '-o', output,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/a.sss',
+ '--syntax',
+ 'sugarss',
+ '-o',
+ output,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(output),
- await read('test/fixtures/a.sss')
- )
+ t.is(await read(output), await read('test/fixtures/a.sss'))
})
diff --git a/test/unchanged.js b/test/unchanged.js
new file mode 100644
index 0000000..beabdb1
--- /dev/null
+++ b/test/unchanged.js
@@ -0,0 +1,17 @@
+import fs from 'node:fs/promises'
+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 6b8a5f6..c35d6cd 100644
--- a/test/use.js
+++ b/test/use.js
@@ -4,22 +4,19 @@ import cli from './helpers/cli.js'
import tmp from './helpers/tmp.js'
import read from './helpers/read.js'
-test('--use works', async function (t) {
+test('--use works', async (t) => {
const output = tmp('i.css')
- const { error, stderr } = await cli(
- [
- 'test/fixtures/import.css',
- '-u', 'postcss-import',
- '-o', output,
- '--no-map'
- ]
- )
+ const { error, stderr } = await cli([
+ 'test/fixtures/import.css',
+ '-u',
+ 'postcss-import',
+ '-o',
+ output,
+ '--no-map',
+ ])
- t.ifError(error, stderr)
+ t.falsy(error, stderr)
- t.is(
- await read(output),
- await read('test/fixtures/a.css')
- )
+ t.is(await read(output), await read('test/fixtures/a.css'))
})
diff --git a/test/watch.js b/test/watch.js
index b02a20f..9895a19 100644
--- a/test/watch.js
+++ b/test/watch.js
@@ -1,25 +1,30 @@
import test from 'ava'
-import fs from 'fs-extra'
+import fs from 'node:fs/promises'
import path from 'path'
-import { exec, execFile } from 'child_process'
+import { exec, spawn } from 'child_process'
import chokidar from 'chokidar'
import ENV from './helpers/env.js'
import read from './helpers/read.js'
+import tmp from './helpers/tmp.js'
-test.cb('--watch works', function (t) {
+// 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
t.plan(2)
ENV('', ['a.css'])
.then((dir) => {
- // Init watcher:
+ // Init watcher:
const watcher = chokidar.watch('.', {
cwd: dir,
ignoreInitial: true,
- awaitWriteFinish: true
+ awaitWriteFinish: true,
})
// On the first output:
@@ -28,7 +33,7 @@ test.cb('--watch works', function (t) {
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))
+ .then((css) => fs.writeFile(path.join(dir, 'a.css'), css))
.catch(done)
}
})
@@ -46,67 +51,54 @@ test.cb('--watch works', function (t) {
watcher.on('ready', () => {
// Using exec() and quoting "*.css" to test watch's glob handling:
cp = exec(
- `${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 => { if (code) t.end(code) })
+ 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 isEqual(p, expected) {
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
- function done (err) {
+ function done(err) {
try {
cp.kill()
- } catch (e) {}
+ } catch {}
t.end(err)
}
})
- .catch(t.end)
-
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
+ .catch(t.end)
})
-test.cb('--watch postcss.config.js', function (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('.', {
cwd: dir,
ignoreInitial: true,
- awaitWriteFinish: true
+ awaitWriteFinish: true,
})
// On the first output:
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)
}
})
@@ -114,7 +106,7 @@ test.cb('--watch postcss.config.js', function (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)
}
@@ -122,167 +114,409 @@ test.cb('--watch postcss.config.js', function (t) {
// Start postcss-cli:
watcher.on('ready', () => {
- cp = execFile(
- path.resolve('bin/postcss'),
- [
- 'import.css',
- '-o', 'output.css',
- '-w',
- '--no-map'
- ],
- { cwd: dir }
+ cp = exec(
+ `node ${path.resolve(
+ 'index.js',
+ )} import.css -o output.css -u postcss-import -w --no-map`,
+ { cwd: dir },
)
cp.on('error', t.end)
- cp.on('exit', (code) => { if (code) t.end(code) })
+ 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 isEqual(p, expected) {
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
- function done (err) {
+ function done(err) {
try {
cp.kill()
- } catch (e) {}
-
+ } catch {}
t.end(err)
}
})
- .catch(t.end)
+ .catch(t.end)
+})
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
+// 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)
+ }
+ })
+
+ 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()
+ })
+
+ function done(err) {
+ try {
+ cp.kill()
+ } catch {}
+
+ 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()
})
-test.cb('--watch dependencies', function (t) {
+testCb('--watch watches dependencies', (t) => {
let cp
t.plan(2)
- ENV('', ['import.css', 'a.css'])
- .then((dir) => {
- // 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/a.css')
- .then(() => read('test/fixtures/b.css'))
- .then(css => fs.writeFile(path.join(dir, 'a.css'), css))
- .catch(done)
+ 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
+ }
+ ]
}
- })
-
- // When the change is picked up:
- watcher.on('change', (p) => {
- if (p === 'output.css') {
- isEqual(p, 'test/fixtures/b.css')
- .then(() => done())
- .catch(done)
+ `,
+ )
+ .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/a.css')
+ .then(() => read('test/fixtures/b.css'))
+ .then((css) => fs.writeFile(path.join(dir, 'a.css'), css))
+ .catch(done)
+ }
+ })
+
+ // When the change is picked up:
+ watcher.on('change', (p) => {
+ if (p === 'output.css') {
+ isEqual(p, 'test/fixtures/b.css')
+ .then(() => done())
+ .catch(done)
+ }
+ })
+
+ // 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)
+ })
+ })
+
+ // Helper functions:
+ function isEqual(p, expected) {
+ return Promise.all([read(path.join(dir, p)), read(expected)]).then(
+ ([a, e]) => t.is(a, e),
+ )
}
- })
- // Start postcss-cli:
- watcher.on('ready', () => {
- cp = execFile(
- path.resolve('bin/postcss'),
- [
- 'import.css',
- '-o', 'output.css',
- '-u', 'postcss-import',
- '-w',
- '--no-map'
- ],
- { cwd: dir }
- )
+ function done(err) {
+ try {
+ cp.kill()
+ } catch {}
- cp.on('error', t.end)
- cp.on('exit', code => { if (code) t.end(code) })
+ t.end(err)
+ }
})
-
- // 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 (e) {}
- t.end(err)
- }
- })
- .catch(t.end)
-
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
+ .catch(t.end)
+ })
})
-test.cb("--watch doesn't exit on CssSyntaxError", function (t) {
- t.plan(0)
+testCb('--watch watches directory dependencies', (t) => {
+ let cp
- 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
- let cp = execFile(
- path.resolve('bin/postcss'),
- [
- 'a.css',
- '-o', 'output.css',
- '-w',
- '--no-map'
- ],
- { cwd: dir }
+ 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
+ }
+ ]
+ }
+ `,
)
- 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()
- })
-
- function done (err) {
- try {
- cp.kill()
- } catch (e) {}
+ .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', () => {
+ 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)
+ },
+ )
+})
- t.end(err)
- }
+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)
})
- .catch(t.end)
-
- // Timeout:
- setTimeout(() => t.end('test timeout'), 50000)
-})
+ },
+)