diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..b8fa91e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# editorconfig.org +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +max_line_length = 120 + +[*.md] +trim_trailing_whitespace = false diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 4dcf981..15c7916 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -39,6 +39,6 @@ jobs: - run: npm run lint name: Run linter - run: npm run format:check - name: Run Prettier check + name: Run format check - run: npm run test name: Run unit tests diff --git a/.releaserc b/.releaserc deleted file mode 100644 index 93cf006..0000000 --- a/.releaserc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "plugins": [ - ["@semantic-release/commit-analyzer", { - "preset": "angular", - "releaseRules": [ - {"type": "chore", "release": "patch"} - ] - }], - ["@semantic-release/release-notes-generator", { - "preset": "conventionalcommits", - "presetConfig": { - "types": [ - {"type": "feat", "section": "Features"}, - {"type": "fix", "section": "Bug Fixes"}, - {"type": "perf", "section": "Performance Improvements"}, - {"type": "revert", "section": "Reverts"}, - {"type": "chore", "section": "Miscellaneous Chores"}, - {"type": "refactor", "section": "Code Refactoring"}, - {"type": "docs", "section": "Documentation", "hidden": true}, - {"type": "style", "section": "Styles", "hidden": true}, - {"type": "test", "section": "Tests", "hidden": true}, - {"type": "build", "section": "Build System", "hidden": true}, - {"type": "ci", "section": "Continuous Integration", "hidden": true} - ] - } - }], - ["@semantic-release/changelog", { - "changelogFile": "CHANGELOG.md" - }], - "@semantic-release/npm", - ["@semantic-release/git", { - "assets": ["docs", "package.json", "CHANGELOG.md"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], - "@semantic-release/github" - ] -} diff --git a/README.md b/README.md index 3cb60ad..f41833c 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ A collection of ES7 async/await utilities. Install via NPM: npm install asyncbox ``` +Published as both ESM and CommonJS, so `import {sleep} from 'asyncbox'` and `const {sleep} = require('asyncbox')` both work. + Then, behold! ### Sleep diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index b963381..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,15 +0,0 @@ -import appiumConfig from '@appium/eslint-config-appium-ts'; -import {defineConfig, globalIgnores} from 'eslint/config'; - -export default defineConfig([ - { - extends: [appiumConfig], - }, - { - files: ['test/**/*.{js,ts}'], - rules: { - 'func-names': 'off' - } - }, - globalIgnores(['build']), -]); diff --git a/lib/asyncbox.ts b/lib/asyncbox.ts index 18fa183..b078811 100644 --- a/lib/asyncbox.ts +++ b/lib/asyncbox.ts @@ -1,4 +1,5 @@ import {limitFunction} from 'p-limit'; + import type { CancellablePromise, LongSleepOptions, diff --git a/oxfmt.config.mjs b/oxfmt.config.mjs new file mode 100644 index 0000000..4752f81 --- /dev/null +++ b/oxfmt.config.mjs @@ -0,0 +1,6 @@ +import appiumConfig, {defineConfig, ignorePatterns} from '@appium/oxc-config/oxfmt'; + +export default defineConfig({ + ...appiumConfig, + ignorePatterns: [...ignorePatterns], +}); diff --git a/oxlint.config.mjs b/oxlint.config.mjs new file mode 100644 index 0000000..34ea238 --- /dev/null +++ b/oxlint.config.mjs @@ -0,0 +1,6 @@ +import appiumConfig, {defineConfig, ignorePatterns} from '@appium/oxc-config/oxlint'; + +export default defineConfig({ + extends: [appiumConfig], + ignorePatterns: [...ignorePatterns], +}); diff --git a/package.json b/package.json index 85e35fe..5513b6f 100644 --- a/package.json +++ b/package.json @@ -1,64 +1,74 @@ { "name": "asyncbox", - "description": "A collection of small async/await utilities", - "tags": [ - "async/await", - "es7", - "async" - ], "version": "6.3.5", - "author": "jlipps@gmail.com", + "description": "A collection of small async/await utilities", + "bugs": { + "url": "https://github.com/appium/asyncbox/issues" + }, "license": "Apache-2.0", + "author": "jlipps@gmail.com", "repository": { "type": "git", "url": "https://github.com/appium/asyncbox.git" }, - "bugs": { - "url": "https://github.com/appium/asyncbox/issues" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0", - "npm": ">=10" - }, - "main": "./build/lib/asyncbox.js", "bin": {}, "directories": { "lib": "./lib" }, "files": [ "lib/**/*", - "build/lib/**/*" + "build/cjs/lib/**/*", + "build/cjs/package.json", + "build/esm/lib/**/*", + "build/esm/package.json" ], + "type": "module", + "main": "./build/cjs/lib/asyncbox.js", + "module": "./build/esm/lib/asyncbox.js", + "types": "./build/cjs/lib/asyncbox.d.ts", + "exports": { + ".": { + "import": { + "types": "./build/esm/lib/asyncbox.d.ts", + "default": "./build/esm/lib/asyncbox.js" + }, + "require": { + "types": "./build/cjs/lib/asyncbox.d.ts", + "default": "./build/cjs/lib/asyncbox.js" + } + }, + "./package.json": "./package.json" + }, "scripts": { - "build": "tsc -b", + "build": "tsc -b tsconfig.esm.json tsconfig.cjs.json", + "postbuild": "node scripts/postbuild.mjs", "clean": "npm run build -- --clean", "rebuild": "npm run clean; npm run build", "dev": "npm run build -- --watch", "prepare": "npm run rebuild", - "test": "node --test --enable-source-maps --test-force-exit --test-timeout=60000 \"./build/test/**/*.spec.js\"", - "lint": "eslint .", - "format": "prettier -w ./lib ./test", - "format:check": "prettier --check ./lib ./test", + "test": "node --test --enable-source-maps --test-force-exit --test-timeout=60000 \"./build/esm/test/**/*.spec.js\" \"./test/*.spec.cjs\"", + "lint": "oxlint -c oxlint.config.mjs .", + "lint:fix": "oxlint -c oxlint.config.mjs --fix .", + "format": "oxfmt -c oxfmt.config.mjs .", + "format:check": "oxfmt -c oxfmt.config.mjs --check .", "watch": "npm run dev" }, - "prettier": { - "bracketSpacing": false, - "printWidth": 100, - "singleQuote": true - }, "dependencies": { "p-limit": "^7.2.0" }, "devDependencies": { - "@appium/eslint-config-appium-ts": "^3.2.3", - "@appium/tsconfig": "^1.0.0", - "@semantic-release/changelog": "^6.0.1", - "@semantic-release/git": "^10.0.1", - "@types/node": "^26.1.0", - "conventional-changelog-conventionalcommits": "^9.0.0", - "prettier": "^3.0.0", - "semantic-release": "^25.0.2", - "typescript": "^6.0.3" + "@appium/oxc-config": "^1.1.0", + "@appium/semantic-release-config": "^1.1.0", + "@appium/tsconfig": "^1.2.0", + "@types/node": "^26.1.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", + "npm": ">=10" }, - "types": "./build/lib/asyncbox.d.ts" + "tags": [ + "async/await", + "es7", + "async" + ] } diff --git a/release.config.mjs b/release.config.mjs new file mode 100644 index 0000000..83f3400 --- /dev/null +++ b/release.config.mjs @@ -0,0 +1,3 @@ +import releaseConfig from '@appium/semantic-release-config'; + +export default releaseConfig(); diff --git a/scripts/postbuild.mjs b/scripts/postbuild.mjs new file mode 100644 index 0000000..1b0f9cf --- /dev/null +++ b/scripts/postbuild.mjs @@ -0,0 +1,9 @@ +import {mkdirSync, writeFileSync} from 'node:fs'; + +for (const [dir, type] of [ + ['build/esm', 'module'], + ['build/cjs', 'commonjs'], +]) { + mkdirSync(dir, {recursive: true}); + writeFileSync(`${dir}/package.json`, `${JSON.stringify({type}, null, 2)}\n`); +} diff --git a/test/asyncbox.spec.ts b/test/asyncbox.spec.ts index 7114455..3379ec9 100644 --- a/test/asyncbox.spec.ts +++ b/test/asyncbox.spec.ts @@ -1,3 +1,6 @@ +import assert from 'node:assert/strict'; +import {it, describe, beforeEach} from 'node:test'; + import { sleep, longSleep, @@ -11,8 +14,6 @@ import { PromiseCancellationError, type SleepOptions, } from '../lib/asyncbox.js'; -import {it, describe, beforeEach} from 'node:test'; -import assert from 'node:assert/strict'; describe('sleep', function () { it('should work like setTimeout', async function () { @@ -93,10 +94,7 @@ describe('sleep', function () { assert.throws(() => sleep([] as unknown as number), /finite number or an object with ms/); }); it('should throw TypeError when object has invalid ms', function () { - assert.throws( - () => sleep({} as unknown as SleepOptions), - /options\.ms must be a finite number/, - ); + assert.throws(() => sleep({} as unknown as SleepOptions), /options\.ms must be a finite number/); assert.throws(() => sleep({ms: Number.NaN}), /options\.ms must be a finite number/); }); it('should accept a null-prototype object with ms', async function () { @@ -309,7 +307,7 @@ describe('retry', function () { assert.ok(err); assert.strictEqual(err!.message, 'not ok yet'); assert.strictEqual(eventuallyOkFnCalls, 3); - assert.ok(Date.now() - start >= 30); + assert.ok(Date.now() - start >= 29); // rerun with ok number of calls start = Date.now(); @@ -317,7 +315,7 @@ describe('retry', function () { const res = await retryInterval(3, 15, eventuallyOkNoSleepFn, 3); assert.strictEqual(eventuallyOkFnCalls, 3); assert.strictEqual(res, 9); - assert.ok(Date.now() - start >= 30); + assert.ok(Date.now() - start >= 29); }); it('should not wait on the final error', async function () { const start = Date.now(); @@ -375,7 +373,7 @@ describe('asyncmap', function () { it('should map elements one at a time', async function () { const start = Date.now(); assert.deepStrictEqual(await asyncmap(coll, mapper, false), newColl); - assert.ok(Date.now() - start >= 50); + assert.ok(Date.now() - start >= 49); }); it('should map elements in parallel', async function () { const start = Date.now(); @@ -386,7 +384,7 @@ describe('asyncmap', function () { const start = Date.now(); assert.deepStrictEqual(await asyncmap(coll, mapper, {concurrency: 2}), newColl); assert.ok(Date.now() - start >= 29); - assert.ok(Date.now() - start <= 40); + assert.ok(Date.now() - start <= 60); }); it('should handle an empty array', async function () { assert.deepStrictEqual(await asyncmap([], mapper, false), []); @@ -414,7 +412,7 @@ describe('asyncfilter', function () { it('should filter elements one at a time', async function () { const start = Date.now(); assert.deepStrictEqual(await asyncfilter(coll, filter, false), newColl); - assert.ok(Date.now() - start >= 50); + assert.ok(Date.now() - start >= 49); }); it('should filter elements in parallel', async function () { const start = Date.now(); @@ -425,7 +423,7 @@ describe('asyncfilter', function () { const start = Date.now(); assert.deepStrictEqual(await asyncfilter(coll, filter, {concurrency: 2}), newColl); assert.ok(Date.now() - start >= 29); - assert.ok(Date.now() - start <= 40); + assert.ok(Date.now() - start <= 60); }); it('should handle an empty array', async function () { assert.deepStrictEqual(await asyncfilter([], filter, false), []); diff --git a/test/cjs-interop.spec.cjs b/test/cjs-interop.spec.cjs new file mode 100644 index 0000000..f24bbb0 --- /dev/null +++ b/test/cjs-interop.spec.cjs @@ -0,0 +1,9 @@ +const assert = require('node:assert/strict'); +const {test} = require('node:test'); +const asyncbox = require('../build/cjs/lib/asyncbox.js'); + +test('require() interop with the CJS build', async () => { + assert.equal(typeof asyncbox.sleep, 'function'); + assert.equal(typeof asyncbox.retry, 'function'); + await asyncbox.sleep(1); +}); diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 0000000..f750934 --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "build/cjs", + "module": "CommonJS", + "moduleResolution": "Bundler" + } +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 0000000..30361d7 --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "build/esm" + } +} diff --git a/tsconfig.json b/tsconfig.json index 6865db8..745b79a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,12 +3,9 @@ "extends": "@appium/tsconfig/tsconfig.json", "compilerOptions": { "strict": true, - "outDir": "build", "types": ["node"], "checkJs": true }, - "include": [ - "lib", - "test" - ] + "include": ["lib", "test"], + "exclude": ["test/*.cjs"] }