2023-01-18 20:50:03 +00:00
/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict" ;
const versions = require ( "process" ) . versions ;
const Resolver = require ( "./Resolver" ) ;
const { getType , PathType } = require ( "./util/path" ) ;
const SyncAsyncFileSystemDecorator = require ( "./SyncAsyncFileSystemDecorator" ) ;
const AliasFieldPlugin = require ( "./AliasFieldPlugin" ) ;
const AliasPlugin = require ( "./AliasPlugin" ) ;
const AppendPlugin = require ( "./AppendPlugin" ) ;
const ConditionalPlugin = require ( "./ConditionalPlugin" ) ;
const DescriptionFilePlugin = require ( "./DescriptionFilePlugin" ) ;
const DirectoryExistsPlugin = require ( "./DirectoryExistsPlugin" ) ;
const ExportsFieldPlugin = require ( "./ExportsFieldPlugin" ) ;
const ExtensionAliasPlugin = require ( "./ExtensionAliasPlugin" ) ;
const FileExistsPlugin = require ( "./FileExistsPlugin" ) ;
const ImportsFieldPlugin = require ( "./ImportsFieldPlugin" ) ;
const JoinRequestPartPlugin = require ( "./JoinRequestPartPlugin" ) ;
const JoinRequestPlugin = require ( "./JoinRequestPlugin" ) ;
const MainFieldPlugin = require ( "./MainFieldPlugin" ) ;
const ModulesInHierarchicalDirectoriesPlugin = require ( "./ModulesInHierarchicalDirectoriesPlugin" ) ;
const ModulesInRootPlugin = require ( "./ModulesInRootPlugin" ) ;
const NextPlugin = require ( "./NextPlugin" ) ;
const ParsePlugin = require ( "./ParsePlugin" ) ;
const PnpPlugin = require ( "./PnpPlugin" ) ;
const RestrictionsPlugin = require ( "./RestrictionsPlugin" ) ;
const ResultPlugin = require ( "./ResultPlugin" ) ;
const RootsPlugin = require ( "./RootsPlugin" ) ;
const SelfReferencePlugin = require ( "./SelfReferencePlugin" ) ;
const SymlinkPlugin = require ( "./SymlinkPlugin" ) ;
const TryNextPlugin = require ( "./TryNextPlugin" ) ;
const UnsafeCachePlugin = require ( "./UnsafeCachePlugin" ) ;
const UseFilePlugin = require ( "./UseFilePlugin" ) ;
/** @typedef {import("./AliasPlugin").AliasOption} AliasOptionEntry */
/** @typedef {import("./ExtensionAliasPlugin").ExtensionAliasOption} ExtensionAliasOption */
/** @typedef {import("./PnpPlugin").PnpApiImpl} PnpApi */
2024-08-26 17:13:37 +00:00
/** @typedef {import("./Resolver").EnsuredHooks} EnsuredHooks */
2023-01-18 20:50:03 +00:00
/** @typedef {import("./Resolver").FileSystem} FileSystem */
2024-08-26 17:13:37 +00:00
/** @typedef {import("./Resolver").KnownHooks} KnownHooks */
2023-01-18 20:50:03 +00:00
/** @typedef {import("./Resolver").ResolveRequest} ResolveRequest */
/** @typedef {import("./Resolver").SyncFileSystem} SyncFileSystem */
/** @typedef {string|string[]|false} AliasOptionNewRequest */
/** @typedef {{[k: string]: AliasOptionNewRequest}} AliasOptions */
/** @typedef {{[k: string]: string|string[] }} ExtensionAliasOptions */
2024-08-26 17:13:37 +00:00
/** @typedef {false | 0 | "" | null | undefined} Falsy */
/** @typedef {{apply: function(Resolver): void} | (function(this: Resolver, Resolver): void) | Falsy} Plugin */
2023-01-18 20:50:03 +00:00
/**
* @typedef {Object} UserResolveOptions
* @property {(AliasOptions | AliasOptionEntry[])=} alias A list of module alias configurations or an object which maps key to value
* @property {(AliasOptions | AliasOptionEntry[])=} fallback A list of module alias configurations or an object which maps key to value, applied only after modules option
* @property {ExtensionAliasOptions=} extensionAlias An object which maps extension to extension aliases
* @property {(string | string[])[]=} aliasFields A list of alias fields in description files
* @property {(function(ResolveRequest): boolean)=} cachePredicate A function which decides whether a request should be cached or not. An object is passed with at least `path` and `request` properties.
* @property {boolean=} cacheWithContext Whether or not the unsafeCache should include request context as part of the cache key.
* @property {string[]=} descriptionFiles A list of description files to read from
* @property {string[]=} conditionNames A list of exports field condition names.
* @property {boolean=} enforceExtension Enforce that a extension from extensions must be used
* @property {(string | string[])[]=} exportsFields A list of exports fields in description files
* @property {(string | string[])[]=} importsFields A list of imports fields in description files
* @property {string[]=} extensions A list of extensions which should be tried for files
* @property {FileSystem} fileSystem The file system which should be used
* @property {(object | boolean)=} unsafeCache Use this cache object to unsafely cache the successful requests
* @property {boolean=} symlinks Resolve symlinks to their symlinked location
* @property {Resolver=} resolver A prepared Resolver to which the plugins are attached
* @property {string[] | string=} modules A list of directories to resolve modules from, can be absolute path or folder name
* @property {(string | string[] | {name: string | string[], forceRelative: boolean})[]=} mainFields A list of main fields in description files
* @property {string[]=} mainFiles A list of main files in directories
* @property {Plugin[]=} plugins A list of additional resolve plugins which should be applied
* @property {PnpApi | null=} pnpApi A PnP API that should be used - null is "never", undefined is "auto"
* @property {string[]=} roots A list of root paths
* @property {boolean=} fullySpecified The request is already fully specified and no extensions or directories are resolved for it
* @property {boolean=} resolveToContext Resolve to a context instead of a file
* @property {(string|RegExp)[]=} restrictions A list of resolve restrictions
* @property {boolean=} useSyncFileSystemCalls Use only the sync constraints of the file system calls
* @property {boolean=} preferRelative Prefer to resolve module requests as relative requests before falling back to modules
* @property {boolean=} preferAbsolute Prefer to resolve server-relative urls as absolute paths before falling back to resolve in roots
*/
/**
* @typedef {Object} ResolveOptions
* @property {AliasOptionEntry[]} alias
* @property {AliasOptionEntry[]} fallback
* @property {Set<string | string[]>} aliasFields
* @property {ExtensionAliasOption[]} extensionAlias
* @property {(function(ResolveRequest): boolean)} cachePredicate
* @property {boolean} cacheWithContext
* @property {Set<string>} conditionNames A list of exports field condition names.
* @property {string[]} descriptionFiles
* @property {boolean} enforceExtension
* @property {Set<string | string[]>} exportsFields
* @property {Set<string | string[]>} importsFields
* @property {Set<string>} extensions
* @property {FileSystem} fileSystem
* @property {object | false} unsafeCache
* @property {boolean} symlinks
* @property {Resolver=} resolver
* @property {Array<string | string[]>} modules
* @property {{name: string[], forceRelative: boolean}[]} mainFields
* @property {Set<string>} mainFiles
* @property {Plugin[]} plugins
* @property {PnpApi | null} pnpApi
* @property {Set<string>} roots
* @property {boolean} fullySpecified
* @property {boolean} resolveToContext
* @property {Set<string|RegExp>} restrictions
* @property {boolean} preferRelative
* @property {boolean} preferAbsolute
*/
/**
* @param {PnpApi | null=} option option
* @returns {PnpApi | null} processed option
*/
function processPnpApiOption ( option ) {
if (
option === undefined &&
/** @type {NodeJS.ProcessVersions & {pnp: string}} */ versions . pnp
) {
2024-08-26 17:13:37 +00:00
const _findPnpApi =
/** @type {function(string): PnpApi | null}} */
(
// @ts-ignore
require ( "module" ) . findPnpApi
) ;
if ( _findPnpApi ) {
return {
resolveToUnqualified ( request , issuer , opts ) {
const pnpapi = _findPnpApi ( issuer ) ;
if ( ! pnpapi ) {
// Issuer isn't managed by PnP
return null ;
}
return pnpapi . resolveToUnqualified ( request , issuer , opts ) ;
}
} ;
}
2023-01-18 20:50:03 +00:00
}
return option || null ;
}
/**
* @param {AliasOptions | AliasOptionEntry[] | undefined} alias alias
* @returns {AliasOptionEntry[]} normalized aliases
*/
function normalizeAlias ( alias ) {
return typeof alias === "object" && ! Array . isArray ( alias ) && alias !== null
? Object . keys ( alias ) . map ( key => {
/** @type {AliasOptionEntry} */
const obj = { name : key , onlyModule : false , alias : alias [ key ] } ;
if ( /\$$/ . test ( key ) ) {
obj . onlyModule = true ;
2024-08-26 17:13:37 +00:00
obj . name = key . slice ( 0 , - 1 ) ;
2023-01-18 20:50:03 +00:00
}
return obj ;
} )
: /** @type {Array<AliasOptionEntry>} */ ( alias ) || [ ] ;
}
/**
* @param {UserResolveOptions} options input options
* @returns {ResolveOptions} output options
*/
function createOptions ( options ) {
const mainFieldsSet = new Set ( options . mainFields || [ "main" ] ) ;
2024-08-26 17:13:37 +00:00
/** @type {ResolveOptions["mainFields"]} */
2023-01-18 20:50:03 +00:00
const mainFields = [ ] ;
for ( const item of mainFieldsSet ) {
if ( typeof item === "string" ) {
mainFields . push ( {
name : [ item ] ,
forceRelative : true
} ) ;
} else if ( Array . isArray ( item ) ) {
mainFields . push ( {
name : item ,
forceRelative : true
} ) ;
} else {
mainFields . push ( {
name : Array . isArray ( item . name ) ? item . name : [ item . name ] ,
forceRelative : item . forceRelative
} ) ;
}
}
return {
alias : normalizeAlias ( options . alias ) ,
fallback : normalizeAlias ( options . fallback ) ,
aliasFields : new Set ( options . aliasFields ) ,
cachePredicate :
options . cachePredicate ||
function ( ) {
return true ;
} ,
cacheWithContext :
typeof options . cacheWithContext !== "undefined"
? options . cacheWithContext
: true ,
exportsFields : new Set ( options . exportsFields || [ "exports" ] ) ,
importsFields : new Set ( options . importsFields || [ "imports" ] ) ,
conditionNames : new Set ( options . conditionNames ) ,
descriptionFiles : Array . from (
new Set ( options . descriptionFiles || [ "package.json" ] )
) ,
enforceExtension :
options . enforceExtension === undefined
? options . extensions && options . extensions . includes ( "" )
? true
: false
: options . enforceExtension ,
extensions : new Set ( options . extensions || [ ".js" , ".json" , ".node" ] ) ,
extensionAlias : options . extensionAlias
? Object . keys ( options . extensionAlias ) . map ( k => ( {
extension : k ,
alias : /** @type {ExtensionAliasOptions} */ ( options . extensionAlias ) [
k
]
} ) )
: [ ] ,
fileSystem : options . useSyncFileSystemCalls
? new SyncAsyncFileSystemDecorator (
/** @type {SyncFileSystem} */ (
/** @type {unknown} */ ( options . fileSystem )
)
)
: options . fileSystem ,
unsafeCache :
options . unsafeCache && typeof options . unsafeCache !== "object"
? { }
: options . unsafeCache || false ,
symlinks : typeof options . symlinks !== "undefined" ? options . symlinks : true ,
resolver : options . resolver ,
modules : mergeFilteredToArray (
Array . isArray ( options . modules )
? options . modules
: options . modules
? [ options . modules ]
: [ "node_modules" ] ,
item => {
const type = getType ( item ) ;
return type === PathType . Normal || type === PathType . Relative ;
}
) ,
mainFields ,
mainFiles : new Set ( options . mainFiles || [ "index" ] ) ,
plugins : options . plugins || [ ] ,
pnpApi : processPnpApiOption ( options . pnpApi ) ,
roots : new Set ( options . roots || undefined ) ,
fullySpecified : options . fullySpecified || false ,
resolveToContext : options . resolveToContext || false ,
preferRelative : options . preferRelative || false ,
preferAbsolute : options . preferAbsolute || false ,
restrictions : new Set ( options . restrictions )
} ;
}
/**
* @param {UserResolveOptions} options resolve options
* @returns {Resolver} created resolver
*/
exports . createResolver = function ( options ) {
const normalizedOptions = createOptions ( options ) ;
const {
alias ,
fallback ,
aliasFields ,
cachePredicate ,
cacheWithContext ,
conditionNames ,
descriptionFiles ,
enforceExtension ,
exportsFields ,
extensionAlias ,
importsFields ,
extensions ,
fileSystem ,
fullySpecified ,
mainFields ,
mainFiles ,
modules ,
plugins : userPlugins ,
pnpApi ,
resolveToContext ,
preferRelative ,
preferAbsolute ,
symlinks ,
unsafeCache ,
resolver : customResolver ,
restrictions ,
roots
} = normalizedOptions ;
const plugins = userPlugins . slice ( ) ;
const resolver = customResolver
? customResolver
: new Resolver ( fileSystem , normalizedOptions ) ;
//// pipeline ////
resolver . ensureHook ( "resolve" ) ;
resolver . ensureHook ( "internalResolve" ) ;
resolver . ensureHook ( "newInternalResolve" ) ;
resolver . ensureHook ( "parsedResolve" ) ;
resolver . ensureHook ( "describedResolve" ) ;
resolver . ensureHook ( "rawResolve" ) ;
resolver . ensureHook ( "normalResolve" ) ;
resolver . ensureHook ( "internal" ) ;
resolver . ensureHook ( "rawModule" ) ;
2024-08-26 17:13:37 +00:00
resolver . ensureHook ( "alternateRawModule" ) ;
2023-01-18 20:50:03 +00:00
resolver . ensureHook ( "module" ) ;
resolver . ensureHook ( "resolveAsModule" ) ;
resolver . ensureHook ( "undescribedResolveInPackage" ) ;
resolver . ensureHook ( "resolveInPackage" ) ;
resolver . ensureHook ( "resolveInExistingDirectory" ) ;
resolver . ensureHook ( "relative" ) ;
resolver . ensureHook ( "describedRelative" ) ;
resolver . ensureHook ( "directory" ) ;
resolver . ensureHook ( "undescribedExistingDirectory" ) ;
resolver . ensureHook ( "existingDirectory" ) ;
resolver . ensureHook ( "undescribedRawFile" ) ;
resolver . ensureHook ( "rawFile" ) ;
resolver . ensureHook ( "file" ) ;
resolver . ensureHook ( "finalFile" ) ;
resolver . ensureHook ( "existingFile" ) ;
resolver . ensureHook ( "resolved" ) ;
// TODO remove in next major
// cspell:word Interal
// Backward-compat
2024-08-26 17:13:37 +00:00
// @ts-ignore
2023-01-18 20:50:03 +00:00
resolver . hooks . newInteralResolve = resolver . hooks . newInternalResolve ;
// resolve
for ( const { source , resolveOptions } of [
{ source : "resolve" , resolveOptions : { fullySpecified } } ,
{ source : "internal-resolve" , resolveOptions : { fullySpecified : false } }
] ) {
if ( unsafeCache ) {
plugins . push (
new UnsafeCachePlugin (
source ,
cachePredicate ,
2024-08-26 17:13:37 +00:00
/** @type {import("./UnsafeCachePlugin").Cache} */ ( unsafeCache ) ,
2023-01-18 20:50:03 +00:00
cacheWithContext ,
` new- ${ source } `
)
) ;
plugins . push (
new ParsePlugin ( ` new- ${ source } ` , resolveOptions , "parsed-resolve" )
) ;
} else {
plugins . push ( new ParsePlugin ( source , resolveOptions , "parsed-resolve" ) ) ;
}
}
// parsed-resolve
plugins . push (
new DescriptionFilePlugin (
"parsed-resolve" ,
descriptionFiles ,
false ,
"described-resolve"
)
) ;
plugins . push ( new NextPlugin ( "after-parsed-resolve" , "described-resolve" ) ) ;
// described-resolve
plugins . push ( new NextPlugin ( "described-resolve" , "raw-resolve" ) ) ;
if ( fallback . length > 0 ) {
plugins . push (
new AliasPlugin ( "described-resolve" , fallback , "internal-resolve" )
) ;
}
// raw-resolve
if ( alias . length > 0 ) {
plugins . push ( new AliasPlugin ( "raw-resolve" , alias , "internal-resolve" ) ) ;
}
aliasFields . forEach ( item => {
plugins . push ( new AliasFieldPlugin ( "raw-resolve" , item , "internal-resolve" ) ) ;
} ) ;
extensionAlias . forEach ( item =>
plugins . push (
new ExtensionAliasPlugin ( "raw-resolve" , item , "normal-resolve" )
)
) ;
plugins . push ( new NextPlugin ( "raw-resolve" , "normal-resolve" ) ) ;
// normal-resolve
if ( preferRelative ) {
plugins . push ( new JoinRequestPlugin ( "after-normal-resolve" , "relative" ) ) ;
}
plugins . push (
new ConditionalPlugin (
"after-normal-resolve" ,
{ module : true } ,
"resolve as module" ,
false ,
"raw-module"
)
) ;
plugins . push (
new ConditionalPlugin (
"after-normal-resolve" ,
{ internal : true } ,
"resolve as internal import" ,
false ,
"internal"
)
) ;
if ( preferAbsolute ) {
plugins . push ( new JoinRequestPlugin ( "after-normal-resolve" , "relative" ) ) ;
}
if ( roots . size > 0 ) {
plugins . push ( new RootsPlugin ( "after-normal-resolve" , roots , "relative" ) ) ;
}
if ( ! preferRelative && ! preferAbsolute ) {
plugins . push ( new JoinRequestPlugin ( "after-normal-resolve" , "relative" ) ) ;
}
// internal
importsFields . forEach ( importsField => {
plugins . push (
new ImportsFieldPlugin (
"internal" ,
conditionNames ,
importsField ,
"relative" ,
"internal-resolve"
)
) ;
} ) ;
// raw-module
exportsFields . forEach ( exportsField => {
plugins . push (
new SelfReferencePlugin ( "raw-module" , exportsField , "resolve-as-module" )
) ;
} ) ;
modules . forEach ( item => {
if ( Array . isArray ( item ) ) {
if ( item . includes ( "node_modules" ) && pnpApi ) {
plugins . push (
new ModulesInHierarchicalDirectoriesPlugin (
"raw-module" ,
item . filter ( i => i !== "node_modules" ) ,
"module"
)
) ;
plugins . push (
2024-08-26 17:13:37 +00:00
new PnpPlugin (
"raw-module" ,
pnpApi ,
"undescribed-resolve-in-package" ,
"alternate-raw-module"
)
) ;
plugins . push (
new ModulesInHierarchicalDirectoriesPlugin (
"alternate-raw-module" ,
[ "node_modules" ] ,
"module"
)
2023-01-18 20:50:03 +00:00
) ;
} else {
plugins . push (
new ModulesInHierarchicalDirectoriesPlugin (
"raw-module" ,
item ,
"module"
)
) ;
}
} else {
plugins . push ( new ModulesInRootPlugin ( "raw-module" , item , "module" ) ) ;
}
} ) ;
// module
plugins . push ( new JoinRequestPartPlugin ( "module" , "resolve-as-module" ) ) ;
// resolve-as-module
if ( ! resolveToContext ) {
plugins . push (
new ConditionalPlugin (
"resolve-as-module" ,
{ directory : false , request : "." } ,
"single file module" ,
true ,
"undescribed-raw-file"
)
) ;
}
plugins . push (
new DirectoryExistsPlugin (
"resolve-as-module" ,
"undescribed-resolve-in-package"
)
) ;
// undescribed-resolve-in-package
plugins . push (
new DescriptionFilePlugin (
"undescribed-resolve-in-package" ,
descriptionFiles ,
false ,
"resolve-in-package"
)
) ;
plugins . push (
new NextPlugin ( "after-undescribed-resolve-in-package" , "resolve-in-package" )
) ;
// resolve-in-package
exportsFields . forEach ( exportsField => {
plugins . push (
new ExportsFieldPlugin (
"resolve-in-package" ,
conditionNames ,
exportsField ,
"relative"
)
) ;
} ) ;
plugins . push (
new NextPlugin ( "resolve-in-package" , "resolve-in-existing-directory" )
) ;
// resolve-in-existing-directory
plugins . push (
new JoinRequestPlugin ( "resolve-in-existing-directory" , "relative" )
) ;
// relative
plugins . push (
new DescriptionFilePlugin (
"relative" ,
descriptionFiles ,
true ,
"described-relative"
)
) ;
plugins . push ( new NextPlugin ( "after-relative" , "described-relative" ) ) ;
// described-relative
if ( resolveToContext ) {
plugins . push ( new NextPlugin ( "described-relative" , "directory" ) ) ;
} else {
plugins . push (
new ConditionalPlugin (
"described-relative" ,
{ directory : false } ,
null ,
true ,
"raw-file"
)
) ;
plugins . push (
new ConditionalPlugin (
"described-relative" ,
{ fullySpecified : false } ,
"as directory" ,
true ,
"directory"
)
) ;
}
// directory
plugins . push (
new DirectoryExistsPlugin ( "directory" , "undescribed-existing-directory" )
) ;
if ( resolveToContext ) {
// undescribed-existing-directory
plugins . push ( new NextPlugin ( "undescribed-existing-directory" , "resolved" ) ) ;
} else {
// undescribed-existing-directory
plugins . push (
new DescriptionFilePlugin (
"undescribed-existing-directory" ,
descriptionFiles ,
false ,
"existing-directory"
)
) ;
mainFiles . forEach ( item => {
plugins . push (
new UseFilePlugin (
"undescribed-existing-directory" ,
item ,
"undescribed-raw-file"
)
) ;
} ) ;
// described-existing-directory
mainFields . forEach ( item => {
plugins . push (
new MainFieldPlugin (
"existing-directory" ,
item ,
"resolve-in-existing-directory"
)
) ;
} ) ;
mainFiles . forEach ( item => {
plugins . push (
new UseFilePlugin ( "existing-directory" , item , "undescribed-raw-file" )
) ;
} ) ;
// undescribed-raw-file
plugins . push (
new DescriptionFilePlugin (
"undescribed-raw-file" ,
descriptionFiles ,
true ,
"raw-file"
)
) ;
plugins . push ( new NextPlugin ( "after-undescribed-raw-file" , "raw-file" ) ) ;
// raw-file
plugins . push (
new ConditionalPlugin (
"raw-file" ,
{ fullySpecified : true } ,
null ,
false ,
"file"
)
) ;
if ( ! enforceExtension ) {
plugins . push ( new TryNextPlugin ( "raw-file" , "no extension" , "file" ) ) ;
}
extensions . forEach ( item => {
plugins . push ( new AppendPlugin ( "raw-file" , item , "file" ) ) ;
} ) ;
// file
if ( alias . length > 0 )
plugins . push ( new AliasPlugin ( "file" , alias , "internal-resolve" ) ) ;
aliasFields . forEach ( item => {
plugins . push ( new AliasFieldPlugin ( "file" , item , "internal-resolve" ) ) ;
} ) ;
plugins . push ( new NextPlugin ( "file" , "final-file" ) ) ;
// final-file
plugins . push ( new FileExistsPlugin ( "final-file" , "existing-file" ) ) ;
// existing-file
if ( symlinks )
plugins . push ( new SymlinkPlugin ( "existing-file" , "existing-file" ) ) ;
plugins . push ( new NextPlugin ( "existing-file" , "resolved" ) ) ;
}
2024-08-26 17:13:37 +00:00
const resolved =
/** @type {KnownHooks & EnsuredHooks} */
( resolver . hooks ) . resolved ;
2023-01-18 20:50:03 +00:00
// resolved
if ( restrictions . size > 0 ) {
2024-08-26 17:13:37 +00:00
plugins . push ( new RestrictionsPlugin ( resolved , restrictions ) ) ;
2023-01-18 20:50:03 +00:00
}
2024-08-26 17:13:37 +00:00
plugins . push ( new ResultPlugin ( resolved ) ) ;
2023-01-18 20:50:03 +00:00
//// RESOLVER ////
for ( const plugin of plugins ) {
if ( typeof plugin === "function" ) {
2024-08-26 17:13:37 +00:00
/** @type {function(this: Resolver, Resolver): void} */
( plugin ) . call ( resolver , resolver ) ;
} else if ( plugin ) {
2023-01-18 20:50:03 +00:00
plugin . apply ( resolver ) ;
}
}
return resolver ;
} ;
/**
* Merging filtered elements
* @param {string[]} array source array
* @param {function(string): boolean} filter predicate
* @returns {Array<string | string[]>} merge result
*/
function mergeFilteredToArray ( array , filter ) {
/** @type {Array<string | string[]>} */
const result = [ ] ;
const set = new Set ( array ) ;
for ( const item of set ) {
if ( filter ( item ) ) {
const lastElement =
result . length > 0 ? result [ result . length - 1 ] : undefined ;
if ( Array . isArray ( lastElement ) ) {
lastElement . push ( item ) ;
} else {
result . push ( [ item ] ) ;
}
} else {
result . push ( item ) ;
}
}
return result ;
}