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

+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) -}) + }, +)