diff --git a/.eslintrc.yaml b/.eslintrc.yaml deleted file mode 100644 index 0473b0f..0000000 --- a/.eslintrc.yaml +++ /dev/null @@ -1,5 +0,0 @@ -env: - node: true -extends: problems -rules: - no-console: off diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a7a22ea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,22 @@ +name: Node.js CI +on: + push: + branches: master + pull_request: + +jobs: + test: + strategy: + matrix: + node: [18, 20] + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - run: git config --global core.autocrlf input + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + - run: npm install + - run: npm run ci diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 410129e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: node_js -git: - autocrlf: input -os: - - linux - - windows -node_js: - - node - - 12 - - 10 -script: - - npm run ci -after_success: - - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' diff --git a/CHANGELOG.md b/CHANGELOG.md index f1e0ce1..4fed6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,67 @@ +# 11.0.1 / 2025-03-12 + +- Update and minimize dependencies + +# 11.0.0 / 2023-12-05 + +- **BREAKING:** Require Node.js v18+ ([#464](https://github.com/postcss/postcss-cli/pull/464)) +- Upgrade to [`postcss-load-config@5`](https://github.com/postcss/postcss-load-config/blob/main/CHANGELOG.md#50-2023-11-20) for improved ESM & TS config support ([#461](https://github.com/postcss/postcss-cli/issues/461), [#462](https://github.com/postcss/postcss-cli/pull/462)) + +# 10.1.0 / 2022-11-29 + +- Allow running `--watch` mode in non-TTY contexts, like Docker ([#448](https://github.com/postcss/postcss-cli/pull/448)) +- Update dependencies + +# 10.0.0 / 2022-06-29 + +- **BREAKING:** Drop Node 12 support ([#438](https://github.com/postcss/postcss-cli/pull/438)) +- Add support for ESM config files ([#437](https://github.com/postcss/postcss-cli/pull/437)) + +# 9.1.0 / 2021-12-10 + +- Don't write to files if they're unchanged ([#320](https://github.com/postcss/postcss-cli/issues/320), [#417](https://github.com/postcss/postcss-cli/pull/417)) + +# 9.0.2 / 2021-11-04 + +- Switch to picocolors ([#409](https://github.com/postcss/postcss-cli/pull/409)) +- Remove test files from npm package + +# 9.0.1 / 2021-09-28 + +- Actually exit with error when attempting to stdout in watch mode +- Remove `bin/` from `files` in package.json + +# 9.0.0 / 2021-09-24 + +- **BREAKING:** Require Node.js v12+ +- **BREAKING:** Must specify full file path, including `.js` extension, when loading local plugins with `--use` ([#401](https://github.com/postcss/postcss-cli/pull/401)) +- **BREAKING:** Officially remove support for watching postcss config (was already broken in previous releases) +- Add support for `dir-dependency` messages ([#383](https://github.com/postcss/postcss-cli/pull/383), [#391](https://github.com/postcss/postcss-cli/pull/391)) +- Update deps + +# 8.3.1 / 2020-12-12 + +- Ensure paths are not interpreted as numbers ([#360](https://github.com/postcss/postcss-cli/issues/360)) +- Better errors for incorrect postcss version ([#361](https://github.com/postcss/postcss-cli/issues/361), [#362](https://github.com/postcss/postcss-cli/pull/362)) + +# 8.3.0 / 2020-11-17 + +- Exit on EOF/`^D` ([#358](https://github.com/postcss/postcss-cli/pull/358)) + +# 8.2.0 / 2020-10-29 + +- Allow backslashes in paths for better Windows experience ([#355](https://github.com/postcss/postcss-cli/pull/355)) + +# 8.1.0 / 2020-10-08 + +- Add support for `postcss.config.cjs` files ([#351](https://github.com/postcss/postcss-cli/pull/351)) + +# 8.0.0 / 2020-09-21 + +- **BREAKING:** Support postcss v8 ([#344](https://github.com/postcss/postcss-cli/issues/344), [#349](https://github.com/postcss/postcss-cli/pull/349)) +- **BREAKING:** postcss is now a `peerDependency`, you must install it seperately ([#344](https://github.com/postcss/postcss-cli/issues/344), [#349](https://github.com/postcss/postcss-cli/pull/349)) +- Upgrade dependencies ([#340](https://github.com/postcss/postcss-cli/pull/340)) + # 7.1.2 / 2020-08-31 - Make `--version` machine-readable ([#334](https://github.com/postcss/postcss-cli/issues/334), [#335](https://github.com/postcss/postcss-cli/pull/335)) @@ -21,7 +85,7 @@ # 6.1.3 / 2019-07-08 -- Fix external sorcemap filenames when the directory name contains `.css` ([#283](https://github.com/postcss/postcss-cli/issues/283), [#286](https://github.com/postcss/postcss-cli/pull/286)) +- Fix external sourcemap filenames when the directory name contains `.css` ([#283](https://github.com/postcss/postcss-cli/issues/283), [#286](https://github.com/postcss/postcss-cli/pull/286)) # 6.1.2 / 2019-02-22 @@ -276,4 +340,4 @@ Migration guide: https://github.com/postcss/postcss-cli/wiki/Migrating-from-v2-t # 0.1.0 / 2015-03-11 -- initial implementaion +- initial implementation diff --git a/README.md b/README.md index 50bade6..a390e3e 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,23 @@ [![npm][npm]][npm-url] [![node][node]][node-url] -[![Greenkeeper badge](https://badges.greenkeeper.io/postcss/postcss-cli.svg)](https://greenkeeper.io/) [![tests][tests]][tests-url] [![cover][cover]][cover-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

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