From a91fcb0699341f106192c86015257a3371aad67f Mon Sep 17 00:00:00 2001 From: Benjamin Lupton Date: Mon, 30 Oct 2023 20:28:47 +0800 Subject: [PATCH] v4.0.0 - use readdir builtin, node >18 required for recursion --- .github/workflows/bevry.yml | 4 - HISTORY.md | 7 ++ README.md | 4 +- package-lock.json | 38 ++++----- package.json | 20 ++--- source/bin.ts | 1 + source/index.ts | 46 ++++------- source/test.ts | 136 ++++++++++++++++++++++++-------- test-fixtures/invalid/ | 0 test-fixtures/invalid/aux | 0 test-fixtures/invalid/aux1 | 0 test-fixtures/invalid/com1 | 0 test-fixtures/invalid/con | 0 test-fixtures/invalid/foo:bar | 0 test-fixtures/invalid/lpt1 | 0 test-fixtures/invalid/nul1 | 0 test-fixtures/valid/foo-bar | 0 17 files changed, 162 insertions(+), 94 deletions(-) delete mode 100644 test-fixtures/invalid/ delete mode 100644 test-fixtures/invalid/aux delete mode 100644 test-fixtures/invalid/aux1 delete mode 100644 test-fixtures/invalid/com1 delete mode 100644 test-fixtures/invalid/con delete mode 100644 test-fixtures/invalid/foo:bar delete mode 100644 test-fixtures/invalid/lpt1 delete mode 100644 test-fixtures/invalid/nul1 delete mode 100644 test-fixtures/valid/foo-bar diff --git a/.github/workflows/bevry.yml b/.github/workflows/bevry.yml index c5fd357..7e354a8 100644 --- a/.github/workflows/bevry.yml +++ b/.github/workflows/bevry.yml @@ -11,10 +11,6 @@ jobs: - macos-latest - windows-latest node: - - '10' - - '12' - - '14' - - '16' - '18' - '20' - '21' diff --git a/HISTORY.md b/HISTORY.md index 49d3526..1fc9e94 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # History +## v4.0.0 2023 October 30 + +- API now returns all paths as the third argument +- Make the tests work on Windows +- Swapped out [readdir-cluster](https://github.com/bevry/readdir-cluster) for `recursive: true` in [`readdir` builtin](https://github.com/bevry/readdir-cluster) to solve `ERR_IPC_CHANNEL_CLOSED` errors + - This requires Node.js versions >= 18 for recursive support + ## v3.10.0 2023 October 30 - Updated dependencies, [base files](https://github.com/bevry/base), and [editions](https://editions.bevry.me) using [boundation](https://github.com/bevry/boundation) diff --git a/README.md b/README.md index d594945..2d65dd0 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ Check whether or not a directory and its descendants are valid Validity determined via the [`@bevry/valid-filename` package](https://github.com/bevry/valid-filename). +Requires Node.js version >= 18 for recursive directory support. + ## Usage [Complete API Documentation.](http://master.valid-directory.bevry.surge.sh/docs/) @@ -63,7 +65,7 @@ validate(path) Run `npm install -g valid-directory` then run against the current working directory with `valid-directory` or a specified directory via `valid-directory `. -Exit code will be `1` if validation failed to execute, `2` if validation failed, and `0` if validation passed. +Exit code will be `1` if a path is invalid, `2` if something went wrong, and `0` if validation passed. ### Packages diff --git a/package-lock.json b/package-lock.json index 6d9547a..82f7f7d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "Artistic-2.0", "dependencies": { "@bevry/valid-filename": "^1.0.0", - "readdir-cluster": "^3.17.0" + "fdir": "^6.1.0" }, "bin": { "valid-directory": "bin.cjs" @@ -35,7 +35,7 @@ "typescript": "5.2.2" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://bevry.me/fund" @@ -1686,10 +1686,9 @@ } }, "node_modules/fdir": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz", - "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==", - "dev": true, + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.1.0.tgz", + "integrity": "sha512-274qhz5PxNnA/fybOu6apTCUnM0GnO3QazB6VH+oag/7DQskdYq8lm07ZSm90kEQuWYH5GvjAxGruuHrEr0bcg==", "peerDependencies": { "picomatch": "2.x" }, @@ -2586,6 +2585,20 @@ "url": "https://bevry.me/fund" } }, + "node_modules/make-deno-edition/node_modules/fdir": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-5.3.0.tgz", + "integrity": "sha512-BtE53+jaa7nNHT+gPdfU6cFAXOJUWDs2b5GFox8dtl6zLXmfNf/N6im69b9nqNNwDyl27mpIWX8qR7AafWzSdQ==", + "dev": true, + "peerDependencies": { + "picomatch": "2.x" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -2967,7 +2980,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=8.6" }, @@ -3116,17 +3129,6 @@ "node": ">= 6" } }, - "node_modules/readdir-cluster": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/readdir-cluster/-/readdir-cluster-3.17.0.tgz", - "integrity": "sha512-h2gHgmqNcdedAyFjhr/WpK7mXVA50yIG6CSXmu+Gr09CIZFRPuer5ZyVvABVjbgEy+ZuyItnyIAoSPbWPxAqaQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://bevry.me/fund" - } - }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", diff --git a/package.json b/package.json index c71d3af..f0a0555 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "valid-directory", - "version": "3.10.0", + "version": "4.0.0", "description": "Check whether or not a directory and its descendants are valid", "homepage": "https://github.com/bevry/valid-directory", "license": "Artistic-2.0", @@ -72,7 +72,7 @@ "url": "https://github.com/bevry/valid-directory.git" }, "engines": { - "node": ">=10" + "node": ">=18" }, "editions": [ { @@ -97,7 +97,7 @@ "require" ], "engines": { - "node": "10 || 12 || 14 || 16 || 18 || 20 || 21", + "node": "18 || 20 || 21", "browsers": false } }, @@ -112,7 +112,7 @@ "import" ], "engines": { - "node": "12 || 14 || 16 || 18 || 20 || 21", + "node": "18 || 20 || 21", "browsers": false } } @@ -129,7 +129,7 @@ }, "dependencies": { "@bevry/valid-filename": "^1.0.0", - "readdir-cluster": "^3.17.0" + "fdir": "^6.1.0" }, "devDependencies": { "@bevry/update-contributors": "^1.22.0", @@ -155,21 +155,21 @@ "our:clean": "rm -Rf ./docs ./edition* ./es2015 ./es5 ./out ./.next", "our:compile": "npm run our:compile:deno && npm run our:compile:edition-es2017 && npm run our:compile:edition-es2017-esm && npm run our:compile:types", "our:compile:deno": "make-deno-edition --attempt", - "our:compile:edition-es2017": "tsc --module commonjs --target ES2017 --outDir ./edition-es2017 --project tsconfig.json && ( test ! -d edition-es2017/source || ( mv edition-es2017/source edition-temp && rm -Rf edition-es2017 && mv edition-temp edition-es2017 ) ) && echo '{\"type\": \"commonjs\"}' > edition-es2017/package.json", - "our:compile:edition-es2017-esm": "tsc --module ESNext --target ES2017 --outDir ./edition-es2017-esm --project tsconfig.json && ( test ! -d edition-es2017-esm/source || ( mv edition-es2017-esm/source edition-temp && rm -Rf edition-es2017-esm && mv edition-temp edition-es2017-esm ) ) && echo '{\"type\": \"module\"}' > edition-es2017-esm/package.json", + "our:compile:edition-es2017": "tsc --module commonjs --target ES2017 --outDir ./edition-es2017 --project tsconfig.json && ( test ! -d edition-es2017/source || ( mv edition-es2017/source edition-temp && rm -Rf edition-es2017 && mv edition-temp edition-es2017 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es2017/package.json", + "our:compile:edition-es2017-esm": "tsc --module ESNext --target ES2017 --outDir ./edition-es2017-esm --project tsconfig.json && ( test ! -d edition-es2017-esm/source || ( mv edition-es2017-esm/source edition-temp && rm -Rf edition-es2017-esm && mv edition-temp edition-es2017-esm ) ) && printf '%s' '{\"type\": \"module\"}' > edition-es2017-esm/package.json", "our:compile:types": "tsc --project tsconfig.json --emitDeclarationOnly --declaration --declarationMap --declarationDir ./compiled-types && ( test ! -d compiled-types/source || ( mv compiled-types/source edition-temp && rm -Rf compiled-types && mv edition-temp compiled-types ) )", - "our:deploy": "echo no need for this project", + "our:deploy": "printf '%s\n' 'no need for this project'", "our:meta": "npm run our:meta:contributors && npm run our:meta:docs && npm run our:meta:projectz", "our:meta:contributors": "update-contributors", "our:meta:docs": "npm run our:meta:docs:typedoc", "our:meta:docs:typedoc": "rm -Rf ./docs && typedoc --exclude '**/+(*test*|node_modules)' --excludeExternals --out ./docs ./source", "our:meta:projectz": "projectz compile", "our:release": "npm run our:release:prepare && npm run our:release:check-changelog && npm run our:release:check-dirty && npm run our:release:tag && npm run our:release:push", - "our:release:check-changelog": "cat ./HISTORY.md | grep v$npm_package_version || (echo add a changelog entry for v$npm_package_version && exit -1)", + "our:release:check-changelog": "cat ./HISTORY.md | grep v$npm_package_version || (printf '%s\n' 'add a changelog entry for v$npm_package_version' && exit -1)", "our:release:check-dirty": "git diff --exit-code", "our:release:prepare": "npm run our:clean && npm run our:compile && npm run our:test && npm run our:meta", "our:release:push": "git push origin && git push origin --tags", - "our:release:tag": "export MESSAGE=$(cat ./HISTORY.md | sed -n \"/## v$npm_package_version/,/##/p\" | sed 's/## //' | awk 'NR>1{print buf}{buf = $0}') && test \"$MESSAGE\" || (echo 'proper changelog entry not found' && exit -1) && git tag v$npm_package_version -am \"$MESSAGE\"", + "our:release:tag": "export MESSAGE=$(cat ./HISTORY.md | sed -n \"/## v$npm_package_version/,/##/p\" | sed 's/## //' | awk 'NR>1{print buf}{buf = $0}') && test \"$MESSAGE\" || (printf '%s\n' 'proper changelog entry not found' && exit -1) && git tag v$npm_package_version -am \"$MESSAGE\"", "our:setup": "npm run our:setup:install", "our:setup:install": "npm install", "our:test": "npm run our:verify && npm test", diff --git a/source/bin.ts b/source/bin.ts index c5be5b0..7626504 100755 --- a/source/bin.ts +++ b/source/bin.ts @@ -12,6 +12,7 @@ validate(path) `${path} is invalid, due to the following paths:\n`, invalidPaths, ) + process.exitCode = 1 } }) .catch((err) => { diff --git a/source/index.ts b/source/index.ts index f57283f..c26e927 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,39 +1,27 @@ -import readdir from 'readdir-cluster' +import { readdir } from 'node:fs/promises' +import { basename } from 'node:path' import isValidFilename from '@bevry/valid-filename' /** Array of paths that are invalid */ -export type InvalidPaths = string[] +export type Paths = string[] /** Whether or not the directory was valid */ export type ValidateResult = - | [valid: false, invalidPaths: InvalidPaths] - | [valid: true] + | [valid: false, invalidRelativePaths: Paths, relativePaths: Paths] + | [valid: true, invalidRelativePaths: [], relativePaths: Paths] -/** Iterator for readdir-cluster that validates the paths using valid-filename */ -export function validator( - this: InvalidPaths, +/** Validate a directory and its descendants */ +export default async function validate( fullPath: string, - relativePath: string, -) { - const valid = isValidFilename(relativePath) - - if (!valid) { - this.push(fullPath) +): Promise { + // https://nodejs.org/api/fs.html#fspromisesreaddirpath-options + const relativePaths: Paths = await readdir(fullPath, { recursive: true }) + const invalidRelativePaths: Paths = relativePaths.filter( + (relativePath) => !isValidFilename(basename(relativePath)), + ) + if (invalidRelativePaths.length) { + return [false, invalidRelativePaths, relativePaths] + } else { + return [true, [], relativePaths] } } - -/** Validate a directory and its descendants */ -export default function validate(fullPath: string): Promise { - return new Promise(function (resolve, reject) { - const invalidPaths: InvalidPaths = [] - readdir(fullPath, validator.bind(invalidPaths), function (err: Error) { - if (err) { - return reject(err) - } else if (invalidPaths.length) { - return resolve([false, invalidPaths]) - } else { - return resolve([true]) - } - }) - }) -} diff --git a/source/test.ts b/source/test.ts index 38b94c8..b27ffd8 100644 --- a/source/test.ts +++ b/source/test.ts @@ -1,35 +1,96 @@ -import { resolve, dirname } from 'path' -import { exec } from 'child_process' +import { join, resolve } from 'node:path' +import { rm, mkdir, writeFile } from 'node:fs/promises' +import { exec } from 'node:child_process' +import { platform, env } from 'node:process' +import { tmpdir } from 'node:os' -import { equal, contains } from 'assert-helpers' +import { equal, deepEqual, contains } from 'assert-helpers' import kava from 'kava' import validate from './index.js' import filedirname from 'filedirname' const [file, dir] = filedirname() - +const fixtures = resolve( + tmpdir(), + 'valid-directory', + `fixtures - ${Math.random()}`, +) const paths = { root: resolve(dir, '..'), bin: resolve(dir, 'bin.js'), - valid: resolve(dir, '..', 'test-fixtures', 'valid'), - invalid: resolve(dir, '..', 'test-fixtures', 'invalid'), + valid: resolve(fixtures, 'valid'), + invalid: resolve(fixtures, 'invalid'), } +const invalidPaths = platform.startsWith('win') + ? [] + : ['', 'foo:bar', 'con', 'prn', 'aux', 'nul', 'com1', 'lpt1'].sort() +const validPaths = ['foo-bar'] +const invalidPathsNested = invalidPaths.map((f) => join('invalid', f)) +const validPathsNested = validPaths.map((f) => join('valid', f)) +const allPathsNested = [ + 'valid', + 'invalid', + ...invalidPathsNested, + ...validPathsNested, +].sort() -kava.suite('valid-directory', function (suite) { +kava.suite('valid-directory', function (suite, test) { + test('mkdirs', function (done) { + Promise.all([ + mkdir(paths.valid, { recursive: true }), + mkdir(paths.invalid, { recursive: true }), + ]) + .then(() => done()) + .catch(done) + }) + test('mkfiles', function (done) { + Promise.all( + invalidPaths + .map((filename) => writeFile(resolve(paths.invalid, filename), '')) + .concat( + validPaths.map((filename) => + writeFile(resolve(paths.valid, filename), ''), + ), + ), + ) + .then(() => done()) + .catch(done) + }) suite('api', function (suite, test) { test('valid', function (done) { validate(paths.valid) - .then(([isValid]) => { - equal(isValid, true, 'path is valid') + .then(([isValid, _invalidPaths, _allPaths]) => { + deepEqual(_invalidPaths, [], 'no invalid paths') + deepEqual(_allPaths.sort(), validPaths, 'all paths are correct') + equal(isValid, true, 'only valid paths') done() }) .catch(done) }) test('invalid', function (done) { validate(paths.invalid) - .then(([isValid, invalidPaths]) => { - equal(isValid, false, 'path is invalid') - equal(invalidPaths?.length, 6) + .then(([isValid, _invalidPaths, _allPaths]) => { + deepEqual( + _invalidPaths.sort(), + invalidPaths, + 'invalid paths are correct', + ) + deepEqual(_allPaths.sort(), invalidPaths, 'all paths are correct') + equal(isValid, invalidPaths.length ? false : true, 'only valid paths') // windows adjustment + done() + }) + .catch(done) + }) + test('all', function (done) { + validate(fixtures) + .then(([isValid, _invalidPaths, _allPaths]) => { + deepEqual( + _invalidPaths.sort(), + invalidPathsNested, + 'invalid paths are correct', + ) + deepEqual(_allPaths.sort(), allPathsNested, 'all paths are correct') + equal(isValid, invalidPaths.length ? false : true, 'only valid paths') // windows adjustment done() }) .catch(done) @@ -48,16 +109,19 @@ kava.suite('valid-directory', function (suite) { }, ) }) - test('invalid', function (done) { - exec( - `node "${paths.bin}"`, - { cwd: paths.invalid, encoding: 'utf8' }, - function (error, stdout, stderr) { - contains(stderr.toString(), `${paths.invalid} is invalid`) - done() - }, - ) - }) + // windows adjustment + if (invalidPaths.length) { + test('invalid', function (done) { + exec( + `node "${paths.bin}"`, + { cwd: paths.invalid, encoding: 'utf8' }, + function (error, stdout, stderr) { + contains(stderr.toString(), `${paths.invalid} is invalid`) + done() + }, + ) + }) + } }) suite('arg', function (suite, test) { test('valid', function (done) { @@ -71,16 +135,24 @@ kava.suite('valid-directory', function (suite) { }, ) }) - test('invalid', function (done) { - exec( - `node "${paths.bin}" "${paths.invalid}"`, - { cwd: paths.root, encoding: 'utf8' }, - function (error, stdout, stderr) { - contains(stderr.toString(), `${paths.invalid} is invalid`) - done() - }, - ) - }) + // windows adjustment + if (invalidPaths.length) { + test('invalid', function (done) { + exec( + `node "${paths.bin}" "${paths.invalid}"`, + { cwd: paths.root, encoding: 'utf8' }, + function (error, stdout, stderr) { + contains(stderr.toString(), `${paths.invalid} is invalid`) + done() + }, + ) + }) + } }) }) + test('cleanup', function (done) { + rm(fixtures, { recursive: true }) + .then(() => done()) + .catch(done) + }) }) diff --git a/test-fixtures/invalid/ b/test-fixtures/invalid/ deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/aux b/test-fixtures/invalid/aux deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/aux1 b/test-fixtures/invalid/aux1 deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/com1 b/test-fixtures/invalid/com1 deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/con b/test-fixtures/invalid/con deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/foo:bar b/test-fixtures/invalid/foo:bar deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/lpt1 b/test-fixtures/invalid/lpt1 deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/invalid/nul1 b/test-fixtures/invalid/nul1 deleted file mode 100644 index e69de29..0000000 diff --git a/test-fixtures/valid/foo-bar b/test-fixtures/valid/foo-bar deleted file mode 100644 index e69de29..0000000