diff --git a/package.json b/package.json index 211d5c8..1012dfa 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,6 @@ "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", - "lodash": "^4.17.21", "semver": "^7.6.3" }, "devDependencies": { @@ -31,7 +30,6 @@ "@eslint/js": "^9.19.0", "@types/debug": "^4.1.7", "@types/estree": "^1.0.0", - "@types/lodash": "^4.14.186", "@types/mocha": "^9.0.0", "@types/node": "^18.8.4", "@types/semver": "^7.3.12", @@ -84,7 +82,7 @@ "watch:tsc": "tsc --module es2015 --watch", "watch:rollup": "wait-on .temp/index.js && rollup -c -o index.js --watch", "watch:test": "wait-on index.js && warun index.js \"test/*.js\" \"test/fixtures/ast/*/*.json\" \"test/fixtures/*\" --debounce 1000 --no-initial -- nyc mocha \"test/*.js\" --reporter dot --timeout 10000", - "watch:update-ast": "wait-on index.js && warun index.js \"test/fixtures/ast/*/*.vue\" -- node scripts/update-fixtures-ast.js", + "watch:update-ast": "wait-on index.js && warun index.js \"test/fixtures/ast/*/*.vue\" -- ts-node scripts/update-fixtures-ast.js", "watch:coverage-report": "wait-on coverage/lcov-report/index.html && opener coverage/lcov-report/index.html" }, "repository": { diff --git a/rollup.config.js b/rollup.config.js index fef6483..ce75d20 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -9,7 +9,7 @@ import replace from "rollup-plugin-replace" const pkg = require("./package.json") const deps = new Set( - ["assert", "events", "path"].concat(Object.keys(pkg.dependencies)) + ["assert", "events", "path"].concat(Object.keys(pkg.dependencies)), ) export default { @@ -31,5 +31,5 @@ export default { "process.env.PACKAGE_VERSION": `"${pkg.version}"`, }), ], - external: id => deps.has(id) || id.startsWith("lodash"), + external: (id) => deps.has(id), } diff --git a/src/common/error-utils.ts b/src/common/error-utils.ts index 378aacb..42f3f14 100644 --- a/src/common/error-utils.ts +++ b/src/common/error-utils.ts @@ -1,5 +1,5 @@ import type { ParseError, VDocumentFragment } from "../ast/index" -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../utils/utils" /** * Insert the given error. * @param document The document that the node is belonging to. diff --git a/src/common/lines-and-columns.ts b/src/common/lines-and-columns.ts index c62d781..b1b5108 100644 --- a/src/common/lines-and-columns.ts +++ b/src/common/lines-and-columns.ts @@ -1,4 +1,4 @@ -import sortedLastIndex from "lodash/sortedLastIndex" +import { sortedLastIndex } from "../utils/utils" import type { Location } from "../ast/index" import type { LocationCalculator } from "./location-calculator" /** diff --git a/src/common/location-calculator.ts b/src/common/location-calculator.ts index 8c8cf34..75126d0 100644 --- a/src/common/location-calculator.ts +++ b/src/common/location-calculator.ts @@ -3,7 +3,7 @@ * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ -import sortedLastIndex from "lodash/sortedLastIndex" +import { sortedLastIndex } from "../utils/utils" import type { Location } from "../ast/index" import { LinesAndColumns } from "./lines-and-columns" diff --git a/src/common/token-utils.ts b/src/common/token-utils.ts index 3013061..21414a8 100644 --- a/src/common/token-utils.ts +++ b/src/common/token-utils.ts @@ -1,5 +1,4 @@ -import sortedIndexBy from "lodash/sortedIndexBy" -import sortedLastIndexBy from "lodash/sortedLastIndexBy" +import { sortedIndexBy, sortedLastIndexBy } from "../utils/utils" import type { LocationRange, Token, VDocumentFragment } from "../ast/index" import type { LinesAndColumns } from "./lines-and-columns" diff --git a/src/external/node-event-generator.ts b/src/external/node-event-generator.ts index b8eb78d..a601f12 100644 --- a/src/external/node-event-generator.ts +++ b/src/external/node-event-generator.ts @@ -4,9 +4,8 @@ import type EventEmitter from "events" import type { ESQueryOptions, Selector } from "esquery" import esquery from "esquery" -import union from "lodash/union" -import intersection from "lodash/intersection" -import memoize from "lodash/memoize" +import { memoize } from "../utils/memoize" +import { union, intersection } from "../utils/utils" import type { Node } from "../ast/index" interface NodeSelector { diff --git a/src/external/token-store/utils.ts b/src/external/token-store/utils.ts index 64cc869..574c934 100644 --- a/src/external/token-store/utils.ts +++ b/src/external/token-store/utils.ts @@ -2,7 +2,7 @@ * @fileoverview Define utilify functions for token store. * @author Toru Nagashima */ -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../../utils/utils" import type { HasLocation } from "../../ast/index" /** diff --git a/src/html/intermediate-tokenizer.ts b/src/html/intermediate-tokenizer.ts index 62d4ca9..fbe5f54 100644 --- a/src/html/intermediate-tokenizer.ts +++ b/src/html/intermediate-tokenizer.ts @@ -4,7 +4,6 @@ * See LICENSE file in root directory for full license. */ import assert from "assert" -import last from "lodash/last" import type { ErrorCode, HasLocation, @@ -175,7 +174,7 @@ export class IntermediateTokenizer { // VExpressionEnd was not found. // Concatenate the deferred tokens to the committed token. const start = this.expressionStartToken - const end = last(this.expressionTokens) || start + const end = this.expressionTokens.at(-1) || start const value = this.expressionTokens.reduce(concat, start.value) this.expressionStartToken = null this.expressionTokens = [] @@ -240,7 +239,7 @@ export class IntermediateTokenizer { if (this.expressionStartToken != null) { // Defer this token until a VExpressionEnd token or a non-text token appear. const lastToken = - last(this.expressionTokens) || this.expressionStartToken + this.expressionTokens.at(-1) || this.expressionStartToken if (lastToken.range[1] === token.range[0]) { this.expressionTokens.push(token) return null @@ -552,7 +551,7 @@ export class IntermediateTokenizer { } const start = this.expressionStartToken - const end = last(this.expressionTokens) || start + const end = this.expressionTokens.at(-1) || start // If it's '{{}}', it's handled as a text. if (token.range[0] === start.range[1]) { diff --git a/src/html/parser.ts b/src/html/parser.ts index ed155e9..908056c 100644 --- a/src/html/parser.ts +++ b/src/html/parser.ts @@ -4,8 +4,6 @@ * See LICENSE file in root directory for full license. */ import assert from "assert" -import last from "lodash/last" -import findLastIndex from "lodash/findLastIndex" import type { ErrorCode, HasLocation, @@ -52,8 +50,7 @@ import { getScriptParser, getParserLangFromSFC, } from "../common/parser-options" -import sortedIndexBy from "lodash/sortedIndexBy" -import sortedLastIndexBy from "lodash/sortedLastIndexBy" +import { sortedIndexBy, sortedLastIndexBy } from "../utils/utils" import type { CustomTemplateTokenizer, CustomTemplateTokenizerConstructor, @@ -160,7 +157,7 @@ function adjustAttributeName(name: string, namespace: Namespace): string { */ function propagateEndLocation(node: VDocumentFragment | VElement): void { const lastChild = - (node.type === "VElement" ? node.endTag : null) || last(node.children) + (node.type === "VElement" ? node.endTag : null) || node.children.at(-1) if (lastChild != null) { node.range[1] = lastChild.range[1] node.loc.end = lastChild.loc.end @@ -236,7 +233,7 @@ export class Parser { * Get the current node. */ private get currentNode(): VDocumentFragment | VElement { - return last(this.elementStack) || this.document + return this.elementStack.at(-1) || this.document } /** @@ -701,8 +698,7 @@ export class Parser { protected EndTag(token: EndTag): void { debug("[html] EndTag %j", token) - const i = findLastIndex( - this.elementStack, + const i = this.elementStack.findLastIndex( (el) => el.name.toLowerCase() === token.name, ) if (i === -1) { diff --git a/src/script/index.ts b/src/script/index.ts index 18f67d1..279095b 100644 --- a/src/script/index.ts +++ b/src/script/index.ts @@ -3,9 +3,7 @@ * @copyright 2017 Toru Nagashima. All rights reserved. * See LICENSE file in root directory for full license. */ -import first from "lodash/first" -import last from "lodash/last" -import sortedIndexBy from "lodash/sortedIndexBy" +import { sortedIndexBy } from "../utils/utils" import type { ESLintArrayExpression, ESLintArrayPattern, @@ -521,7 +519,7 @@ function parseFilter( } } - const token = last(ast.tokens)! + const token = ast.tokens!.at(-1)! return throwUnexpectedTokenError(token.value, token) } @@ -536,7 +534,7 @@ function parseFilter( // Update range. const firstToken = tokens[0] - const lastToken = last(tokens)! + const lastToken = tokens.at(-1)! expression.range = [firstToken.range[0], lastToken.range[1]] expression.loc = { start: firstToken.loc.start, end: lastToken.loc.end } @@ -778,7 +776,7 @@ export function parseExpression( } // Update range. - const lastToken = last(ret.tokens)! + const lastToken = ret.tokens.at(-1)! ret.expression.range[1] = lastToken.range[1] ret.expression.loc.end = lastToken.loc.end @@ -933,7 +931,7 @@ function parseVForExpressionForEcmaVersion5( if (open != null) { open.value = "(" } - const close = last(parsedAliases.tokens) + const close = parsedAliases.tokens.at(-1) if (close != null) { close.value = ")" } @@ -977,7 +975,7 @@ function parseVForExpressionForEcmaVersion5( comments.push(...parsedIterator.comments) const { right, references } = parsedIterator const firstToken = tokens[0] - const lastToken = last(tokens) || firstToken + const lastToken = tokens.at(-1) || firstToken const expression: VForExpression = { type: "VForExpression", range: [firstToken.range[0], lastToken.range[1]], @@ -1136,8 +1134,8 @@ function parseVOnExpressionBody( ).argument as ESLintFunctionExpression const block = functionDecl.body const body = block.body - const firstStatement = first(body) - const lastStatement = last(body) + const firstStatement = body[0] + const lastStatement = body.at(-1) const expression: VOnExpression = { type: "VOnExpression", range: [ @@ -1231,8 +1229,8 @@ export function parseSlotScopeExpression( ) const references = scope.references const variables = scope.variables - const firstParam = first(params)! - const lastParam = last(params)! + const firstParam = params[0] + const lastParam = params.at(-1)! const expression: VSlotScopeExpression = { type: "VSlotScopeExpression", range: [firstParam.range[0], lastParam.range[1]], @@ -1330,8 +1328,8 @@ export function parseGenericExpression( ) const references = scope.references const variables = scope.variables - const firstParam = first(params)! - const lastParam = last(params)! + const firstParam = params[0] + const lastParam = params.at(-1)! const expression: VGenericExpression = { type: "VGenericExpression", range: [firstParam.range[0], lastParam.range[1]], diff --git a/src/utils/memoize.ts b/src/utils/memoize.ts new file mode 100644 index 0000000..1dda47a --- /dev/null +++ b/src/utils/memoize.ts @@ -0,0 +1,157 @@ +/** + * Creates a memoized version of the provided function. The memoized function caches + * results based on the argument it receives, so if the same argument is passed again, + * it returns the cached result instead of recomputing it. + * + * This function works with functions that take zero or just one argument. If your function + * originally takes multiple arguments, you should refactor it to take a single object or array + * that combines those arguments. + * + * If the argument is not primitive (e.g., arrays or objects), provide a + * `getCacheKey` function to generate a unique cache key for proper caching. + * + * @template F - The type of the function to be memoized. + * @param {F} fn - The function to be memoized. It should accept a single argument and return a value. + * @param {MemoizeOptions[0], ReturnType>} [options={}] - Optional configuration for the memoization. + * @param {MemoizeCache} [options.cache] - The cache object used to store results. Defaults to a new `Map`. + * @param {(args: A) => unknown} [options.getCacheKey] - An optional function to generate a unique cache key for each argument. + * + * @returns The memoized function with an additional `cache` property that exposes the internal cache. + * + * @example + * // Example using the default cache + * const add = (x: number) => x + 10; + * const memoizedAdd = memoize(add); + * + * console.log(memoizedAdd(5)); // 15 + * console.log(memoizedAdd(5)); // 15 (cached result) + * console.log(memoizedAdd.cache.size); // 1 + * + * @example + * // Example using a custom resolver + * const sum = (arr: number[]) => arr.reduce((x, y) => x + y, 0); + * const memoizedSum = memoize(sum, { getCacheKey: (arr: number[]) => arr.join(',') }); + * console.log(memoizedSum([1, 2])); // 3 + * console.log(memoizedSum([1, 2])); // 3 (cached result) + * console.log(memoizedSum.cache.size); // 1 + * + * @example + * // Example using a custom cache implementation + * class CustomCache implements MemoizeCache { + * private cache = new Map(); + * + * set(key: K, value: T): void { + * this.cache.set(key, value); + * } + * + * get(key: K): T | undefined { + * return this.cache.get(key); + * } + * + * has(key: K): boolean { + * return this.cache.has(key); + * } + * + * delete(key: K): boolean { + * return this.cache.delete(key); + * } + * + * clear(): void { + * this.cache.clear(); + * } + * + * get size(): number { + * return this.cache.size; + * } + * } + * const customCache = new CustomCache(); + * const memoizedSumWithCustomCache = memoize(sum, { cache: customCache }); + * console.log(memoizedSumWithCustomCache([1, 2])); // 3 + * console.log(memoizedSumWithCustomCache([1, 2])); // 3 (cached result) + * console.log(memoizedAddWithCustomCache.cache.size); // 1 + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/function/memoize.ts + */ +export function memoize any>( + fn: F, + options: { + cache?: MemoizeCache> + getCacheKey?: (args: Parameters[0]) => unknown + } = {}, +): F & { cache: MemoizeCache> } { + const { cache = new Map>(), getCacheKey } = options + + const memoizedFn = function ( + this: unknown, + arg: Parameters[0], + ): ReturnType { + const key = getCacheKey ? getCacheKey(arg) : arg + + if (cache.has(key)) { + return cache.get(key)! + } + + const result = fn.call(this, arg) + + cache.set(key, result) + + return result + } + + memoizedFn.cache = cache + + return memoizedFn as F & { cache: MemoizeCache> } +} + +/** + * Represents a cache for memoization, allowing storage and retrieval of computed values. + * + * @template K - The type of keys used to store values in the cache. + * @template V - The type of values stored in the cache. + */ +interface MemoizeCache { + /** + * Stores a value in the cache with the specified key. + * + * @param key - The key to associate with the value. + * @param value - The value to store in the cache. + */ + set(key: K, value: V): void + + /** + * Retrieves a value from the cache by its key. + * + * @param key - The key of the value to retrieve. + * @returns The value associated with the key, or undefined if the key does not exist. + */ + get(key: K): V | undefined + + /** + * Checks if a value exists in the cache for the specified key. + * + * @param key - The key to check for existence in the cache. + * @returns True if the cache contains the key, false otherwise. + */ + has(key: K): boolean + + /** + * Deletes a value from the cache by its key. + * + * @param key - The key of the value to delete. + * @returns True if the value was successfully deleted, false otherwise. + */ + delete(key: K): boolean | void + + /** + * Clears all values from the cache. + */ + clear(): void + + /** + * The number of entries in the cache. + */ + size: number +} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 1ecc241..87ade94 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -4,3 +4,277 @@ export function camelize(str: string) { return str.replace(/-(\w)/gu, (_, c) => (c ? c.toUpperCase() : "")) } + +/** + * A binary search implementation that finds the index at which `predicate` + * stops returning `true` and starts returning `false` (consistently) when run + * on the items of the array. It **assumes** that mapping the array via the + * predicate results in the shape `[...true[], ...false[]]`. *For any other case + * the result is unpredictable*. + * + * This is the base implementation of the `sortedIndex` functions which define + * the predicate for the user, for common use-cases. + * + * It is similar to `findIndex`, but runs at O(logN), whereas the latter is + * general purpose function which runs on any array and predicate, but runs at + * O(N) time. + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/internal/binarySearchCutoffIndex.ts#L15 + */ +function binarySearchCutoffIndex( + array: readonly T[], + predicate: (value: T, index: number, data: readonly T[]) => boolean, +): number { + let lowIndex = 0 + let highIndex = array.length + + while (lowIndex < highIndex) { + const pivotIndex = (lowIndex + highIndex) >>> 1 + const pivot = array[pivotIndex] + + if (predicate(pivot, pivotIndex, array)) { + lowIndex = pivotIndex + 1 + } else { + highIndex = pivotIndex + } + } + + return highIndex +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order; so that `splice(sortedIndex, 0, item)` would result in + * maintaining the array's sort-ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *last* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * @param item - The item to insert. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedLastIndex(item)(data) + * @example + * R.pipe(['a','a','b','c','c'], sortedLastIndex('c')) // => 5 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedLastIndex.ts#L51 + */ +export function sortedLastIndex(array: readonly T[], item: T): number { + return binarySearchCutoffIndex(array, (pivot) => pivot <= item) +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order using a value function; so that + * `splice(sortedIndex, 0, item)` would result in maintaining the arrays sort- + * ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *first* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * See also: + * * `findIndex` - scans a possibly unsorted array in-order (linear search). + * * `sortedIndex` - like this function, but doesn't take a callbackfn. + * * `sortedLastIndexBy` - like this function, but finds the last suitable index. + * * `sortedLastIndex` - like `sortedIndex`, but finds the last suitable index. + * * `rankBy` - scans a possibly unsorted array in-order, returning the index based on a sorting criteria. + * + * @param data - The (ascending) sorted array. + * @param item - The item to insert. + * @param valueFunction - All comparisons would be performed on the result of + * calling this function on each compared item. Preferably this function should + * return a `number` or `string`. This function should be the same as the one + * provided to sortBy to sort the array. The function is called exactly once on + * each items that is compared against in the array, and once at the beginning + * on `item`. When called on `item` the `index` argument is `undefined`. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedIndexBy(data, item, valueFunction) + * @example + * R.sortedIndexBy([{age:20},{age:22}],{age:21},prop('age')) // => 1 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedIndexBy.ts#L37 + */ +export function sortedIndexBy( + array: readonly T[], + item: T, + valueFunction: ( + item: T, + index: number | undefined, + data: readonly T[], + ) => number, +): number { + const value = valueFunction(item, undefined, array) + + return binarySearchCutoffIndex( + array, + (pivot, index) => valueFunction(pivot, index, array) < value, + ) +} + +/** + * Find the insertion position (index) of an item in an array with items sorted + * in ascending order using a value function; so that + * `splice(sortedIndex, 0, item)` would result in maintaining the arrays sort- + * ness. The array can contain duplicates. + * If the item already exists in the array the index would be of the *last* + * occurrence of the item. + * + * Runs in O(logN) time. + * + * See also: + * * `findIndex` - scans a possibly unsorted array in-order (linear search). + * * `sortedLastIndex` - a simplified version of this function, without a callbackfn. + * * `sortedIndexBy` - like this function, but returns the first suitable index. + * * `sortedIndex` - like `sortedLastIndex` but without a callbackfn. + * * `rankBy` - scans a possibly unsorted array in-order, returning the index based on a sorting criteria. + * + * @param data - The (ascending) sorted array. + * @param item - The item to insert. + * @param valueFunction - All comparisons would be performed on the result of + * calling this function on each compared item. Preferably this function should + * return a `number` or `string`. This function should be the same as the one + * provided to sortBy to sort the array. The function is called exactly once on + * each items that is compared against in the array, and once at the beginning + * on `item`. When called on `item` the `index` argument is `undefined`. + * @returns Insertion index (In the range 0..data.length). + * @signature + * R.sortedLastIndexBy(data, item, valueFunction) + * @example + * R.sortedLastIndexBy([{age:20},{age:22}],{age:21},prop('age')) // => 1 + * + * MIT License | Copyright (c) 2018 remeda | https://remedajs.com/ + * + * The implementation is copied from remeda package: + * https://github.com/remeda/remeda/blob/df5fe74841c07bc356bbaa2c89bc7ba0cafafd0a/packages/remeda/src/sortedLastIndexBy.ts#L37 + */ +export function sortedLastIndexBy( + array: readonly T[], + item: T, + valueFunction: ( + item: T, + index: number | undefined, + data: readonly T[], + ) => number, +): number { + const value = valueFunction(item, undefined, array) + + return binarySearchCutoffIndex( + array, + (pivot, index) => valueFunction(pivot, index, array) <= value, + ) +} + +/** + * Creates a duplicate-free version of an array. + * + * This function takes an array and returns a new array containing only the unique values + * from the original array, preserving the order of first occurrence. + * + * @template T - The type of elements in the array. + * @param {T[]} arr - The array to process. + * @returns {T[]} A new array with only unique values from the original array. + * + * @example + * const array = [1, 2, 2, 3, 4, 4, 5]; + * const result = uniq(array); + * // result will be [1, 2, 3, 4, 5] + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/array/uniq.ts#L16 + */ +export function uniq(arr: readonly T[]): T[] { + return Array.from(new Set(arr)) +} + +/** + * Returns the intersection of multiple arrays. + * + * This function takes multiple arrays and returns a new array containing the elements that are + * present in all provided arrays. It effectively filters out any elements that are not found + * in every array. + * + * @template T - The type of elements in the arrays. + * @param {...(ArrayLike | null | undefined)} arrays - The arrays to compare. + * @returns {T[]} A new array containing the elements that are present in all arrays. + * + * @example + * const array1 = [1, 2, 3, 4, 5]; + * const array2 = [3, 4, 5, 6, 7]; + * const result = intersection(array1, array2); + * // result will be [3, 4, 5] since these elements are in both arrays. + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/intersection.ts#L22 + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/array/intersection.ts#L19 + */ +export function intersection(...arrays: (T[] | null | undefined)[]): T[] { + if (arrays.length === 0) { + return [] + } + + let result: T[] = uniq(arrays[0]!) + + for (let i = 1; i < arrays.length; i++) { + const array = arrays[i] + const secondSet = new Set(array) + + result = result.filter((item) => secondSet.has(item)) + } + + return result +} + +/** + * This function takes multiple arrays and returns a new array containing only the unique values + * from all input arrays, preserving the order of their first occurrence. + * + * @template T - The type of elements in the arrays. + * @param {Array | null | undefined>} arrays - The arrays to inspect. + * @returns {T[]} Returns the new array of combined unique values. + * + * @example + * // Returns [2, 1] + * union([2], [1, 2]); + * + * @example + * // Returns [2, 1, 3] + * union([2], [1, 2], [2, 3]); + * + * @example + * // Returns [1, 3, 2, [5], [4]] (does not deeply flatten nested arrays) + * union([1, 3, 2], [1, [5]], [2, [4]]); + * + * @example + * // Returns [0, 2, 1] (ignores non-array values like 3 and { '0': 1 }) + * union([0], 3, { '0': 1 }, null, [2, 1]); + * @example + * // Returns [0, 'a', 2, 1] (treats array-like object { 0: 'a', length: 1 } as a valid array) + * union([0], { 0: 'a', length: 1 }, [2, 1]); + * + * MIT © Viva Republica, Inc. | https://es-toolkit.dev/ + * + * The implementation is copied from es-toolkit package: + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/union.ts#L61 + * https://github.com/toss/es-toolkit/blob/16709839f131269b84cdd96e9645df52648ccedf/src/compat/array/flattenDepth.ts#L21 + */ +export function union(...arrays: T[][]): T[] { + const flattened = arrays.flat() + + return uniq(flattened) +} diff --git a/test/ast.js b/test/ast.js index c033c80..0331042 100644 --- a/test/ast.js +++ b/test/ast.js @@ -12,7 +12,6 @@ const assert = require("assert") const fs = require("fs") const path = require("path") -const lodash = require("lodash") const parser = require("../src") const eslint = require("eslint") const semver = require("semver") @@ -116,7 +115,7 @@ function validateParent(source, parserOptions) { ruleContext.sourceCode.parserServices.defineTemplateBodyVisitor({ "*"(node) { if (stack.length >= 1) { - const parent = lodash.last(stack) + const parent = stack.at(-1) assert( node.parent === parent, `The parent of ${nodeToString( diff --git a/tsconfig.json b/tsconfig.json index e4cbfcc..c5dc0e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "inlineSources": true, - "lib": ["es2015"], + "lib": ["es2023"], "module": "commonjs", "moduleResolution": "node", "newLine": "LF",