UNPKG

webpack-dependency-suite

Version:

A set of Webpack plugins, loaders and utilities designed for advanced dependency resolution

222 lines (193 loc) 11.1 kB
import { AddLoadersMethod, PathWithLoaders, RequireData, RequireDataBase } from '../typings/definitions' import * as path from 'path' import * as loaderUtils from 'loader-utils' import * as SourceMap from 'source-map' import { getFilesInDir, concatPromiseResults, cacheInvalidationDebounce } from './index' import ModuleDependency = require('webpack/lib/dependencies/ModuleDependency') import escapeStringForRegex = require('escape-string-regexp') import {memoize, uniqBy} from 'lodash' import * as debug from 'debug' const log = debug('utils') export function appendCodeAndCallback(loader: Webpack.Core.LoaderContext, source: string, inject: string, sourceMap?: SourceMap.RawSourceMap, synchronousIfPossible = false) { inject += (!source.trim().endsWith(';')) ? ';\n' : '\n' // support existing SourceMap // https://github.com/mozilla/source-map#sourcenode // https://github.com/webpack/imports-loader/blob/master/index.js#L34-L44 // https://webpack.github.io/docs/loaders.html#writing-a-loader if (sourceMap) { const currentRequest = loaderUtils.getCurrentRequest(loader) const SourceNode = SourceMap.SourceNode const SourceMapConsumer = SourceMap.SourceMapConsumer const sourceMapConsumer = new SourceMapConsumer(sourceMap) const node = SourceNode.fromStringWithSourceMap(source, sourceMapConsumer) node.add(inject) const result = node.toStringWithSourceMap({ file: currentRequest }) loader.callback(null, result.code, result.map.toJSON()) } else { if (synchronousIfPossible) { return inject ? source + inject : source } else { loader.callback(null, source + inject) } } } export async function splitRequest(literal: string, loaderInstance?: Webpack.Core.LoaderContext) { // log(`Split Request: ${literal}`) let pathBits = literal.split(`/`) let remainingRequestBits = pathBits.slice() const literalIsRelative = literal[0] === '.' if (!literalIsRelative) { const fullPathNdIdx = pathBits.lastIndexOf('node_modules') if (fullPathNdIdx >= 0) { // conform full hard disk path /.../node_modules/MODULE_NAME/... to just MODULE_NAME/... pathBits = pathBits.slice(fullPathNdIdx + 1) } const moduleNameLength = pathBits[0].startsWith(`@`) ? 2 : 1 const moduleName = pathBits.slice(0, moduleNameLength).join(`/`) // remainingRequest may be globbed: let ifModuleRemainingRequestBits = pathBits.slice(moduleNameLength) const remainingRequest = ifModuleRemainingRequestBits.join(`/`) let moduleRoot = '' let tryModule: { resolve: EnhancedResolve.ResolveResult | undefined; } = { resolve: undefined } if (loaderInstance && !moduleName.includes(`*`)) { // TODO: test this tryModule = await resolveLiteral({ literal: `${moduleName}` }, loaderInstance, undefined, false) if (tryModule.resolve && tryModule.resolve.descriptionFileRoot) { moduleRoot = tryModule.resolve.descriptionFileRoot } log(`does module '${moduleName}' exist?: ${tryModule.resolve && 'true' || 'false'}`) } if (!loaderInstance || tryModule.resolve) { return { moduleName, moduleRoot, remainingRequest, pathBits, remainingRequestBits: ifModuleRemainingRequestBits } } } return { remainingRequest: literal, remainingRequestBits, pathBits, moduleName: '', moduleRoot: '' } } export async function expandGlobBase(literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath)) { const { pathBits, remainingRequest, remainingRequestBits, moduleName, moduleRoot } = await splitRequest(literal, loaderInstance) let possibleRoots = loaderInstance.options.resolve.modules.filter((m: string) => path.isAbsolute(m)) as Array<string> const nextGlobAtIndex = remainingRequestBits.findIndex(pb => pb.includes(`*`)) const relativePathUntilFirstGlob = remainingRequestBits.slice(0, nextGlobAtIndex).join(`/`) const relativePathFromFirstGlob = remainingRequestBits.slice(nextGlobAtIndex).join(`/`) if (moduleName && moduleRoot) { // TODO: add support for aliases when they point to a subdirectory // Or maybe the resolve will already include it? possibleRoots = [moduleRoot] } else if (rootForRelativeResolving) { possibleRoots = [rootForRelativeResolving, ...possibleRoots] } let possiblePaths = await concatPromiseResults( possibleRoots.map(async directory => await getFilesInDir(path.join(directory, relativePathUntilFirstGlob), { recursive: true, emitWarning: loaderInstance.emitWarning, emitError: loaderInstance.emitError, fileSystem: loaderInstance.fs, skipHidden: true })) ) possiblePaths = uniqBy(possiblePaths, 'filePath') // test case: escape('werwer/**/werwer/*.html').replace(/\//g, '[\\/]+').replace(/\\\*\\\*/g, '\.*?').replace(/\\\*/g, '[^/\\\\]*?') const globRegexString = escapeStringForRegex(relativePathFromFirstGlob) .replace(/\//g, '[\\/]+') // accept Windows and Unix slashes .replace(/\\\*\\\*/g, '\.*?') // multi glob ** => any number of subdirectories .replace(/\\\*/g, '[^/\\\\]*?') // single glob * => one directory (stops at first slash/backslash) const globRegex = new RegExp(`^${globRegexString}$`) // (?:\.\w+) const correctPaths = possiblePaths.filter(p => p.stat.isFile() && globRegex.test(p.relativePath)) return correctPaths.map(p => p.filePath) } const expandGlob = memoize(expandGlobBase, (literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving = path.dirname(loaderInstance.resourcePath)) => { /** valid for 10 seconds for the same literal and resoucePath */ const cacheKey = `${literal}::${path.dirname(loaderInstance.resourcePath)}::${rootForRelativeResolving}` // invalidate every 10 seconds based on each unique Webpack compilation cacheInvalidationDebounce(cacheKey, expandGlob.cache, loaderInstance._compilation) return cacheKey }) function fixWindowsPath(windowsPath: string) { return windowsPath.replace(/\\/g, '/') } export async function expandAllRequiresForGlob<T extends { literal: string }>(requires: Array<T>, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath), returnRelativeLiteral = false) { const needDeglobbing = requires.filter(r => r.literal.includes(`*`)) const deglobbed = requires.filter(r => !r.literal.includes(`*`)) const allDeglobbed = deglobbed.concat(await concatPromiseResults(needDeglobbing.map(async r => (await expandGlob(r.literal, loaderInstance, rootForRelativeResolving)) .map(correctPath => Object.assign({}, r, { literal: returnRelativeLiteral ? `./${fixWindowsPath( path.relative(path.dirname(loaderInstance.resourcePath), correctPath) )}` : correctPath })) ))) return uniqBy(allDeglobbed, 'literal') } // TODO: function cleanUpPath // this func does: makes a relative path from absolute // OR strips all node_modules and makes a 'module' request path instead // USE IT in the above glob expansion or better yet, in the below getRequireString, so we have nice requests instead of full paths! export async function getRequireStrings(maybeResolvedRequires: Array<RequireData | { literal: string, resolve?: undefined }>, addLoadersMethod: AddLoadersMethod | undefined, loaderInstance: Webpack.Core.LoaderContext, forceFallbackLoaders = false): Promise<Array<string>> { const requires = (await Promise.all(maybeResolvedRequires.map( async r => !r.resolve ? await resolveLiteral(r, loaderInstance) : r )) as Array<RequireData>).filter(r => !!r.resolve) type PathsAndLoadersWithLiterals = PathWithLoaders & {removed?: boolean, literal: string} let pathsAndLoaders: Array<PathsAndLoadersWithLiterals> if (typeof addLoadersMethod === 'function') { const maybePromise = addLoadersMethod(requires, loaderInstance) const pathsAndLoadersReturnValue = (maybePromise as Promise<Array<PathWithLoaders>>).then ? await maybePromise : maybePromise as Array<PathWithLoaders> pathsAndLoaders = pathsAndLoadersReturnValue.map(p => { const rq = requires.find(r => r.resolve.path === p.path) if (!rq) return Object.assign(p, {removed: true, literal: undefined}) return Object.assign(p, { loaders: (p.loaders && !forceFallbackLoaders) ? p.loaders : (rq.loaders || rq.fallbackLoaders || []), literal: rq.literal, removed: false }) }).filter(r => !r.removed) as Array<PathsAndLoadersWithLiterals> } else { pathsAndLoaders = requires.map(r => ({ literal: r.literal, loaders: r.loaders || r.fallbackLoaders || [], path: r.resolve.path })) } return pathsAndLoaders.map(p => (p.loaders && p.loaders.length) ? `!${p.loaders.join('!')}!${p.literal}` : p.literal ) } export function wrapInRequireInclude(toRequire: string) { return `require.include('${toRequire}');` } // TODO: memoize: export function resolveLiteral<T extends { literal: string }>(toRequire: T, loaderInstance: Webpack.Core.LoaderContext, contextPath = path.dirname(loaderInstance.resourcePath) /* TODO: could this simply be loaderInstance.context ? */, sendWarning = true) { debug('resolve')(`Resolving: ${toRequire.literal}`) return new Promise<{resolve: EnhancedResolve.ResolveResult | undefined} & T>((resolve, reject) => loaderInstance.resolve(contextPath, toRequire.literal, (err, result, value) => err ? resolve(Object.assign({resolve: value}, toRequire)) || (sendWarning && loaderInstance.emitWarning(err.message)) : resolve(Object.assign({resolve: value}, toRequire)) ) ) } export function addBundleLoader<T extends RequireDataBase>(resources: Array<T>, property = 'fallbackLoaders') { return resources.map(toRequire => { const lazy = toRequire.lazy && 'lazy' || '' const chunkName = (toRequire.chunk && `name=${toRequire.chunk}`) || '' const and = lazy && chunkName && '&' || '' const bundleLoaderPrefix = (lazy || chunkName) ? 'bundle?' : '' const bundleLoaderQuery = `${bundleLoaderPrefix}${lazy}${and}${chunkName}` return bundleLoaderQuery ? Object.assign({ [property]: [bundleLoaderQuery] }, toRequire) : toRequire }) as Array<T & { loaders?: Array<string>, fallbackLoaders?: Array<string> }> } // TODO: use custom ModuleDependency instead of injecting code export class SimpleDependencyClass extends ModuleDependency { module: Webpack.Core.NormalModule type = 'simple-dependency' constructor(request: string) { super(request) debugger } } export class SimpleDependencyTemplate { apply(parentDependency: SimpleDependencyClass, source: Webpack.WebpackSources.ReplaceSource, outputOptions: { pathinfo }, requestShortener: { shorten: (request: string) => string }) { debugger if (outputOptions.pathinfo && parentDependency.module) { const comment = ("/*! simple-dependency " + requestShortener.shorten(parentDependency.request) + " */") source.insert(source.size(), comment) } } } export const SimpleDependency = Object.assign(SimpleDependencyClass, { Template: SimpleDependencyTemplate })