diff --git a/package.json b/package.json index b89e59e11..bab1eb55c 100644 --- a/package.json +++ b/package.json @@ -265,6 +265,7 @@ "fs-extra": "^11.1.0", "gh-pages": "^6.0.0", "globby": "^14.0.0", + "is-plain-obj": "^4.1.0", "kleur": "^4.1.4", "lilconfig": "^3.0.0", "listr": "~0.14.2", @@ -275,7 +276,6 @@ "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", - "merge-options": "^3.0.4", "micromark-extension-gfm": "^3.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", @@ -292,7 +292,6 @@ "path": "^0.12.7", "playwright-test": "^14.0.0", "polka": "^0.5.2", - "premove": "^4.0.0", "prompt": "^1.2.2", "proper-lockfile": "^4.1.2", "react-native-test-runner": "^5.0.0", diff --git a/src/build/index.js b/src/build/index.js index 1f6c458fe..54537dfbb 100644 --- a/src/build/index.js +++ b/src/build/index.js @@ -6,8 +6,8 @@ import esbuild from 'esbuild' import { execa } from 'execa' import fs from 'fs-extra' import Listr from 'listr' -import merge from 'merge-options' import pascalcase from 'pascalcase' +import merge from '../utils/merge-options.js' import { gzipSize, pkg, hasTsconfig, isTypescript, fromRoot, paths, findBinary } from './../utils.js' const defaults = merge.bind({ diff --git a/src/check-project/manifests/typed-cjs.js b/src/check-project/manifests/typed-cjs.js index e3c746094..5a90ace9e 100644 --- a/src/check-project/manifests/typed-cjs.js +++ b/src/check-project/manifests/typed-cjs.js @@ -1,4 +1,4 @@ -import mergeOptions from 'merge-options' +import mergeOptions from '../../utils/merge-options.js' import { semanticReleaseConfig } from '../semantic-release-config.js' import { sortFields, diff --git a/src/check-project/manifests/typed-esm.js b/src/check-project/manifests/typed-esm.js index be0f14880..d8aed8423 100644 --- a/src/check-project/manifests/typed-esm.js +++ b/src/check-project/manifests/typed-esm.js @@ -1,4 +1,4 @@ -import mergeOptions from 'merge-options' +import mergeOptions from '../../utils/merge-options.js' import { semanticReleaseConfig } from '../semantic-release-config.js' import { sortFields, diff --git a/src/check-project/manifests/typescript.js b/src/check-project/manifests/typescript.js index 79e90af99..671a8e9fc 100644 --- a/src/check-project/manifests/typescript.js +++ b/src/check-project/manifests/typescript.js @@ -1,6 +1,6 @@ /* eslint-disable no-console */ -import mergeOptions from 'merge-options' +import mergeOptions from '../../utils/merge-options.js' import { semanticReleaseConfig } from '../semantic-release-config.js' import { sortFields, diff --git a/src/check-project/manifests/untyped-cjs.js b/src/check-project/manifests/untyped-cjs.js index 34b02de54..84b3daf3a 100644 --- a/src/check-project/manifests/untyped-cjs.js +++ b/src/check-project/manifests/untyped-cjs.js @@ -1,4 +1,4 @@ -import mergeOptions from 'merge-options' +import mergeOptions from '../../utils/merge-options.js' import { semanticReleaseConfig } from '../semantic-release-config.js' import { sortFields, diff --git a/src/check-project/manifests/untyped-esm.js b/src/check-project/manifests/untyped-esm.js index 628107f3c..4a9dd4693 100644 --- a/src/check-project/manifests/untyped-esm.js +++ b/src/check-project/manifests/untyped-esm.js @@ -1,4 +1,4 @@ -import mergeOptions from 'merge-options' +import mergeOptions from '../../utils/merge-options.js' import { semanticReleaseConfig } from '../semantic-release-config.js' import { sortFields, diff --git a/src/cmds/check.js b/src/cmds/check.js index 09e17afe2..f8db9785d 100644 --- a/src/cmds/check.js +++ b/src/cmds/check.js @@ -6,9 +6,9 @@ import path from 'path' import esbuild from 'esbuild' import fs from 'fs-extra' import kleur from 'kleur' -import merge from 'merge-options' import { readPackageUp } from 'read-pkg-up' import { loadUserConfig } from '../config/user.js' +import merge from '../utils/merge-options.js' import { fromRoot, paths } from '../utils.js' const defaults = merge.bind({ diff --git a/src/config/user.js b/src/config/user.js index 3c5f5b023..0f8d0cc83 100644 --- a/src/config/user.js +++ b/src/config/user.js @@ -2,7 +2,7 @@ import { pathToFileURL } from 'url' import { lilconfig } from 'lilconfig' -import merge from 'merge-options' +import merge from '../utils/merge-options.js' import { isTypescript } from '../utils.js' /** diff --git a/src/docs.js b/src/docs.js index 5a0dc0ebb..e7b8caa75 100644 --- a/src/docs.js +++ b/src/docs.js @@ -5,7 +5,6 @@ import { execa } from 'execa' import fs from 'fs-extra' import ghPages from 'gh-pages' import Listr from 'listr' -import { premove as del } from 'premove/sync' import { hasTsconfig, fromAegir, fromRoot, readJson, isMonorepoParent } from './utils.js' const publishPages = promisify(ghPages.publish) @@ -153,7 +152,11 @@ const tasks = new Listr( * @param {GlobalOptions & DocsOptions} ctx */ task: (ctx) => { - del(ctx.directory) + if (fs.existsSync(ctx.directory)) { + fs.rmdirSync(ctx.directory, { + recursive: true + }) + } } }, { diff --git a/src/document-check.js b/src/document-check.js index eba44ed36..1885f8acd 100644 --- a/src/document-check.js +++ b/src/document-check.js @@ -4,8 +4,8 @@ import fs from 'fs-extra' import { globby } from 'globby' import kleur from 'kleur' import Listr from 'listr' -import merge from 'merge-options' import { compileSnippets } from 'typescript-docs-verifier' +import merge from './utils/merge-options.js' import { formatCode, formatError, fromRoot, hasTsconfig, readJson } from './utils.js' /** * @typedef {import("./types").GlobalOptions} GlobalOptions diff --git a/src/lint.js b/src/lint.js index c46e84268..f873ab283 100644 --- a/src/lint.js +++ b/src/lint.js @@ -8,7 +8,7 @@ import fs from 'fs-extra' import { globby } from 'globby' import kleur from 'kleur' import Listr from 'listr' -import merge from 'merge-options' +import merge from './utils/merge-options.js' import { fromRoot, readJson, hasTsconfig, isTypescript, findBinary, hasDocCheck } from './utils.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/src/test/browser.js b/src/test/browser.js index ac9a870c0..9302c124f 100644 --- a/src/test/browser.js +++ b/src/test/browser.js @@ -1,7 +1,7 @@ import path from 'path' import { fileURLToPath } from 'url' import { execa } from 'execa' -import merge from 'merge-options' +import merge from '../utils/merge-options.js' import { fromAegir, findBinary } from '../utils.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/src/test/electron.js b/src/test/electron.js index f6670c2dd..341805730 100644 --- a/src/test/electron.js +++ b/src/test/electron.js @@ -1,7 +1,7 @@ import path from 'path' import { fileURLToPath } from 'url' import { execa } from 'execa' -import merge from 'merge-options' +import merge from '../utils/merge-options.js' import { getElectron, findBinary } from '../utils.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/src/test/node.js b/src/test/node.js index d29e7a2b7..378ec859f 100644 --- a/src/test/node.js +++ b/src/test/node.js @@ -3,8 +3,8 @@ import path from 'path' import { fileURLToPath } from 'url' import { execa } from 'execa' import kleur from 'kleur' -import merge from 'merge-options' import * as tempy from 'tempy' +import merge from '../utils/merge-options.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/src/test/react-native.js b/src/test/react-native.js index a06dca886..7799ac8b7 100644 --- a/src/test/react-native.js +++ b/src/test/react-native.js @@ -1,7 +1,7 @@ import path from 'path' import { fileURLToPath } from 'url' import { execa } from 'execa' -import merge from 'merge-options' +import merge from '../utils/merge-options.js' const __dirname = path.dirname(fileURLToPath(import.meta.url)) diff --git a/src/utils/merge-options.js b/src/utils/merge-options.js new file mode 100644 index 000000000..5669cd0ec --- /dev/null +++ b/src/utils/merge-options.js @@ -0,0 +1,199 @@ +import isOptionObject from 'is-plain-obj' + +const { hasOwnProperty } = Object.prototype +const { propertyIsEnumerable } = Object +/** + * @param {*} object + * @param {string | symbol | number} name + * @param {any} value + */ +const defineProperty = (object, name, value) => Object.defineProperty(object, name, { + value, + writable: true, + enumerable: true, + configurable: true +}) + +const globalThis = this +const defaultMergeOptions = { + concatArrays: false, + ignoreUndefined: false +} + +/** + * @param {*} value + * @returns {Array} + */ +const getEnumerableOwnPropertyKeys = value => { + /** @type {Array} */ + const keys = [] + + for (const key in value) { + if (hasOwnProperty.call(value, key)) { + keys.push(key) + } + } + + /* istanbul ignore else */ + if (Object.getOwnPropertySymbols) { + const symbols = Object.getOwnPropertySymbols(value) + + for (const symbol of symbols) { + if (propertyIsEnumerable.call(value, symbol)) { + keys.push(symbol) + } + } + } + + return keys +} + +/** + * @param {*} value + */ +function clone (value) { + if (Array.isArray(value)) { + return cloneArray(value) + } + + if (isOptionObject(value)) { + return cloneOptionObject(value) + } + + return value +} + +/** + * @param {*} array + */ +function cloneArray (array) { + const result = array.slice(0, 0) + + getEnumerableOwnPropertyKeys(array).forEach(key => { + defineProperty(result, key, clone(array[key])) + }) + + return result +} + +/** + * @param {*} object + */ +function cloneOptionObject (object) { + const result = Object.getPrototypeOf(object) === null ? Object.create(null) : {} + + getEnumerableOwnPropertyKeys(object).forEach(key => { + defineProperty(result, key, clone(object[key])) + }) + + return result +} + +/** + * @param {*} merged - already cloned + * @param {*} source - something to merge + * @param {Array} keys - keys to merge + * @param {object} config - Config Object + * @param {boolean} [config.ignoreUndefined] - whether to ignore undefined values + * @param {boolean} [config.concatArrays] - Config Object + * @returns {*} cloned Object + */ +const mergeKeys = (merged, source, keys, config) => { + keys.forEach(key => { + if (typeof source[key] === 'undefined' && config.ignoreUndefined) { + return + } + + // Do not recurse into prototype chain of merged + if (key in merged && merged[key] !== Object.getPrototypeOf(merged)) { + defineProperty(merged, key, merge(merged[key], source[key], config)) + } else { + defineProperty(merged, key, clone(source[key])) + } + }) + + return merged +} + +/** + * @param {*} merged - already cloned + * @param {*} source - something to merge + * @param {object} config - Config Object + * @returns {*} cloned Object + * + * see [Array.prototype.concat ( ...arguments )](http://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.concat) + */ +const concatArrays = (merged, source, config) => { + let result = merged.slice(0, 0) + let resultIndex = 0; + + [merged, source].forEach(array => { + /** @type {Array} */ + const indices = [] + + // `result.concat(array)` with cloning + for (let k = 0; k < array.length; k++) { + if (!hasOwnProperty.call(array, k)) { + continue + } + + indices.push(String(k)) + + if (array === merged) { + // Already cloned + defineProperty(result, resultIndex++, array[k]) + } else { + defineProperty(result, resultIndex++, clone(array[k])) + } + } + + // Merge non-index keys + result = mergeKeys(result, array, getEnumerableOwnPropertyKeys(array).filter(key => !indices.includes(key)), config) + }) + + return result +} + +/** + * @param {*} merged - already cloned + * @param {*} source - something to merge + * @param {object} config - Config Object + * @param {boolean} [config.concatArrays] - Config Object + * @returns {*} cloned Object + */ +function merge (merged, source, config) { + if (config.concatArrays && Array.isArray(merged) && Array.isArray(source)) { + return concatArrays(merged, source, config) + } + + if (!isOptionObject(source) || !isOptionObject(merged)) { + return clone(source) + } + + return mergeKeys(merged, source, getEnumerableOwnPropertyKeys(source), config) +} + +/** + * @param {...any} options + */ +function mergeOptions (...options) { + // @ts-expect-error this is shadowed by the container + const config = merge(clone(defaultMergeOptions), (this !== globalThis && this) || {}, defaultMergeOptions) + let merged = { _: {} } + + for (const option of options) { + if (option === undefined) { + continue + } + + if (!isOptionObject(option)) { + throw new TypeError('`' + option + '` is not an Option Object') + } + + merged = merge(merged, { _: option }, config) + } + + return merged._ +} + +export default mergeOptions diff --git a/test/lint.js b/test/lint.js index 8c51a7916..008422d17 100644 --- a/test/lint.js +++ b/test/lint.js @@ -1,9 +1,8 @@ /* eslint-env mocha */ -import fs from 'fs' import path from 'path' import { fileURLToPath } from 'url' -import { premove as del } from 'premove/sync' +import fs from 'fs-extra' import { loadUserConfig } from '../src/config/user.js' import lint from '../src/lint.js' import { expect } from '../utils/chai.js' @@ -75,7 +74,11 @@ describe('lint', () => { after(() => { process.chdir(cwd) - del(TEMP_FOLDER) + if (fs.existsSync(TEMP_FOLDER)) { + fs.rmdirSync(TEMP_FOLDER, { + recursive: true + }) + } }) it('lint itself (aegir)', async function () {