From fa3b95c2e1a3a3351dc1b0168aa2c26f0cc4e9b3 Mon Sep 17 00:00:00 2001 From: Sosuke Suzuki Date: Sun, 10 Apr 2022 15:26:07 +0900 Subject: [PATCH] Remove `lodash` (#12628) * Replace `groupBy` * Replace `partition` * Replace `pick` * Move `partition` * Replace `uniqBy` * Replace `fromPairs` * Remove usage from website * Reuse `partition` * Remove from package.json * Fix * Use `ignore.default` * Add `resolveConfig` to `FileInfoOptions` * Explicit tuple types * Make `cache` optional * Fix cosmiconfig type * Use `@ts-expect-error` for private property access * Add type annotation for validate * Fix type errors for `OptionInfo` * Add `@ts-expect-error` * Fix type of `normalizeOptions` * Fix `OptionInfo` type * Add `@ts-expect-error` for `option-normalizer` * Avoid type error * Fix typo * Add types for lodash fns Co-authored-by: fisker Cheung --- package.json | 1 - src/cli/options/create-minimist-options.js | 4 +- src/cli/options/parse-cli-arguments.js | 2 +- src/cli/usage.js | 2 +- src/cli/utils.js | 37 ++++++++++++++++++- src/common/create-ignorer.js | 2 +- src/common/get-file-info.js | 2 +- src/common/load-plugins.js | 6 +-- src/config/resolve-config.js | 29 ++++++++------- src/index.js | 6 +++ src/main/core-options.js | 2 +- src/main/multiparser.js | 2 + src/main/options-normalizer.js | 28 ++++++++++++-- src/main/support.js | 6 +++ src/utils/partition.js | 20 ++++++++++ src/utils/uniq-by-key.js | 24 ++++++++++++ tests/format/markdown/auto-link/jsfmt.spec.js | 8 +++- tests/integration/__tests__/file-info.js | 5 +-- website/playground/sidebar/SidebarOptions.js | 2 +- 19 files changed, 155 insertions(+), 33 deletions(-) create mode 100644 src/utils/partition.js create mode 100644 src/utils/uniq-by-key.js diff --git a/package.json b/package.json index 6f5d50c2a..b2691d5db 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "leven": "4.0.0", "lines-and-columns": "2.0.3", "linguist-languages": "7.15.0", - "lodash": "4.17.21", "mem": "9.0.2", "meriyah": "4.2.1", "micromatch": "4.0.5", diff --git a/src/cli/options/create-minimist-options.js b/src/cli/options/create-minimist-options.js index baaa4cf46..fe0deb200 100644 --- a/src/cli/options/create-minimist-options.js +++ b/src/cli/options/create-minimist-options.js @@ -1,6 +1,8 @@ "use strict"; -const partition = require("lodash/partition"); +const { + utils: { partition }, +} = require("../prettier-internal.js"); module.exports = function createMinimistOptions(detailedOptions) { const [boolean, string] = partition( diff --git a/src/cli/options/parse-cli-arguments.js b/src/cli/options/parse-cli-arguments.js index 66433266e..ba3142162 100644 --- a/src/cli/options/parse-cli-arguments.js +++ b/src/cli/options/parse-cli-arguments.js @@ -1,6 +1,6 @@ "use strict"; -const pick = require("lodash/pick"); const camelCase = require("camelcase"); +const { pick } = require("../utils.js"); const getContextOptions = require("./get-context-options.js"); const minimist = require("./minimist.js"); const createMinimistOptions = require("./create-minimist-options.js"); diff --git a/src/cli/usage.js b/src/cli/usage.js index e1be6badd..d74bdb237 100644 --- a/src/cli/usage.js +++ b/src/cli/usage.js @@ -1,8 +1,8 @@ "use strict"; -const groupBy = require("lodash/groupBy"); const camelCase = require("camelcase"); const constant = require("./constant.js"); +const { groupBy } = require("./utils.js"); const OPTION_USAGE_THRESHOLD = 25; const CHOICE_USAGE_MARGIN = 3; diff --git a/src/cli/utils.js b/src/cli/utils.js index 6c0bff9fd..3fc2637dd 100644 --- a/src/cli/utils.js +++ b/src/cli/utils.js @@ -3,4 +3,39 @@ // eslint-disable-next-line no-console const printToScreen = console.log.bind(console); -module.exports = { printToScreen }; +/** + * @template Obj + * @template Key + * @param {Array} array + * @param {(value: Obj) => Key} iteratee + * @returns {{[p in Key]: T}} + */ +function groupBy(array, iteratee) { + const result = Object.create(null); + + for (const value of array) { + const key = iteratee(value); + + if (Array.isArray(result[key])) { + result[key].push(value); + } else { + result[key] = [value]; + } + } + + return result; +} + +/** + * @template Obj + * @template {keyof Obj} Keys + * @param {Obj} object + * @param {Array} keys + * @returns {{[key in Keys]: Obj[key]}} + */ +function pick(object, keys) { + const entries = keys.map((key) => [key, object[key]]); + return Object.fromEntries(entries); +} + +module.exports = { printToScreen, groupBy, pick }; diff --git a/src/common/create-ignorer.js b/src/common/create-ignorer.js index 4ccfc4121..5bae530ad 100644 --- a/src/common/create-ignorer.js +++ b/src/common/create-ignorer.js @@ -1,7 +1,7 @@ "use strict"; const path = require("path"); -const ignore = require("ignore"); +const ignore = require("ignore").default; const getFileContentOrNull = require("../utils/get-file-content-or-null.js"); /** diff --git a/src/common/get-file-info.js b/src/common/get-file-info.js index d01d1ee95..e6851e39f 100644 --- a/src/common/get-file-info.js +++ b/src/common/get-file-info.js @@ -6,7 +6,7 @@ const config = require("../config/resolve-config.js"); const createIgnorer = require("./create-ignorer.js"); /** - * @typedef {{ ignorePath?: string, withNodeModules?: boolean, plugins: object }} FileInfoOptions + * @typedef {{ ignorePath?: string, withNodeModules?: boolean, plugins: object, resolveConfig?: boolean }} FileInfoOptions * @typedef {{ ignored: boolean, inferredParser: string | null }} FileInfoResult */ diff --git a/src/common/load-plugins.js b/src/common/load-plugins.js index 374730ed4..fd520ef1d 100644 --- a/src/common/load-plugins.js +++ b/src/common/load-plugins.js @@ -2,9 +2,9 @@ const fs = require("fs"); const path = require("path"); -const uniqBy = require("lodash/uniqBy"); -const partition = require("lodash/partition"); const fastGlob = require("fast-glob"); +const partition = require("../utils/partition.js"); +const uniqByKey = require("../utils/uniq-by-key.js"); const internalPlugins = require("../languages.js"); const { default: mem, memClear } = require("../../vendors/mem.js"); const thirdParty = require("./third-party.js"); @@ -91,7 +91,7 @@ function load(plugins, pluginSearchDirs) { ); const externalPlugins = [ - ...uniqBy( + ...uniqByKey( [...externalManualLoadPluginInfos, ...externalAutoLoadPluginInfos], "requirePath" ).map((externalPluginInfo) => ({ diff --git a/src/config/resolve-config.js b/src/config/resolve-config.js index 560ac428e..e5ee80221 100644 --- a/src/config/resolve-config.js +++ b/src/config/resolve-config.js @@ -6,17 +6,21 @@ const thirdParty = require("../common/third-party.js"); const loadToml = require("../utils/load-toml.js"); const loadJson5 = require("../utils/load-json5.js"); +const partition = require("../utils/partition.js"); const resolve = require("../common/resolve.js"); const { default: mem, memClear } = require("../../vendors/mem.js"); const resolveEditorConfig = require("./resolve-config-editorconfig.js"); /** - * @typedef {import("cosmiconfig/dist/Explorer").Explorer} Explorer - * @typedef {{sync: boolean; cache: boolean }} Options + * @typedef {ReturnType} Explorer + * @typedef {ReturnType} SyncExplorer + * @typedef {{sync?: boolean; cache?: boolean }} Options */ /** - * @type {(opts: Options) => Explorer} + * @template {Options} Opts + * @param {Opts} opts + * @return {Opts["sync"] extends true ? SyncExplorer : Explorer} */ const getExplorerMemoized = mem( (opts) => { @@ -67,8 +71,9 @@ const getExplorerMemoized = mem( ); /** - * @param {Options} opts - * @return {Explorer} + * @template {Options} Opts + * @param {Opts} opts + * @return {Opts["sync"] extends true ? SyncExplorer : Explorer} */ function getExplorer(opts) { // Normalize opts before passing to a memoized function @@ -85,6 +90,7 @@ function _resolveConfig(filePath, opts, sync) { }; const { load, search } = getExplorer(loadOpts); const loadEditorConfig = resolveEditorConfig.getLoadFunction(loadOpts); + /** @type {[any, any]} */ const arr = [ opts.config ? load(opts.config) : search(filePath), loadEditorConfig(filePath), @@ -169,15 +175,10 @@ function pathMatchesGlobs(filePath, patterns, excludedPatterns) { const patternList = Array.isArray(patterns) ? patterns : [patterns]; // micromatch always matches against basename when the option is enabled // use only patterns without slashes with it to match minimatch behavior - const withSlashes = []; - const withoutSlashes = []; - for (const pattern of patternList) { - if (pattern.includes("/")) { - withSlashes.push(pattern); - } else { - withoutSlashes.push(pattern); - } - } + const [withSlashes, withoutSlashes] = partition(patternList, (pattern) => + pattern.includes("/") + ); + return ( micromatch.isMatch(filePath, withoutSlashes, { ignore: excludedPatterns, diff --git a/src/index.js b/src/index.js index ed6e6884b..dae7138ec 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,11 @@ function _withPlugins( }; } +/** + * @param {*} fn + * @param {*} optsArgIdx + * @returns {*} + */ function withPlugins(fn, optsArgIdx) { const resultingFn = _withPlugins(fn, optsArgIdx); if (fn.sync) { @@ -75,6 +80,7 @@ module.exports = { utils: { arrayify: require("./utils/arrayify.js"), getLast: require("./utils/get-last.js"), + partition: require("./utils/partition.js"), isNonEmptyArray: require("./common/util.js").isNonEmptyArray, }, }, diff --git a/src/main/core-options.js b/src/main/core-options.js index 5992da931..fa448b2fd 100644 --- a/src/main/core-options.js +++ b/src/main/core-options.js @@ -14,7 +14,7 @@ const CATEGORY_SPECIAL = "Special"; * @typedef {Object} OptionInfo * @property {string} [since] - available since version * @property {string} category - * @property {'int' | 'boolean' | 'choice' | 'path'} type + * @property {'int' | 'boolean' | 'choice' | 'path' | 'string' | 'flag'} type * @property {boolean} [array] - indicate it's an array of the specified type * @property {OptionValueInfo} [default] * @property {OptionRangeInfo} [range] - for type int diff --git a/src/main/multiparser.js b/src/main/multiparser.js index 3af8b24f7..0de83cd09 100644 --- a/src/main/multiparser.js +++ b/src/main/multiparser.js @@ -49,7 +49,9 @@ function textToDoc( const astComments = ast.comments; delete ast.comments; comments.attach(astComments, ast, text, nextOptions); + // @ts-expect-error -- Casting to `unique symbol` isn't allowed in JSDoc comment nextOptions[Symbol.for("comments")] = astComments || []; + // @ts-expect-error -- Casting to `unique symbol` isn't allowed in JSDoc comment nextOptions[Symbol.for("tokens")] = ast.tokens || []; const doc = printAstToDoc(ast, nextOptions); diff --git a/src/main/options-normalizer.js b/src/main/options-normalizer.js index 9fe5fe0d2..17b2eb5c3 100644 --- a/src/main/options-normalizer.js +++ b/src/main/options-normalizer.js @@ -3,6 +3,10 @@ const vnopts = require("vnopts"); const getLast = require("../utils/get-last.js"); +/** + * @typedef {import("./support").NamedOptionInfo} NamedOptionInfo + */ + const cliDescriptor = { key: (key) => (key.length === 1 ? `-${key}` : `--${key}`), value: (value) => vnopts.apiDescriptor.value(value), @@ -55,15 +59,20 @@ const getFlagSchema = ({ colorsModule, levenshteinDistance }) => let hasDeprecationWarned; +/** + * @param {*} options + * @param {*} optionInfos + * @param {{ logger?: false; isCLI?: boolean; passThrough?: boolean; colorsModule?: any; levenshteinDistance?: any }} param2 + */ function normalizeOptions( options, optionInfos, { - logger, + logger = false, isCLI = false, passThrough = false, - colorsModule, - levenshteinDistance, + colorsModule = null, + levenshteinDistance = null, } = {} ) { const unknown = !passThrough @@ -95,12 +104,14 @@ function normalizeOptions( const shouldSuppressDuplicateDeprecationWarnings = logger !== false; if (shouldSuppressDuplicateDeprecationWarnings && hasDeprecationWarned) { + // @ts-expect-error normalizer._hasDeprecationWarned = hasDeprecationWarned; } const normalized = normalizer.normalize(options); if (shouldSuppressDuplicateDeprecationWarnings) { + // @ts-expect-error hasDeprecationWarned = normalizer._hasDeprecationWarned; } @@ -134,6 +145,7 @@ function optionInfosToSchemas( if (optionInfo.alias && isCLI) { schemas.push( vnopts.AliasSchema.create({ + // @ts-expect-error name: optionInfo.alias, sourceName: optionInfo.name, }) @@ -144,6 +156,11 @@ function optionInfosToSchemas( return schemas; } +/** + * @param {NamedOptionInfo} optionInfo + * @param {any} param1 + * @returns + */ function optionInfoToSchema( optionInfo, { isCLI, optionInfos, colorsModule, levenshteinDistance } @@ -152,6 +169,7 @@ function optionInfoToSchema( if (name === "plugin-search-dir" || name === "pluginSearchDirs") { return vnopts.AnySchema.create({ + // @ts-expect-error name, preprocess(value) { if (value === false) { @@ -160,6 +178,9 @@ function optionInfoToSchema( value = Array.isArray(value) ? value : [value]; return value; }, + /** + * @param {Array | false} value + */ validate(value) { if (value === false) { return true; @@ -260,6 +281,7 @@ function optionInfoToSchema( ? vnopts.ArraySchema.create({ ...(isCLI ? { preprocess: (v) => (Array.isArray(v) ? v : [v]) } : {}), ...handlers, + // @ts-expect-error valueSchema: SchemaConstructor.create(parameters), }) : SchemaConstructor.create({ ...parameters, ...handlers }); diff --git a/src/main/support.js b/src/main/support.js index 6bf3ad8d8..c298ff180 100644 --- a/src/main/support.js +++ b/src/main/support.js @@ -9,6 +9,11 @@ const arrayify = require("../utils/arrayify.js"); const currentVersion = require("../../package.json").version; const coreOptions = require("./core-options.js").options; +/** + * @typedef {import("./core-options").OptionInfo} OptionInfo + * @typedef {{ name: string; pluginDefaults: Array } & OptionInfo} NamedOptionInfo + */ + /** * Strings in `plugins` and `pluginSearchDirs` are handled by a wrapped version * of this function created by `withPlugins`. Don't pass them here directly. @@ -18,6 +23,7 @@ const coreOptions = require("./core-options.js").options; * @param {boolean=} param0.showUnreleased * @param {boolean=} param0.showDeprecated * @param {boolean=} param0.showInternal + * @return {{ languages: Array, options: Array }} */ function getSupportInfo({ plugins = [], diff --git a/src/utils/partition.js b/src/utils/partition.js new file mode 100644 index 000000000..4d6b92a46 --- /dev/null +++ b/src/utils/partition.js @@ -0,0 +1,20 @@ +"use strict"; + +/** + * @template Element + * @param {Array} array + * @param {(value: Element) => boolean} predicate + * @returns {[Array, Array]} + */ +function partition(array, predicate) { + /** @type {[Array, Array]} */ + const result = [[], []]; + + for (const value of array) { + result[predicate(value) ? 0 : 1].push(value); + } + + return result; +} + +module.exports = partition; diff --git a/src/utils/uniq-by-key.js b/src/utils/uniq-by-key.js new file mode 100644 index 000000000..ad8b249c0 --- /dev/null +++ b/src/utils/uniq-by-key.js @@ -0,0 +1,24 @@ +"use strict"; + +/** + * @template {object} Element + * @param {Array} array + * @param {string | number | symbol} key + * @returns + */ +function uniqByKey(array, key) { + const result = []; + const seen = new Set(); + + for (const element of array) { + const value = element[key]; + if (!seen.has(value)) { + seen.add(value); + result.push(element); + } + } + + return result; +} + +module.exports = uniqByKey; diff --git a/tests/format/markdown/auto-link/jsfmt.spec.js b/tests/format/markdown/auto-link/jsfmt.spec.js index df6f3d64a..176a31f18 100644 --- a/tests/format/markdown/auto-link/jsfmt.spec.js +++ b/tests/format/markdown/auto-link/jsfmt.spec.js @@ -1,4 +1,10 @@ -const flat = require("lodash/flatten"); +/** + * @template T + * @param {Array>} array + * @returns {Array} + */ +// eslint-disable-next-line unicorn/prefer-spread +const flat = (array) => [].concat(...array); const urls = [ "http://www.example.com:80/_a", diff --git a/tests/integration/__tests__/file-info.js b/tests/integration/__tests__/file-info.js index 4ba53362c..d0d8f9938 100644 --- a/tests/integration/__tests__/file-info.js +++ b/tests/integration/__tests__/file-info.js @@ -2,7 +2,6 @@ const path = require("path"); const fs = require("fs"); -const fromPairs = require("lodash/fromPairs"); const prettier = require("prettier-local"); const runPrettier = require("../run-prettier.js"); @@ -162,7 +161,7 @@ test("API getFileInfo.sync with filepath only", () => { }); describe("API getFileInfo resolveConfig", () => { - const files = fromPairs( + const files = Object.fromEntries( ["foo", "js", "bar", "css"].map((ext) => [ ext, path.resolve( @@ -269,7 +268,7 @@ describe("API getFileInfo resolveConfig", () => { }); describe("API getFileInfo resolveConfig when no config is present", () => { - const files = fromPairs( + const files = Object.fromEntries( ["foo", "js"].map((ext) => [ ext, path.resolve(path.join(__dirname, `../cli/non-exists-dir/file.${ext}`)), diff --git a/website/playground/sidebar/SidebarOptions.js b/website/playground/sidebar/SidebarOptions.js index c41c3c484..085a5ecba 100644 --- a/website/playground/sidebar/SidebarOptions.js +++ b/website/playground/sidebar/SidebarOptions.js @@ -1,5 +1,5 @@ import * as React from "react"; -import groupBy from "lodash/groupBy"; +import { groupBy } from "../../../src/cli/utils.js"; import { SidebarCategory } from "./components.js"; import Option from "./options.js";