webpack-dependency-suite
Version:
A set of Webpack plugins, loaders and utilities designed for advanced dependency resolution
189 lines (161 loc) • 6.97 kB
text/typescript
import * as path from 'path'
import * as fs from 'fs'
import * as cheerio from 'cheerio'
import {memoize, MapCache} from 'lodash'
import { AddLoadersOptions, AddLoadersMethod, RequireData, RequireDataBase, PathWithLoaders, SelectorAndAttribute } from '../typings/definitions'
import {
appendCodeAndCallback,
expandAllRequiresForGlob,
getRequireStrings,
splitRequest,
wrapInRequireInclude
} from './inject';
import {get} from 'lodash'
import * as debug from 'debug'
const log = debug('utils')
const invalidationDebounceDirectory = new WeakMap<any, Map<string, NodeJS.Timer>>()
export function cacheInvalidationDebounce(cacheKey: string, cache: MapCache, dictionaryKey: any, debounceMs = 10000) {
let invalidationDebounce = invalidationDebounceDirectory.get(dictionaryKey)
if (!invalidationDebounce) {
invalidationDebounce = new Map<string, NodeJS.Timer>()
invalidationDebounceDirectory.set(dictionaryKey, invalidationDebounce)
}
const previousTimeout = invalidationDebounce.get(cacheKey)
invalidationDebounce.delete(cacheKey)
if (previousTimeout) clearTimeout(previousTimeout)
const timeout = setTimeout(() => cache.delete(cacheKey), debounceMs)
timeout.unref() // do not require the Node.js event loop to remain active
invalidationDebounce.set(cacheKey, timeout)
}
export const getFilesInDir = memoize(getFilesInDirBase, (directory: string, {
skipHidden = true, recursive = false, regexFilter = undefined, emitWarning = console.warn.bind(console), emitError = console.error.bind(console), fileSystem = fs, regexIgnore = [/node_modules/], returnRelativeTo = directory
}: GetFilesInDirOptions = {}) => {
/** valid for 10 seconds before invalidating cache **/
const cacheKey = `${directory}::${skipHidden}::${recursive}::${regexFilter}::${regexIgnore.join('::')}`
cacheInvalidationDebounce(cacheKey, getFilesInDir.cache, fileSystem)
return cacheKey
})
export interface GetFilesInDirOptions {
skipHidden?: boolean
recursive?: boolean
regexFilter?: RegExp
emitWarning?: (warn: string) => void
emitError?: (warn: string) => void
fileSystem?: { readdir: Function, stat: Function }
regexIgnore?: Array<RegExp>
/**
* If set to a path, additionally returns the part of the path
* starting from the directory base without the leading './'
*/
returnRelativeTo?: string
ignoreIfNotExists?: boolean
}
export async function getFilesInDirBase(directory: string, {
skipHidden = true, recursive = false, regexFilter = undefined, emitWarning = console.warn.bind(console), emitError = console.error.bind(console), fileSystem = fs, regexIgnore = [/node_modules/], returnRelativeTo = directory, ignoreIfNotExists = false
}: GetFilesInDirOptions = {}
): Promise<Array<{ filePath: string, stat: fs.Stats, relativePath: string }>> {
if (!directory) {
emitError(`No directory supplied`)
return []
}
const exists = await new Promise<fs.Stats | undefined>((resolve, reject) =>
fileSystem.stat(directory, (err, stat) =>
err ? resolve() :
resolve(stat)
)
)
if (!exists || !exists.isDirectory()) {
if (!ignoreIfNotExists) {
emitError(`The supplied directory does not exist ${directory}`)
}
return []
}
let files = await new Promise<string[]>((resolve, reject) =>
fileSystem.readdir(directory, (err, value) => err ? resolve([]) || emitWarning(`Error when trying to load ${directory}: ${err.message}`) : resolve(value)))
if (regexIgnore && regexIgnore.length) {
files = files
.filter(filePath => !regexIgnore.some(regex => regex.test(filePath)))
}
if (skipHidden) {
files = files
.filter(filePath => path.basename(filePath)[0] !== '.')
}
files = files.map(filePath => path.join(directory, filePath))
let stats = (await Promise.all(
files
.map(filePath => new Promise<{ filePath: string, stat: fs.Stats, relativePath: string }>((resolve, reject) =>
fileSystem.stat(filePath, (err, stat) =>
err ? resolve({filePath, stat, relativePath: ''}) :
resolve({filePath, stat, relativePath: path.relative(returnRelativeTo, filePath)})
)
))
)).filter(stat => !!stat.stat)
if (regexFilter) {
stats = stats
.filter(file =>
!(file.stat.isFile() && !file.filePath.match(regexFilter))
)
}
if (!recursive)
return stats.filter(file => file.stat.isFile())
const subDirectoryStats = await Promise.all(
stats.filter(file => file.stat.isDirectory()).map(
file => getFilesInDir(file.filePath, {
skipHidden, recursive, regexFilter, emitWarning, emitError, fileSystem, regexIgnore, returnRelativeTo
})
)
)
return stats.filter(file => file.stat.isFile()).concat(
...subDirectoryStats
)
}
// export async function concatPromiseResults<T>(values: (Array<T> | PromiseLike<Array<T>>)[]): Promise<T[]> {
export async function concatPromiseResults<T>(values: Array<PromiseLike<Array<T>>>): Promise<Array<T>> {
return ([] as Array<T>).concat(...(await Promise.all<Array<T>>(values)))
}
export interface ResourcesInput {
path: Array<string> | string
lazy?: boolean
bundle?: string
chunk?: string
}
export function getResourcesFromList(json: Object, propertyPath: string) {
const resources = get(json, propertyPath, [] as Array<ResourcesInput | string>)
if (!resources.length) return []
const allResources = [] as Array<RequireDataBase>
resources.forEach(input => {
const r = input instanceof Object && !Array.isArray(input) ? input as ResourcesInput : { path: input }
const paths = Array.isArray(r.path) ? r.path : [r.path]
paths.forEach(
literal => allResources.push({ literal, lazy: r.lazy || false, chunk: r.bundle || r.chunk })
)
})
return allResources
}
/**
* Generates list of dependencies based on the passed in selectors, e.g.:
* - <require from="paths">
* - <template view-model="./file"></template>
* - <template view="file.html"></template>
*/
export function getTemplateResourcesData(html: string, selectorsAndAttributes: Array<SelectorAndAttribute>, globRegex: RegExp | undefined) {
const $ = cheerio.load(html) // { decodeEntities: false }
function extractRequire(context: Cheerio, fromAttribute = 'from') {
const resources: Array<RequireDataBase> = []
context.each(index => {
let path = context[index].attribs[fromAttribute] as string | undefined
if (!path) return
if (globRegex && globRegex.test(path)) {
path = path.replace(globRegex, `*`)
}
const lazy = context[index].attribs.hasOwnProperty('lazy')
const chunk = context[index].attribs['bundle'] || context[index].attribs['chunk']
resources.push({ literal: path, lazy, chunk })
})
return resources
}
const resourcesArray = selectorsAndAttributes
.map(saa => extractRequire($(saa.selector), saa.attribute))
const resources = ([] as RequireDataBase[]).concat(...resourcesArray)
return resources
}