├── .editorconfig ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode ├── settings.json └── tasks.json ├── DEV_NOTES.md ├── LICENSE.md ├── custom_typings ├── any.d.ts ├── enhanced-resolve.d.ts ├── enhanced-resolve.files.d.ts ├── webpack.d.ts └── webpack.files.d.ts ├── example └── aurelia.ts ├── index.html ├── index.ts ├── loaders ├── comment-loader.ts ├── convention-loader.ts ├── html-require-loader.ts └── list-based-require-loader.ts ├── package.json ├── plugins ├── convention-invalidate-plugin.ts ├── mapped-module-ids-plugin.ts ├── rewrite-module-subdirectory-plugin.ts └── root-most-resolve-plugin.ts ├── readme.md ├── temp ├── tmp.ts └── useful.ts ├── test-fixtures ├── app-extra │ ├── extra.js │ └── sub │ │ ├── extra-sub.js │ │ ├── hello.html │ │ └── hello.js └── app │ ├── app.html │ ├── app.js │ ├── car.js │ ├── car.spec.js │ ├── engine.js │ ├── engine.spec.js │ ├── index.html │ ├── main.js │ ├── nav-bar.html │ ├── resources │ ├── double.js │ ├── glooob │ │ ├── subdir │ │ │ └── test-c.js │ │ ├── test-a.js │ │ ├── test-b.html │ │ └── test-b.js │ ├── hello.html │ ├── hello.js │ └── triple.js │ ├── root-most.js │ ├── sub │ ├── double.js │ ├── hello.html │ └── hello.js │ ├── welcome.html │ └── welcome.js ├── test ├── e2e.spec.js └── preprocessor.js ├── tsconfig.json ├── typings ├── definitions.d.ts └── promisify.d.ts ├── utils ├── index.spec.ts ├── index.ts └── inject.ts ├── webpack.config.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | max_line_length = 80 10 | 11 | [*.json] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | dist/ 4 | *.log 5 | test-fixtures/webpack-dist 6 | loaders/*.js 7 | example/*.js 8 | plugins/*.js 9 | utils/*.js 10 | /index.js 11 | loaders/*.d.ts 12 | example/*.d.ts 13 | plugins/*.d.ts 14 | utils/*.d.ts 15 | /index.d.ts 16 | *.map 17 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | .vscode 4 | *.log 5 | test-fixtures/webpack-dist 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | notifications: 3 | email: false 4 | language: node_js 5 | node_js: 6 | - '8' 7 | cache: 8 | directories: 9 | - $HOME/.yarn-cache 10 | - node_modules 11 | before_install: 12 | # Repo for newer Node.js versions 13 | - npm install -g yarn 14 | install: 15 | - yarn 16 | before_script: 17 | - yarn run build 18 | script: 19 | - yarn run test 20 | after_success: 21 | - yarn run semantic-release 22 | branches: 23 | except: 24 | - /^v\d+\.\d+\.\d+$/ 25 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": false, 5 | "**/bower_components": true 6 | }, 7 | "typescript.tsdk": "./node_modules/typescript/lib" 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-w", "-p", ".", "-t", "es5"], 8 | "showOutput": "silent", 9 | "isWatching": true, 10 | "problemMatcher": "$tsc-watch" 11 | } 12 | -------------------------------------------------------------------------------- /DEV_NOTES.md: -------------------------------------------------------------------------------- 1 | # Notes 2 | 3 | ## TODO 4 | - better easy-webpack: config is an pure object AND a list of packages to be installed as dev-dependencies 5 | - generator of require.include duplicate plugins, so that we can better name reasons when doing --display-reasons 6 | - think about globs in comments (can't do them now) 7 | - (maybe) fork (or require) bundle loader https://github.com/webpack/bundle-loader/blob/master/index.js 8 | and add a parameter, e.g. module.exports.SIGNIFIER = true 9 | so that its clear to the aurelia-loader its an unresolved method 10 | - add tests for adding resources from list when they are relative to package's "main" (currently tries resolving as ${module_name}/thing FIRST) 11 | - document the option to use the package.json dependencies only as the SINGLE SOURCE OF TRUTH, 12 | without adding any external dependencies from it for the local package (maybe: only for dependencies) 13 | - add main package.json to dependencies with the loader so webpack reloads when it changes 14 | 15 | ## Other ideas 16 | - PLUGIN: add a statically named custom module that's loaded in the aurelia-loader 17 | ```js 18 | export = function(moduleId) { 19 | var map = { 20 | 'aurelia-module-id': 2 // webpack moduleId 21 | } 22 | } 23 | ``` 24 | 25 | see webpack/lib/ContextModule.js 26 | and Module.prototype.source / ExternalModule 27 | or maybe hook somewhere where list of all modules is generated? 28 | 29 | alternatively, generate the list dynamically in client 30 | and TODO this later properly 31 | 32 | after all modules are resolved 33 | - template lint plugin 34 | - custom CSS loaders for HTML requires 35 | 36 | ## Dev Notes 37 | 38 | ``` 39 | /** 40 | * to list all already previously resources, iterate: 41 | * loader._compilation.modules[0].resource (or userRequest ?) 42 | * then loader._compilation.modules[0].issuer.resource / userRequest will contain the origin of the addition 43 | */ 44 | ``` 45 | - its enough if list-based require only cares about its OWN resources 46 | resources of the request being made. 47 | - maybe extra module property instead of ID? 48 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Bazyli Brzóska 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /custom_typings/any.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'acorn/dist/walk' { 2 | import * as ESTree from 'estree' 3 | export function findNodeAfter(program: ESTree.Program, after: number): {node: ESTree.Node} 4 | } 5 | declare module 'loader-utils' { 6 | export function parseQuery(query: any): any 7 | export function getCurrentRequest(webpackLoader) 8 | } 9 | declare module 'html-loader' 10 | declare module 'enhanced-resolve/lib/getInnerRequest' 11 | -------------------------------------------------------------------------------- /custom_typings/enhanced-resolve.d.ts: -------------------------------------------------------------------------------- 1 | export as namespace EnhancedResolve; 2 | export = EnhancedResolve 3 | 4 | declare namespace EnhancedResolve { 5 | export type ResolveCallback = Webpack.Core.StandardCallbackWithLog & { missing?: Array } 6 | 7 | export interface ResolveContext { 8 | issuer?: string 9 | } 10 | 11 | export interface ResolveResult { 12 | context: ResolveContext 13 | /** 14 | * related package.json file 15 | */ 16 | descriptionFileData: { [index: string]: any, version: string, name: string, dependencies: {[index:string]: string} } 17 | /** 18 | * full path to package.json 19 | */ 20 | descriptionFilePath: string 21 | /** 22 | * full path to module root directory 23 | */ 24 | descriptionFileRoot: string 25 | file: boolean 26 | module: boolean 27 | path: string 28 | query: string | undefined 29 | relativePath: string 30 | request: undefined | any // TODO 31 | } 32 | 33 | export class Resolver { 34 | resolve(context: ResolveContext, path: string, request: string, callback: EnhancedResolve.ResolveCallback): void 35 | doResolve 36 | plugin 37 | } 38 | 39 | export function createInnerCallback(callback: T, options: { log?: (msg:string) => void, stack?: any, missing?: Array }, message?: string | null, messageOptional?: boolean): T & { log: (msg: string) => void, stack: any, missing: Array } 40 | } 41 | -------------------------------------------------------------------------------- /custom_typings/enhanced-resolve.files.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'enhanced-resolve/lib/Resolver' { 2 | export = EnhancedResolve.Resolver 3 | } 4 | 5 | declare module 'enhanced-resolve/lib/createInnerCallback' { 6 | export = EnhancedResolve.createInnerCallback 7 | } 8 | -------------------------------------------------------------------------------- /custom_typings/webpack.d.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as Resolver from './enhanced-resolve' 3 | import * as SourceMap from 'source-map' 4 | 5 | export as namespace Webpack; 6 | export = Webpack 7 | declare namespace Webpack { 8 | export namespace Core { 9 | export class ModuleDependency {//extends Module { 10 | constructor(request: string) 11 | request: string 12 | userRequest: string 13 | isEqualResource(otherResource: ModuleDependency) 14 | } 15 | 16 | export class Module { 17 | constructor(request: string) 18 | module: NormalModule | null 19 | } 20 | 21 | export class SingleEntryDependency { 22 | module: NormalModule; 23 | request: string; 24 | userRequest: string; 25 | loc: string; 26 | } 27 | 28 | export class NormalModule extends MultiModule { 29 | request: string; 30 | userRequest: string; 31 | rawRequest: string; 32 | parser: Parser; 33 | resource: string; 34 | loaders: Loader[]; 35 | fileDependencies: any[]; 36 | contextDependencies: any[]; 37 | error?: any; 38 | _source?: any; 39 | assets: Asset; 40 | _cachedSource?: any; 41 | optional: boolean; 42 | building: any[]; 43 | buildTimestamp: number; 44 | libIdent(options: { context?: string }): string 45 | } 46 | 47 | export class MultiModule { 48 | dependencies: SingleEntryDependency[]; 49 | blocks: any[]; 50 | variables: any[]; 51 | context: string; 52 | reasons: ModuleReason[]; 53 | debugId: number; 54 | lastId: number; 55 | id?: string | number | null; 56 | index?: any; 57 | index2?: any; 58 | used?: any; 59 | usedExports?: any; 60 | providedExports?: any; 61 | chunks: any[]; 62 | warnings: any[]; 63 | dependenciesWarnings: any[]; 64 | errors: any[]; 65 | dependenciesErrors: any[]; 66 | strict: boolean; 67 | meta: Object; 68 | name: string; 69 | built: boolean; 70 | cacheable: boolean; 71 | issuer?: MultiModule | null; 72 | } 73 | 74 | export interface ModuleReason { 75 | module: MultiModule; 76 | dependency: string; 77 | } 78 | 79 | export interface ParserPlugins { 80 | 'evaluate Literal': any[]; 81 | 'evaluate LogicalExpression': any[]; 82 | 'evaluate BinaryExpression': any[]; 83 | 'evaluate UnaryExpression': any[]; 84 | 'evaluate typeof undefined': any[]; 85 | 'evaluate Identifier': any[]; 86 | 'evaluate MemberExpression': any[]; 87 | 'evaluate CallExpression': any[]; 88 | 'evaluate CallExpression .replace': any[]; 89 | 'evaluate CallExpression .substr': any[]; 90 | 'evaluate CallExpression .substring': any[]; 91 | 'evaluate CallExpression .split': any[]; 92 | 'evaluate ConditionalExpression': any[]; 93 | 'evaluate ArrayExpression': any[]; 94 | 'expression process': any[]; 95 | 'expression global': any[]; 96 | 'expression Buffer': any[]; 97 | 'expression setImmediate': any[]; 98 | 'expression clearImmediate': any[]; 99 | 'call require': any[]; 100 | 'expression __filename': any[]; 101 | 'evaluate Identifier __filename': any[]; 102 | 'expression __dirname': any[]; 103 | 'evaluate Identifier __dirname': any[]; 104 | 'expression require.main': any[]; 105 | 'expression require.extensions': any[]; 106 | 'expression module.loaded': any[]; 107 | 'expression module.id': any[]; 108 | 'expression module.exports': any[]; 109 | 'evaluate Identifier module.hot': any[]; 110 | 'expression module': any[]; 111 | 'call require.config': any[]; 112 | 'call requirejs.config': any[]; 113 | 'expression require.version': any[]; 114 | 'expression requirejs.onError': any[]; 115 | 'expression __webpack_require__': any[]; 116 | 'evaluate typeof __webpack_require__': any[]; 117 | 'expression __webpack_public_path__': any[]; 118 | 'evaluate typeof __webpack_public_path__': any[]; 119 | 'expression __webpack_modules__': any[]; 120 | 'evaluate typeof __webpack_modules__': any[]; 121 | 'expression __webpack_chunk_load__': any[]; 122 | 'evaluate typeof __webpack_chunk_load__': any[]; 123 | 'expression __non_webpack_require__': any[]; 124 | 'evaluate typeof __non_webpack_require__': any[]; 125 | 'expression require.onError': any[]; 126 | 'evaluate typeof require.onError': any[]; 127 | 'statement if': any[]; 128 | 'expression ?:': any[]; 129 | 'evaluate Identifier __resourceQuery': any[]; 130 | 'expression __resourceQuery': any[]; 131 | 'program': any[]; 132 | 'call require.include': any[]; 133 | 'evaluate typeof require.include': any[]; 134 | 'typeof require.include': any[]; 135 | 'call require.ensure': any[]; 136 | 'evaluate typeof require.ensure': any[]; 137 | 'typeof require.ensure': any[]; 138 | 'call require.context': any[]; 139 | 'call require:amd:array': any[]; 140 | 'call require:amd:item': any[]; 141 | 'call require:amd:context': any[]; 142 | 'call define': any[]; 143 | 'call define:amd:array': any[]; 144 | 'call define:amd:item': any[]; 145 | 'call define:amd:context': any[]; 146 | 'expression require.amd': any[]; 147 | 'expression define.amd': any[]; 148 | 'expression define': any[]; 149 | 'expression __webpack_amd_options__': any[]; 150 | 'evaluate typeof define.amd': any[]; 151 | 'evaluate typeof require.amd': any[]; 152 | 'evaluate Identifier define.amd': any[]; 153 | 'evaluate Identifier require.amd': any[]; 154 | 'evaluate typeof define': any[]; 155 | 'typeof define': any[]; 156 | 'can-rename define': any[]; 157 | 'rename define': any[]; 158 | 'evaluate typeof require': any[]; 159 | 'typeof require': any[]; 160 | 'evaluate typeof require.resolve': any[]; 161 | 'typeof require.resolve': any[]; 162 | 'evaluate typeof require.resolveWeak': any[]; 163 | 'typeof require.resolveWeak': any[]; 164 | 'evaluate typeof module': any[]; 165 | 'assign require': any[]; 166 | 'can-rename require': any[]; 167 | 'rename require': any[]; 168 | 'typeof module': any[]; 169 | 'evaluate typeof exports': any[]; 170 | 'expression require.cache': any[]; 171 | 'expression require': any[]; 172 | 'call require:commonjs:item': any[]; 173 | 'call require:commonjs:context': any[]; 174 | 'call require.resolve': any[]; 175 | 'call require.resolve(Weak)': any[]; 176 | 'call require.resolve(Weak):item': any[]; 177 | 'call require.resolve(Weak):context': any[]; 178 | 'import': any[]; 179 | 'import specifier': any[]; 180 | 'expression imported var.*': any[]; 181 | 'call imported var': any[]; 182 | 'hot accept callback': any[]; 183 | 'hot accept without callback': any[]; 184 | 'export': any[]; 185 | 'export import': any[]; 186 | 'export expression': any[]; 187 | 'export declaration': any[]; 188 | 'export specifier': any[]; 189 | 'export import specifier': any[]; 190 | 'evaluate typeof System': any[]; 191 | 'typeof System': any[]; 192 | 'evaluate typeof System.import': any[]; 193 | 'typeof System.import': any[]; 194 | 'evaluate typeof System.set': any[]; 195 | 'expression System.set': any[]; 196 | 'evaluate typeof System.get': any[]; 197 | 'expression System.get': any[]; 198 | 'evaluate typeof System.register': any[]; 199 | 'expression System.register': any[]; 200 | 'expression System': any[]; 201 | 'call System.import': any[]; 202 | } 203 | 204 | export interface Parser { 205 | _plugins: ParserPlugins; 206 | } 207 | 208 | export interface Loader { 209 | /** 210 | * contents of 'query' object passed in the webpack config 211 | */ 212 | options: any; 213 | loader: string; 214 | } 215 | 216 | export interface Asset {} 217 | 218 | export type LoaderCallback = (error?: Error | undefined | null, code?: string, jsonSourceMap?: SourceMap.RawSourceMap) => void 219 | export interface LoaderContext { 220 | fs: typeof fs & CachedInputFileSystem 221 | 222 | _compiler // Compiler 223 | _compilation 224 | _module 225 | version: number 226 | emitWarning: (warning: string) => void 227 | emitError: (error: string) => void 228 | /** 229 | * Compiles the code and returns its module.exports 230 | */ 231 | exec: (code: string, filename: string) => any 232 | resolve: (path: string, request: string, callback: EnhancedResolve.ResolveCallback) => void 233 | resolveSync: (path: string, request: string) => void 234 | sourceMap: boolean 235 | webpack: boolean 236 | options // webpack options 237 | target // options.target 238 | loadModule: Function 239 | context: string 240 | loaderIndex: number 241 | loaders: Array 242 | 243 | /** 244 | * Full path to the file being loaded 245 | */ 246 | resourcePath: string 247 | resourceQuery: string 248 | /** 249 | * Mark the Loader as asynchronous (use together with the callback) 250 | */ 251 | async: () => LoaderCallback 252 | cacheable: () => void 253 | callback: LoaderCallback 254 | addDependency: Function 255 | dependency: Function 256 | addContextDependency: Function 257 | getDependencies: Function 258 | getContextDependencies: Function 259 | clearDependencies: Function 260 | resource 261 | request 262 | remainingRequest 263 | currentRequest 264 | previousRequest 265 | query: any 266 | data: null | any 267 | } 268 | 269 | export interface CachedInputFileSystem { 270 | fileSystem: typeof fs 271 | } 272 | 273 | export interface LoaderInfo { 274 | path: string 275 | query: string 276 | options 277 | normal: Function // executes loader? 278 | pitch: Function // executes loader? 279 | raw 280 | data 281 | pitchExecuted: boolean 282 | normalExecuted: boolean 283 | request 284 | } 285 | 286 | export type StandardCallback = (err: Error | undefined, result1: T, result2: R) => void 287 | export type StandardCallbackWithLog = StandardCallback & { log?: (info: string) => void } 288 | } 289 | 290 | export namespace WebpackSources { 291 | export class ReplaceSource extends Source { 292 | constructor(source, name) 293 | insert(pos, newValue) 294 | listMap(options) 295 | map(options) 296 | node: (options)=>any 297 | replace(start, end, newValue) 298 | source: (options)=>string 299 | sourceAndMap(options) 300 | } 301 | class Source { 302 | listNode: any|null 303 | map(options) 304 | node: any|null 305 | size() 306 | source: any|null 307 | sourceAndMap(options) 308 | updateHash(hash) 309 | } 310 | } 311 | } 312 | -------------------------------------------------------------------------------- /custom_typings/webpack.files.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webpack/lib/dependencies/ModuleDependency' { 2 | export = Webpack.Core.ModuleDependency 3 | } 4 | 5 | declare module 'webpack/lib/dependencies/Module' { 6 | export = Webpack.Core.Module 7 | } 8 | 9 | declare module 'webpack/lib/NormalModule' { 10 | export = Webpack.Core.NormalModule 11 | } 12 | 13 | declare module 'webpack-sources/lib/ReplaceSource' { 14 | export = Webpack.WebpackSources.ReplaceSource 15 | } 16 | 17 | declare module 'webpack-sources/lib/Source' { 18 | export = Webpack.WebpackSources.Source 19 | } 20 | -------------------------------------------------------------------------------- /example/aurelia.ts: -------------------------------------------------------------------------------- 1 | import { concatPromiseResults, getResourcesFromList } from '../utils' 2 | import { addBundleLoader, expandAllRequiresForGlob, resolveLiteral } from '../utils/inject' 3 | import { PathWithLoaders, RequireData, RequireDataBaseResolved } from '../typings/definitions' 4 | import * as path from 'path' 5 | import * as debug from 'debug' 6 | const log = debug('aurelia') 7 | 8 | /** 9 | * 1. load MAIN package.json 10 | * 2. get the aurelia resources: packageJson.aurelia && packageJson.aurelia.build && packageJson.aurelia.build.resources 11 | * 3. glob all resources 12 | * 4. resolve each resource in the context of MAIN package.json 13 | * 5. foreach files, match with resolved resources and replace loaders or return what was there 14 | * 15 | * @param {Object} packageJson 16 | * @param {string} rootDir 17 | * @param {Array} files 18 | * @param {Webpack.Core.LoaderContext} loaderInstance 19 | * @returns {Promise>} 20 | */ 21 | export async function addLoadersMethod(rootDir: string, files: Array, loaderInstance: Webpack.Core.LoaderContext): Promise> { 22 | let resolvedResources = loaderInstance._compilation._aureliaResolvedResources as Array 23 | if (!resolvedResources) { 24 | // TODO: acquire packageJson via builtin FileSystem, not Node 25 | const packageJsonPath = path.join(rootDir, 'package.json') 26 | // loaderInstance.addDependency(packageJsonPath) 27 | const packageJson = require(packageJsonPath) 28 | const resources = getResourcesFromList(packageJson, 'aurelia.build.resources') 29 | const resourceData = await addBundleLoader(resources, 'loaders') 30 | const globbedResources = await expandAllRequiresForGlob(resourceData, loaderInstance, false) 31 | loaderInstance._compilation._aureliaResolvedResources = resolvedResources = (await concatPromiseResults( 32 | globbedResources.map(r => resolveLiteral(r, loaderInstance, rootDir) as any /* TODO: typings */) 33 | )).filter(rr => !!rr.resolve) 34 | 35 | } 36 | 37 | // resolvedResources.forEach(rr => log(rr.resolve.path)) 38 | // const hmm = files.filter(f => f.resolve.path.includes(`aurelia-templating-resources`)).map(f => f.resolve.path) 39 | // if (hmm.length) { 40 | // log(hmm.find(f => !!resolvedResources.find(rr => rr.resolve.path === f))) 41 | // const fss = resolvedResources.find(rr => rr.resolve.path === hmm.find(f => f.includes(`signal-binding`))) 42 | // if (fss) log(fss) 43 | // } 44 | 45 | return files 46 | // .filter(f => !!f.resolve) 47 | .map(f => { 48 | const resolvedFile = resolvedResources.find(rr => rr.resolve.path === f.resolve.path) 49 | return { path: f.resolve.path, loaders: (resolvedFile && resolvedFile.loaders) || undefined } 50 | // return (resolvedFile && resolvedFile.loaders) ? (Object.assign(f, { loaders: resolvedFile.loaders })) : f 51 | }) 52 | 53 | // return enforcedLoadersFiles.map(f => ({ 54 | // path: f.resolve.path, 55 | // loaders: f.loaders 56 | // })) 57 | } 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webpack Developer Kit 7 | 8 | 9 | 12 | 13 | 17 | 18 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugins/mapped-module-ids-plugin' 2 | export * from './plugins/rewrite-module-subdirectory-plugin' 3 | export * from './plugins/root-most-resolve-plugin' 4 | export * from './plugins/convention-invalidate-plugin' 5 | export * from './utils' 6 | export * from './utils/inject' 7 | export * from './typings/definitions' 8 | export { default as CommentLoader } from './loaders/comment-loader' 9 | export { default as ConventionLoader, conventions } from './loaders/convention-loader' 10 | export { default as HtmlRequireLoader } from './loaders/html-require-loader' 11 | export { default as ListBasedRequireLoader } from './loaders/list-based-require-loader' 12 | -------------------------------------------------------------------------------- /loaders/comment-loader.ts: -------------------------------------------------------------------------------- 1 | import { CommentLoaderOptions } from '../typings/definitions' 2 | import * as path from 'path' 3 | import * as loaderUtils from 'loader-utils' 4 | import * as SourceMap from 'source-map' 5 | import * as acorn from 'acorn' 6 | import * as walk from 'acorn/dist/walk' 7 | import * as ESTree from 'estree' 8 | import * as debug from 'debug' 9 | import {appendCodeAndCallback, getRequireStrings, wrapInRequireInclude, resolveLiteral, addBundleLoader, SimpleDependency, expandAllRequiresForGlob} from '../utils/inject' 10 | 11 | const log = debug('comment-loader') 12 | 13 | export function findLiteralNodesAfterBlockComment(ast: ESTree.Program, comments: Array, commentRegex: RegExp) { 14 | return comments 15 | .filter(comment => comment.type === 'Block') 16 | .map(commentAst => { 17 | let value = commentAst.value.trim() 18 | let match = value.match(commentRegex) 19 | return { ast: commentAst, match } 20 | }) 21 | .filter(commentMatch => !!commentMatch.match) 22 | .map(comment => { 23 | const result = walk.findNodeAfter(ast, comment.ast.end) 24 | return { 25 | commentMatch: comment.match, 26 | literal: result.node && result.node.type === 'Literal' && typeof result.node.value === 'string' ? result.node.value : '' 27 | } 28 | }) 29 | .filter(comment => !!comment.literal) 30 | } 31 | 32 | export default async function CommentLoader (this: Webpack.Core.LoaderContext, source: string, sourceMap?: SourceMap.RawSourceMap) { 33 | const query = Object.assign({}, loaderUtils.parseQuery(this.query)) as CommentLoaderOptions 34 | 35 | if (this.cacheable) { 36 | this.cacheable() 37 | } 38 | 39 | this.async() 40 | 41 | // log(`Parsing ${path.basename(this.resourcePath)}`) 42 | 43 | const comments: Array = [] 44 | let ast: ESTree.Program | undefined = undefined 45 | 46 | const POSSIBLE_AST_OPTIONS = [{ 47 | ranges: true, 48 | locations: true, 49 | ecmaVersion: 2017, 50 | sourceType: 'module', 51 | onComment: comments 52 | }, { 53 | ranges: true, 54 | locations: true, 55 | ecmaVersion: 2017, 56 | sourceType: 'script', 57 | onComment: comments 58 | }] as Array 59 | 60 | let i = POSSIBLE_AST_OPTIONS.length 61 | while (!ast && i--) { 62 | try { 63 | comments.length = 0 64 | ast = acorn.parse(source, POSSIBLE_AST_OPTIONS[i]); 65 | } catch(e) { 66 | // ignore the error 67 | if (!i) { 68 | this.emitError(`Error while parsing ${this.resourcePath}: ${e.message}`) 69 | return this.callback(undefined, source, sourceMap) 70 | } 71 | } 72 | } 73 | 74 | /** 75 | * @import @lazy @chunk('module') 'something' 76 | */ 77 | const commentsAndLiterals = 78 | findLiteralNodesAfterBlockComment(ast as ESTree.Program, comments, /^@import *(@lazy)? *(?:@chunk\(['"`]*([\w-]+)['"`]*\))? *(@lazy)?/) 79 | .map((cal: { commentMatch: RegExpMatchArray, literal: string }) => ({ 80 | literal: cal.literal, 81 | lazy: !!(cal.commentMatch[1] || cal.commentMatch[3]), 82 | chunk: cal.commentMatch[2] 83 | })) 84 | 85 | /** 86 | * @import('module') @lazy @chunk('module') 87 | */ 88 | const commentOnlyImports = comments 89 | .filter(c => c.type === 'Block') 90 | .map(c => c.value.trim().match(/^@import\([\'"`]*([- \./\w*]+)['"`]\)* *(@lazy)? *(?:@chunk\(['"`]*([\w-]+)['"`]*\))? *(@lazy)?$/)) 91 | .filter(c => !!c) 92 | .map((c: RegExpMatchArray) => ({ 93 | literal: c[1], 94 | lazy: !!(c[2] || c[4]), 95 | chunk: c[3] 96 | })) 97 | 98 | if (!commentsAndLiterals.length && !commentOnlyImports.length) { 99 | this.callback(undefined, source, sourceMap) 100 | return 101 | } 102 | 103 | const allResources = [...commentsAndLiterals, ...commentOnlyImports] 104 | 105 | try { 106 | let resourceData = await addBundleLoader(allResources) 107 | 108 | if (query.enableGlobbing) { 109 | resourceData = await expandAllRequiresForGlob(resourceData, this) 110 | } else { 111 | resourceData = resourceData.filter(r => !r.literal.includes(`*`)) 112 | } 113 | 114 | log(`Adding resources to ${this.resourcePath}: ${resourceData.map(r => r.literal).join(', ')}`) 115 | 116 | const requireStrings = await getRequireStrings(resourceData, query.addLoadersCallback, this, query.alwaysUseCommentBundles) 117 | const inject = requireStrings.map(wrapInRequireInclude).join('\n') 118 | appendCodeAndCallback(this, source, inject, sourceMap) 119 | } catch (e) { 120 | log(e) 121 | this.emitError(e.message) 122 | this.callback(undefined, source, sourceMap) 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /loaders/convention-loader.ts: -------------------------------------------------------------------------------- 1 | import { ConventionFunction, ConventionOptions, Convention } from '../typings/definitions' 2 | import * as path from 'path' 3 | import * as fs from 'fs' 4 | import * as loaderUtils from 'loader-utils' 5 | import * as SourceMap from 'source-map' 6 | import * as webpack from 'webpack' 7 | import {appendCodeAndCallback, getRequireStrings, resolveLiteral, wrapInRequireInclude, SimpleDependency} from '../utils/inject' 8 | import {getFilesInDir} from '../utils' 9 | import * as debug from 'debug' 10 | const log = debug('convention-loader') 11 | 12 | export const conventions: { [convention: string]: ConventionFunction } = { 13 | 'extension-swap'(fullPath: string, query: ConventionOptions) { 14 | const basename = path.basename(fullPath) 15 | const noExtension = basename.substr(0, basename.lastIndexOf('.')) || basename 16 | let extensions: string[] 17 | if (Array.isArray(query.extension)) { 18 | extensions = query.extension 19 | } else { 20 | extensions = query.extension ? [query.extension] : ['.html', '.css'] 21 | } 22 | const basepath = path.dirname(fullPath) 23 | return extensions.map(extension => path.join(basepath, noExtension + extension)) 24 | }, 25 | 26 | async 'all-files-matching-regex'(fullPath: string, query: ConventionOptions & {regex: RegExp, directory: string}, loaderInstance: Webpack.Core.LoaderContext) { 27 | const files = await getFilesInDir(query.directory, { 28 | regexFilter: query.regex, 29 | emitWarning: loaderInstance.emitWarning.bind(loaderInstance), 30 | emitError: loaderInstance.emitError.bind(loaderInstance), 31 | fileSystem: loaderInstance.fs, 32 | recursive: true 33 | }) 34 | 35 | return files 36 | .filter(file => file.filePath !== loaderInstance.resourcePath) 37 | .map(file => file.filePath) 38 | }, 39 | 40 | // async 'list-based'(fullPath: string, query: ConventionQuery & { packageProperty: string }, loaderInstance: Webpack.Core.LoaderContext) { 41 | 42 | // }, 43 | } 44 | 45 | export default async function ConventionLoader (this: Webpack.Core.LoaderContext, source: string, sourceMap?: SourceMap.RawSourceMap) { 46 | this.async() 47 | 48 | const query = Object.assign({}, loaderUtils.parseQuery(this.query)) as ConventionOptions 49 | 50 | if (this.cacheable) { 51 | this.cacheable() 52 | } 53 | 54 | if (!query || !query.convention) { 55 | this.emitError(`No convention defined, passing through: ${this.currentRequest} / ${this.request}`) 56 | this.callback(undefined, source, sourceMap) 57 | return 58 | } 59 | 60 | // log(`Convention loading ${path.basename(this.resourcePath)}`) 61 | 62 | let requires: Array = [] 63 | const maybeAddResource = async (input: string | string[] | Promise) => { 64 | if (!input) return 65 | const value = (input as Promise).then ? await input : input as string | string[] 66 | const fullPaths = typeof value === 'string' ? [value] : value 67 | await Promise.all(fullPaths.map(async fullPath => { 68 | const stat = await new Promise((resolve, reject) => 69 | this.fs.stat(fullPath, (err, value) => resolve(value))) 70 | if (stat) { 71 | requires.push(fullPath) 72 | } 73 | })) 74 | } 75 | 76 | const actOnConvention = async (convention: Convention) => { 77 | if (typeof convention === 'function') { 78 | await maybeAddResource(convention(this.resourcePath, query, this)) 79 | } else { 80 | if (conventions[convention]) 81 | await maybeAddResource(conventions[convention](this.resourcePath, query, this)) 82 | else 83 | throw new Error(`No default convention named '${convention}' found`) 84 | } 85 | } 86 | 87 | try { 88 | if (typeof query.convention !== 'function' && typeof query.convention !== 'string') { 89 | await Promise.all(query.convention.map(actOnConvention)) 90 | } else { 91 | await actOnConvention(query.convention) 92 | } 93 | 94 | if (!requires.length) { 95 | this.callback(undefined, source, sourceMap) 96 | return 97 | } 98 | 99 | const resourceDir = path.dirname(this.resourcePath) 100 | const relativeRequires = requires.map(r => ({ literal: `./${path.relative(resourceDir, r)}` })) 101 | 102 | log(`Adding resources to ${this.resourcePath}: ${relativeRequires.map(r => r.literal).join(', ')}`) 103 | 104 | const requireStrings = await getRequireStrings( 105 | relativeRequires, query.addLoadersCallback, this 106 | ) 107 | 108 | const inject = requireStrings.map(wrapInRequireInclude).join('\n') 109 | return appendCodeAndCallback(this, source, inject, sourceMap) 110 | } catch (e) { 111 | log(e) 112 | this.emitError(e.message) 113 | this.callback(undefined, source, sourceMap) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /loaders/html-require-loader.ts: -------------------------------------------------------------------------------- 1 | import { SelectorAndAttribute, HtmlRequireOptions, RequireDataBase } from '../typings/definitions' 2 | import * as path from 'path' 3 | import * as loaderUtils from 'loader-utils' 4 | import * as SourceMap from 'source-map' 5 | import {addBundleLoader, getRequireStrings, wrapInRequireInclude, appendCodeAndCallback, SimpleDependency, expandAllRequiresForGlob} from '../utils/inject' 6 | import {getTemplateResourcesData} from '../utils' 7 | import * as htmlLoader from 'html-loader' 8 | import * as debug from 'debug' 9 | const log = debug('html-require-loader') 10 | 11 | export const htmlRequireDefaults = { 12 | selectorsAndAttributes: [ 13 | // e.g. 14 | // e.g. 15 | { selector: 'require', attribute: 'from' }, 16 | // e.g. 17 | { selector: '[view-model]', attribute: 'view-model' }, 18 | // e.g. 19 | { selector: '[view]', attribute: 'view' }, 20 | ], 21 | // by default glob template string: e.g. '${anything}' 22 | globReplaceRegex: /\${.+?}/g, 23 | enableGlobbing: true 24 | } as HtmlRequireOptions 25 | 26 | export default function HtmlRequireLoader (this: Webpack.Core.LoaderContext, pureHtml: string, sourceMap?: SourceMap.RawSourceMap) { 27 | if (this.cacheable) { 28 | this.cacheable() 29 | } 30 | const query = Object.assign({}, htmlRequireDefaults, loaderUtils.parseQuery(this.query)) as HtmlRequireOptions & {selectorsAndAttributes: Array} 31 | const source = htmlLoader.bind(this)(pureHtml, sourceMap) as string 32 | 33 | try { 34 | const resources = getTemplateResourcesData(pureHtml, query.selectorsAndAttributes, query.globReplaceRegex) 35 | if (!resources.length) { 36 | return source 37 | } 38 | 39 | return (async () => { 40 | this.async() 41 | let resourceData = await addBundleLoader(resources) 42 | log(`Adding resources to ${this.resourcePath}: ${resourceData.map(r => r.literal).join(', ')}`) 43 | 44 | if (query.enableGlobbing) { 45 | resourceData = await expandAllRequiresForGlob(resourceData, this) 46 | } else { 47 | resourceData = resourceData.filter(r => !r.literal.includes(`*`)) 48 | } 49 | 50 | const requireStrings = await getRequireStrings( 51 | resourceData, query.addLoadersCallback, this 52 | ) 53 | 54 | const inject = requireStrings.map(wrapInRequireInclude).join('\n') 55 | return appendCodeAndCallback(this, source, inject, sourceMap) 56 | })().catch(e => { 57 | log(e) 58 | this.emitError(e.message) 59 | return this.callback(undefined, source, sourceMap) 60 | }) 61 | } catch (e) { 62 | log(e) 63 | this.emitError(e.message) 64 | return source 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /loaders/list-based-require-loader.ts: -------------------------------------------------------------------------------- 1 | import { 2 | AddLoadersOptions, 3 | RequireData, 4 | RequireDataBase, 5 | RequireDataBaseMaybeResolved, 6 | RequireDataBaseResolved, 7 | ListBasedRequireOptions 8 | } from '../typings/definitions' 9 | import { 10 | addBundleLoader, 11 | appendCodeAndCallback, 12 | expandAllRequiresForGlob, 13 | getRequireStrings, 14 | resolveLiteral, 15 | wrapInRequireInclude 16 | } from '../utils/inject' 17 | import * as SourceMap from 'source-map' 18 | import * as loaderUtils from 'loader-utils' 19 | import { concatPromiseResults, getResourcesFromList } from '../utils' 20 | import * as path from 'path' 21 | import * as debug from 'debug' 22 | const log = debug('list-based-require-loader') 23 | 24 | export default async function ListBasedRequireLoader (this: Webpack.Core.LoaderContext, source: string, sourceMap?: SourceMap.RawSourceMap) { 25 | this.async() 26 | 27 | // add defaults: 28 | const query = Object.assign({ requireInFirstFileOnly: true, enableGlobbing: false }, loaderUtils.parseQuery(this.query)) as ListBasedRequireOptions 29 | 30 | if (this.cacheable) { 31 | this.cacheable() 32 | } 33 | 34 | /** 35 | * 1. resolve SELF to get the package.json contents 36 | * 2. _.get to the object containing resource info 37 | * 3. include 38 | */ 39 | try { 40 | const self = await resolveLiteral({ literal: this.resourcePath }, this) 41 | const resolve = self.resolve 42 | 43 | // only do require.include in the FIRST file that comes along, when that option is enabled 44 | const listBasedRequireDone: Set = this._compilation.listBasedRequireDone || (this._compilation.listBasedRequireDone = new Set()) 45 | if (!resolve || (query.requireInFirstFileOnly && listBasedRequireDone.has(resolve.descriptionFileRoot))) { 46 | return this.callback(undefined, source, sourceMap) 47 | } else if (query.requireInFirstFileOnly) { 48 | listBasedRequireDone.add(resolve.descriptionFileRoot) 49 | } 50 | 51 | const resources = resolve ? 52 | getResourcesFromList(resolve.descriptionFileData, query.packagePropertyPath) : 53 | [] 54 | 55 | if (!resources.length) { 56 | return this.callback(undefined, source, sourceMap) 57 | } 58 | 59 | let resourceData = await addBundleLoader(resources, 'loaders') 60 | 61 | const isRootRequest = query.rootDir === resolve.descriptionFileRoot 62 | 63 | if (query.enableGlobbing) { 64 | resourceData = await expandAllRequiresForGlob(resourceData, this, isRootRequest ? false : resolve.descriptionFileRoot) 65 | } else { 66 | resourceData = resourceData.filter(r => !r.literal.includes(`*`)) 67 | } 68 | 69 | // log(`resourceData for ${this.resourcePath}`, resourceData.map(r => r.literal)) 70 | 71 | const resolvedResources = (await Promise.all( 72 | resourceData.map(async r => { 73 | let resource: RequireDataBaseMaybeResolved | null = null 74 | const packageName = resolve.descriptionFileData && resolve.descriptionFileData.name 75 | const tryContexts = [resolve.descriptionFileRoot, ...(query.fallbackToMainContext ? [query.rootDir] : [])] 76 | let contextDir: string | undefined 77 | let tryCount = 0 78 | const isSameModuleRequest = packageName && (r.literal.startsWith(`${packageName}/`) || r.literal === packageName) 79 | 80 | while ((!resource || !resource.resolve) && (contextDir = tryContexts.shift())) { 81 | if (!isSameModuleRequest && packageName && !path.isAbsolute(r.literal) && !isRootRequest) { 82 | const literal = `${packageName}/${r.literal}` 83 | // resolve as MODULE_NAME/REQUEST_PATH 84 | resource = await resolveLiteral(Object.assign({}, r, { literal }), this, contextDir, false) 85 | log(`[${resource && resource.resolve ? 'SUCCESS' : 'FAIL'}] [${++tryCount}] '${literal}' in '${contextDir}'`) 86 | } 87 | if (!resource || !resource.resolve) { 88 | // resolve as REQUEST_PATH 89 | resource = await resolveLiteral(r, this, contextDir, false) as RequireDataBaseMaybeResolved 90 | log(`[${resource && resource.resolve ? 'SUCCESS' : 'FAIL'}] [${++tryCount}] '${r.literal}' in '${contextDir}'`) 91 | } 92 | } 93 | if (!resource || !resource.resolve) { 94 | return this.emitWarning(`Unable to resolve ${r.literal} in context of ${packageName}`) 95 | } 96 | 97 | if (!resource.literal.startsWith('.') && (resource.resolve.descriptionFileData && resource.resolve.descriptionFileData.name) === packageName) { 98 | // we're dealing with a request from within the same package 99 | // let's make sure its relative: 100 | let relativeLiteral = path.relative(path.dirname(resolve.path), resource.resolve.path) 101 | if (!relativeLiteral.startsWith('..')) { 102 | relativeLiteral = `./${relativeLiteral}` 103 | } 104 | log(`Mapped an internal module-based literal to a relative one: ${resource.literal} => ${relativeLiteral}`) 105 | resource.literal = relativeLiteral 106 | } 107 | return resource as RequireData 108 | }) 109 | )).filter(r => !!r && r.resolve.path !== this.resourcePath) as Array 110 | 111 | log(`Adding resources to ${this.resourcePath}: ${resolvedResources.map(r => r.literal).join(', ')}`) 112 | 113 | let requireStrings = await getRequireStrings(resolvedResources, query.addLoadersCallback, this) 114 | const inject = requireStrings.map(wrapInRequireInclude).join('\n') 115 | appendCodeAndCallback(this, source, inject, sourceMap) 116 | } catch (e) { 117 | log(e) 118 | this.emitError(e.message) 119 | this.callback(undefined, source, sourceMap) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack-dependency-suite", 3 | "description": "A set of Webpack plugins, loaders and utilities designed for advanced dependency resolution", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "webpack-dev-server --inline --content-base build/", 7 | "prebuild": "rimraf ./loaders/*.js ./plugins/*.js ./utils/*.js ./loaders/*.d.ts ./plugins/*.d.ts ./utils/*.d.ts", 8 | "build": "tsc -p . -t es5 --listFiles", 9 | "webpack:build": "rimraf ./test-fixtures/webpack-dist && node --harmony_async_await ./node_modules/webpack/bin/webpack.js", 10 | "webpack": "node --harmony_async_await ./node_modules/webpack/bin/webpack.js", 11 | "test": "jest", 12 | "test:unit": "jest", 13 | "test:e2e": "jasmine test/e2e.spec.js", 14 | "debug": "node --harmony_async_await --inspect --debug-brk ./node_modules/webpack/bin/webpack.js --watch", 15 | "semantic-release": "semantic-release pre && npm publish && semantic-release post" 16 | }, 17 | "aurelia": { 18 | "_note": "this is only here for testing purposes", 19 | "build": { 20 | "resources": [ 21 | "resources/hello", 22 | { 23 | "path": "resources/double", 24 | "lazy": true, 25 | "chunk": "double" 26 | }, 27 | [ 28 | "resources/triple" 29 | ], 30 | "resources/glo*b/test-*.js", 31 | "root-most", 32 | { 33 | "path": "aurelia-templating-resources/signal-binding-behavior", 34 | "lazy": true, 35 | "chunk": "aurelia" 36 | } 37 | ] 38 | } 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/niieani/webpack-dependency-suite.git" 43 | }, 44 | "keywords": [ 45 | "webpack", 46 | "toolkit", 47 | "suite", 48 | "plugin", 49 | "loader", 50 | "require.include" 51 | ], 52 | "author": { 53 | "name": "Bazyli Brzóska", 54 | "email": "bazyli.brzoska@gmail.com" 55 | }, 56 | "license": "MIT", 57 | "jest": { 58 | "moduleFileExtensions": [ 59 | "ts", 60 | "tsx", 61 | "js" 62 | ], 63 | "transform": { 64 | "^.+\\.(ts|tsx)$": "/test/preprocessor.js" 65 | }, 66 | "testRegex": "\\.spec\\.(ts|tsx)$" 67 | }, 68 | "dependencies": { 69 | "@types/acorn": "^4.0.2", 70 | "@types/cheerio": "^0.22.1", 71 | "@types/debug": "^0.0.29", 72 | "@types/enhanced-resolve": "^3.0.3", 73 | "@types/escape-string-regexp": "^0.0.30", 74 | "@types/estree": "0.0.35", 75 | "@types/lodash": "^4.14.67", 76 | "@types/node": "^8.0.0", 77 | "@types/semver": "^5.3.32", 78 | "@types/webpack": "^3.0.0", 79 | "acorn": "^5.0.3", 80 | "cheerio": "^1.0.0-rc.1", 81 | "debug": "^3.0.0", 82 | "escape-string-regexp": "^1.0.5", 83 | "html-loader": "^0.4.5", 84 | "loader-utils": "^1.1.0", 85 | "lodash": "^4.17.4", 86 | "semver": "^5.3.0", 87 | "source-map": "^0.5.6" 88 | }, 89 | "devDependencies": { 90 | "@types/jest": "^20.0.0", 91 | "aurelia-templating-resources": "^1.4.0", 92 | "chromedriver": "^2.30.1", 93 | "enhanced-resolve": "^3.1.0", 94 | "file-loader": "^0.11.2", 95 | "html-webpack-plugin": "^2.29.0", 96 | "http-server": "^0.10.0", 97 | "jasmine": "^2.6.0", 98 | "jest": "^20.0.4", 99 | "rimraf": "^2.6.1", 100 | "selenium-webdriver": "^3.4.0", 101 | "semantic-release": "^6.3.2", 102 | "ts-node": "^3.1.0", 103 | "typescript": "^2.4.1", 104 | "webpack": "^3.0.0", 105 | "webpack-dev-server": "^2.5.0", 106 | "webpack-sources": "^1.0.1" 107 | }, 108 | "peerDependencies": { 109 | "enhanced-resolve": "^3.0.0" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /plugins/convention-invalidate-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as debug from 'debug' 2 | const log = debug('convention-invalidate-plugin') 3 | 4 | export class ConventionInvalidatePlugin { 5 | constructor(public getInvalidationList = differentExtensionTransformer) { } 6 | 7 | apply(compiler) { 8 | compiler.plugin('after-environment', () => { 9 | compiler.watchFileSystem = new TransformWatchFileSystem(compiler.watchFileSystem, compiler, this.getInvalidationList) 10 | }) 11 | } 12 | } 13 | 14 | export class TransformWatchFileSystem { 15 | constructor(public wfs, public compiler, public getInvalidationList: OnChangedTransformer) {} 16 | 17 | // getters mapping to origin: 18 | get inputFileSystem() { return this.wfs.inputFileSystem } 19 | get watcherOptions() { return this.wfs.watcherOptions } 20 | // needed for ts-loader: 21 | get watcher() { return this.wfs.watcher } 22 | 23 | watch(files, dirs, missing, startTime, options, callback: Function, callbackUndelayed: Function) { 24 | this.wfs.watch(files, dirs, missing, startTime, options, 25 | ( 26 | err: Error, 27 | filesModified: Array, 28 | dirsModified: Array, 29 | missingModified: Array, 30 | fileTimestamps: Timestamps, 31 | dirTimestamps: Timestamps) => { 32 | if (err) return callback(err) 33 | 34 | const watchedPaths = Object.keys(fileTimestamps) 35 | const pathsToInvalidate = this.getInvalidationList(filesModified, watchedPaths, this.compiler) 36 | pathsToInvalidate.forEach(filePath => { 37 | log(`Invalidating: ${filePath}`) 38 | fileTimestamps[filePath] = Date.now() 39 | filesModified.push(filePath) 40 | }) 41 | callback(err, filesModified, dirsModified, missingModified, fileTimestamps, dirTimestamps) 42 | }, 43 | (filePath: string, changeTime: number) => { 44 | const watchedFiles = this.watcher.fileWatchers.map(watcher => watcher.path) 45 | const toInvalidate = this.getInvalidationList([filePath], watchedFiles, this.compiler) 46 | toInvalidate.forEach(file => callbackUndelayed(file, changeTime)) 47 | callbackUndelayed.call(this.compiler, filePath, changeTime) 48 | }) 49 | } 50 | } 51 | 52 | /** 53 | * "touch", or invalidate all files of the same same path, but different extension 54 | */ 55 | export const differentExtensionTransformer = function differentExtensionTransformer(changedPaths, watchedFiles: string[]) { 56 | const pathsToInvalidate = [] as Array 57 | changedPaths.forEach(filePath => { 58 | const pathWithoutExtension = filePath.replace(/\.[^/.]+$/, '') 59 | const relatedFiles = watchedFiles 60 | .filter(watchedPath => watchedPath.indexOf(pathWithoutExtension) === 0 && watchedPath !== filePath) 61 | pathsToInvalidate.push(...relatedFiles) 62 | }) 63 | return pathsToInvalidate 64 | } as OnChangedTransformer 65 | 66 | export type OnChangedTransformer = (changed: Array, watchedFiles?: Array, compiler?: any) => Array 67 | export interface Timestamps { [path: string]: number } 68 | export interface WatchResult { 69 | filesModified: Array 70 | dirsModified: Array 71 | missingModified: Array 72 | fileTimestamps: Timestamps 73 | dirTimestamps: Timestamps 74 | } 75 | -------------------------------------------------------------------------------- /plugins/mapped-module-ids-plugin.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import {promisify} from 'util' 4 | import * as resolve from 'enhanced-resolve' 5 | import { 6 | LoggingCallbackWrapper, 7 | ResolveContext 8 | } from "enhanced-resolve/lib/common-types"; 9 | 10 | type ResolverInstance = { 11 | (path: string, request: string, callback: LoggingCallbackWrapper): void; 12 | (context: ResolveContext, path: string, request: string, callback: LoggingCallbackWrapper): void; 13 | } 14 | 15 | export type Prefix = string | false | ((moduleId: string) => string) 16 | export type LoaderInfo = { loader: string, prefix: Prefix } 17 | type LoaderInfoResolve = Pick & LoaderInfo 18 | type LoaderInfoError = {error: Error | null | undefined} & LoaderInfo 19 | 20 | export type DuplicateHandler = (proposedModuleId: string, module: Webpack.Core.NormalModule, modules: Webpack.Core.NormalModule[], previouslyAssigned: Map, retryCount: number) => string 21 | 22 | function resolveLoader(compiler, origin, contextPath, loaderInfo: LoaderInfo, resolver: ResolverInstance) { 23 | return new Promise((resolve, reject) => 24 | resolver(origin, contextPath, loaderInfo.loader, (error, resolvedPath, resolveObj) => 25 | (error || !resolveObj) ? (resolve({error, ...loaderInfo}) || console.error(`No loader resolved for '${loaderInfo.loader}'`)) : 26 | resolve({...resolveObj, ...loaderInfo}) 27 | ) 28 | ) 29 | } 30 | 31 | /** 32 | * Small description of how this plugin creates moduleIds: 33 | * uses module.rawRequest if it doesn't start with '.' or '!' and isn't path.isAbsolute 34 | * otherwise makes module ID relative to appDir 35 | * if necessary (see after rawRequest impl.): 36 | * cuts out '...../node_modules', in case it's nested, cut that nesting too 37 | * if the another module of the SAME name already exists, sends a WARNING 38 | * checks module.loaders[x].loader (that's a path) for loaders that need prefixing 39 | * then name looks e.g. like: 'async!whatever/lalala' 40 | * compares pure path with rawRequest and optionally LOGs if different 41 | * 42 | * to use in a dynamic loader test: if ('async!my-thing' in __webpack_require__.m) 43 | * then based on existence: handle e.g. __webpack_require__('async!my-thing') 44 | * 45 | * run optional path convertion methods (moduleId) => string 46 | * e.g. to strip .../dist/native-modules/... 47 | */ 48 | export class MappedModuleIdsPlugin { 49 | constructor (public options: { 50 | appDir: string 51 | prefixLoaders: Array 52 | dotSlashWhenRelativeToAppDir?: boolean 53 | beforeLoadersTransform?: (currentModuleId: string, module?: Webpack.Core.NormalModule) => string 54 | afterLoadersTransform?: (currentModuleId: string, module?: Webpack.Core.NormalModule) => string 55 | afterExtensionTrimmingTransform?: (currentModuleId: string, module?: Webpack.Core.NormalModule) => string 56 | keepAllExtensions?: boolean 57 | logWhenRawRequestDiffers?: boolean 58 | warnOnNestedSubmodules?: boolean 59 | /** 60 | * RegExp or function, return true if you want to ignore the module 61 | */ 62 | ignore?: RegExp | ((module: Webpack.Core.NormalModule) => boolean) 63 | duplicateHandler?: DuplicateHandler 64 | errorOnDuplicates?: boolean 65 | useManualResolve?: boolean | 'node-fs' // uses node's filesystem instead of Webpack's builtin 66 | }) { 67 | const ignore = options.ignore 68 | if (ignore) { 69 | this.ignoreMethod = typeof ignore === 'function' ? ignore : (module) => { 70 | return ignore.test(module.rawRequest) 71 | } 72 | } 73 | } 74 | 75 | ignoreMethod: ((module: Webpack.Core.NormalModule) => boolean) | undefined 76 | 77 | apply(compiler) { 78 | const {options} = this 79 | if (!options.appDir) { 80 | options.appDir = compiler.options.context 81 | } 82 | 83 | let resolvedLoaders = [] as Array 84 | const fileSystem = options.useManualResolve && options.useManualResolve !== 'node-fs' && (compiler.inputFileSystem as typeof fs) || (require('fs') as typeof fs) 85 | const resolver = options.useManualResolve ? resolve.create({fileSystem, ...compiler.options.resolveLoader}) : undefined 86 | 87 | const beforeRunStep = async (compilingOrWatching, callback) => { 88 | if (resolvedLoaders.length) { 89 | // cached from previous resolve 90 | return callback() 91 | } 92 | const webpackLoaderResolver = compiler.resolvers.loader.resolve.bind(compiler.resolvers.loader) as ResolverInstance 93 | const resolved = await Promise.all(options.prefixLoaders.map( 94 | (loaderName) => resolveLoader(compiler, {}, compiler.options.context, loaderName, resolver || webpackLoaderResolver) 95 | )) 96 | resolvedLoaders = resolved.filter((r: LoaderInfoError) => !r.error) as Array 97 | return callback() 98 | } 99 | 100 | compiler.plugin('run', beforeRunStep) 101 | compiler.plugin('watch-run', beforeRunStep) 102 | 103 | compiler.plugin('compilation', (compilation) => { 104 | const previouslyAssigned = new Map() 105 | 106 | compilation.plugin('before-module-ids', (modules: Array) => { 107 | modules.forEach((module) => { 108 | if (module.userRequest && module.rawRequest && module.id === null && (!this.ignoreMethod || !this.ignoreMethod(module))) { 109 | const userRequest = module.userRequest || '' 110 | const rawRequest = module.rawRequest || '' 111 | const requestSep = userRequest.split('!') 112 | const loadersUsed = requestSep.length > 1 113 | const userRequestLoaders = requestSep.slice(0, requestSep.length - 1) 114 | const userRequestLoaderPaths = userRequestLoaders.map(name => { 115 | const queryStart = name.indexOf('?') 116 | return (queryStart > -1) ? name.substring(0, queryStart) : name 117 | }) 118 | 119 | const requestedFilePath = requestSep[requestSep.length - 1] 120 | let moduleId = path.relative(options.appDir, requestedFilePath) 121 | if (path.sep === '\\') 122 | moduleId = moduleId.replace(/\\/g, '/') 123 | 124 | const lastMentionOfNodeModules = moduleId.lastIndexOf('node_modules') 125 | if (lastMentionOfNodeModules >= 0) { 126 | const firstMentionOfNodeModules = moduleId.indexOf('node_modules') 127 | if (options.warnOnNestedSubmodules && firstMentionOfNodeModules != lastMentionOfNodeModules) { 128 | console.warn(`Path is a nested node_modules`) 129 | } 130 | // cut out node_modules 131 | moduleId = moduleId.slice(lastMentionOfNodeModules + 'node_modules'.length + 1) 132 | } else if (options.dotSlashWhenRelativeToAppDir) { 133 | moduleId = `./${moduleId}` 134 | } 135 | 136 | if (options.beforeLoadersTransform) { 137 | moduleId = options.beforeLoadersTransform(moduleId, module) 138 | } 139 | 140 | const rawRequestSplit = rawRequest.split(`!`) 141 | const rawRequestPath = rawRequestSplit[rawRequestSplit.length - 1] 142 | const rawRequestPathParts = rawRequestPath.split(`/`) 143 | 144 | if (!path.isAbsolute(rawRequestPath) && !rawRequestPath.startsWith(`.`) && 145 | (rawRequestPathParts.length === 1 || 146 | (rawRequestPathParts.length === 2 && rawRequestPathParts[0].startsWith(`@`))) 147 | ) { 148 | // we're guessing that this is a call to the package.json/main field 149 | // we want to keep the module name WITHOUT the full path, so lets try naming this with the request 150 | moduleId = rawRequestPath 151 | } 152 | 153 | let loadersAdded = 0 154 | module.loaders.forEach(loader => { 155 | const resolved = resolvedLoaders.find(l => l.path === loader.loader) 156 | const wasInUserRequest = userRequestLoaderPaths.find(loaderPath => loaderPath === loader.loader) 157 | if (!resolved || resolved.prefix === '' || resolved.prefix === undefined) { 158 | if (wasInUserRequest) { 159 | console.warn( 160 | `Warning: Keeping '${rawRequest}' without the loader prefix '${loader.loader}'.` + '\n' + 161 | `Explicitly silence these warnings by defining the loader in MappedModuleIdsPlugin configuration`) 162 | } 163 | return 164 | } 165 | // actively supress prefixing when false 166 | if (resolved.prefix === false) return 167 | if (typeof resolved.prefix === 'function') { 168 | moduleId = resolved.prefix(moduleId) 169 | } else { 170 | moduleId = `${resolved.prefix}!${moduleId}` 171 | } 172 | loadersAdded++ 173 | }) 174 | 175 | if (options.afterLoadersTransform) { 176 | moduleId = options.afterLoadersTransform(moduleId, module) 177 | } 178 | 179 | if (!options.keepAllExtensions) { 180 | const trimExtensions = compiler.options.resolve.extensions as Array 181 | trimExtensions.forEach(ext => { 182 | if (moduleId.endsWith(ext)) { 183 | moduleId = moduleId.slice(0, moduleId.length - ext.length) 184 | } 185 | }) 186 | } 187 | 188 | if (options.afterExtensionTrimmingTransform) { 189 | moduleId = options.afterExtensionTrimmingTransform(moduleId, module) 190 | } 191 | 192 | const proposedModuleIdSplit = moduleId.split(`!`) 193 | const proposedModuleIdPath = proposedModuleIdSplit[proposedModuleIdSplit.length - 1] 194 | 195 | if (options.logWhenRawRequestDiffers && !rawRequestPath.startsWith(`.`) && (proposedModuleIdPath !== rawRequestPath)) { // (!loadersAdded && (moduleId !== module.rawRequest) || ...) 196 | console.info(`Raw Request Path (${rawRequestPath}) differs from the generated ID (${proposedModuleIdPath})`) 197 | } 198 | 199 | let retryCount = 0 200 | while (previouslyAssigned.has(moduleId)) { 201 | const { 202 | duplicateHandler = ((moduleId, module, modules, previouslyAssigned, retryCount) => { 203 | if (options.errorOnDuplicates) { 204 | console.error(`Error: Multiple modules with the same ID: '${moduleId}'`) 205 | } 206 | return `${moduleId}#${retryCount}` 207 | }) as DuplicateHandler 208 | } = options 209 | 210 | moduleId = duplicateHandler(moduleId, module, modules, previouslyAssigned, retryCount) 211 | retryCount++ 212 | } 213 | 214 | previouslyAssigned.set(moduleId, module) 215 | module.id = moduleId 216 | } 217 | }) 218 | }) 219 | }) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /plugins/rewrite-module-subdirectory-plugin.ts: -------------------------------------------------------------------------------- 1 | import { splitRequest } from '../utils/inject' 2 | import * as path from 'path' 3 | import * as debug from 'debug' 4 | const log = debug('rewrite-subdir-plugin') 5 | 6 | /** 7 | * Webpack Resolve plugin, used to check in additional places for the root directory of a given module 8 | */ 9 | export class RewriteModuleSubdirectoryPlugin { 10 | constructor(public getIndexPath: (moduleName: string, remainingRequest: string, request: any) => string) {} 11 | 12 | apply(resolver) { 13 | const getIndexPath = this.getIndexPath 14 | resolver.plugin('raw-module', async (request, callback) => { 15 | if (path.isAbsolute(request.request)) 16 | return callback() 17 | 18 | const { moduleName, remainingRequest } = await splitRequest(request.request) 19 | if (!moduleName) 20 | return callback() 21 | const newRequest = getIndexPath(moduleName, remainingRequest, request) 22 | if (!newRequest) return callback() 23 | log(`${request.request} => ${newRequest}`) 24 | const obj = Object.assign({}, request, { 25 | request: newRequest 26 | }) 27 | resolver.doResolve('module', obj, `looking for modules in ${newRequest}`, callback, true) 28 | }) 29 | } 30 | } 31 | 32 | 33 | // class DynamicMainPlugin { 34 | // constructor(public getIndexPath: (request) => string) {} 35 | 36 | // apply(resolver) { 37 | // const getIndexPath = this.getIndexPath 38 | // // "existing-directory", item, "resolve" 39 | // resolver.plugin("existing-directory", (request, callback) => { 40 | // // if (request.path !== request.descriptionFileRoot) return callback(); 41 | // const filename = getIndexPath(request) 42 | // if (!filename) return callback() 43 | // const fs = resolver.fileSystem; 44 | // const topLevelCallback = callback; 45 | // const filePath = resolver.join(request.path, filename); 46 | // const obj = Object.assign({}, request, { 47 | // path: filePath, 48 | // relativePath: request.relativePath && resolver.join(request.relativePath, filename) 49 | // }); 50 | // resolver.doResolve("undescribed-raw-file", obj, `using path: ${filePath}`, callback); 51 | // }); 52 | // } 53 | // } 54 | 55 | // export = DynamicMainPlugin 56 | -------------------------------------------------------------------------------- /plugins/root-most-resolve-plugin.ts: -------------------------------------------------------------------------------- 1 | import {get} from 'lodash' 2 | import createInnerCallback = require('enhanced-resolve/lib/createInnerCallback') 3 | import * as getInnerRequest from 'enhanced-resolve/lib/getInnerRequest' 4 | import * as semver from 'semver' 5 | import * as path from 'path' 6 | import * as debug from 'debug' 7 | const log = debug('root-most-resolve-plugin') 8 | 9 | function getDependencyVersion(packageJson: Object, packageName: string): string { 10 | return get(packageJson, ['dependencies', packageName]) || 11 | get(packageJson, ['devDependencies', packageName]) || 12 | get(packageJson, ['optionalDependencies', packageName]) || 13 | get(packageJson, ['peerDependencies', packageName]) 14 | } 15 | 16 | /** 17 | * @description Uses the root-most package instead of a nested node_modules package. 18 | * Useful when doing 'npm link' for nested dependencies, 19 | * so you can be sure all packages use the right copy of the given module. 20 | */ 21 | export class RootMostResolvePlugin { 22 | constructor(public context: string, public force?: boolean, public overwriteInvalidSemVer = true) {} 23 | 24 | apply(resolver: EnhancedResolve.Resolver) { 25 | let context = this.context 26 | let force = this.force 27 | let overwriteInvalidSemVer = this.overwriteInvalidSemVer 28 | 29 | resolver.plugin('resolved', async function (originalResolved: EnhancedResolve.ResolveResult, callback) { 30 | if (originalResolved.context['rootMostResolve']) { 31 | // do not loop! 32 | return callback(null, originalResolved) 33 | } 34 | 35 | const previousPathSep = originalResolved.path.split(path.sep) 36 | const nodeModulesCount = previousPathSep.filter(p => p === 'node_modules').length 37 | const relativeToContext = path.relative(context, originalResolved.path) 38 | if (!force && !relativeToContext.includes(`..`) && nodeModulesCount <= 1) { 39 | return callback(null, originalResolved) 40 | } 41 | const lastNodeModulesAt = previousPathSep.lastIndexOf('node_modules') 42 | const actualRequestPath = previousPathSep.slice(lastNodeModulesAt + 1).join('/') 43 | 44 | if (!originalResolved.context || !originalResolved.context.issuer) { 45 | return callback(null, originalResolved) 46 | } 47 | 48 | const issuer = await new Promise((resolve, reject) => 49 | resolver.doResolve('resolve', 50 | { context: { rootMostResolve: true }, path: originalResolved.context.issuer, request: originalResolved.context.issuer }, `resolve issuer of ${originalResolved.path}`, (err, value) => err ? resolve() : resolve(value))); 51 | 52 | if (!issuer) { 53 | return callback(null, originalResolved) 54 | } 55 | 56 | const resolvedInParentContext = await new Promise((resolve, reject) => 57 | resolver.doResolve('resolve', { 58 | context: {}, // originalResolved.context, 59 | path: context, 60 | request: actualRequestPath 61 | }, `resolve ${actualRequestPath} in ${context}`, createInnerCallback((err, value) => err ? resolve() : resolve(value), callback, null))); 62 | 63 | if (!resolvedInParentContext) { 64 | return callback(null, originalResolved) 65 | } 66 | 67 | const resolvedVersion = resolvedInParentContext.descriptionFileData && resolvedInParentContext.descriptionFileData.version 68 | const packageName = resolvedInParentContext.descriptionFileData && resolvedInParentContext.descriptionFileData.name 69 | const allowedRange = getDependencyVersion(issuer.descriptionFileData, packageName) 70 | const isValidRange = allowedRange && semver.validRange(allowedRange) 71 | 72 | log(`Analyzing whether package ${packageName}@${allowedRange} can be substituted by a parent version ${resolvedVersion}`) 73 | 74 | if (!isValidRange) 75 | log(`Package ${packageName} has an invalid SemVer range, ${overwriteInvalidSemVer ? 'overwriting anyway' : 'not overwriting'}`) 76 | 77 | if (resolvedVersion && packageName && allowedRange && ((!isValidRange && overwriteInvalidSemVer) || semver.satisfies(resolvedVersion, allowedRange, true))) { 78 | log(`Rewriting ${relativeToContext} with ${actualRequestPath}`) 79 | return callback(null, resolvedInParentContext) 80 | } else { 81 | return callback(null, originalResolved) 82 | } 83 | }) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # NO LONGER MAINTAINED. PLEASE DO NOT USE OR FORK. 2 | 3 | # Webpack Dependency Suite 4 | 5 | [![Greenkeeper badge](https://badges.greenkeeper.io/niieani/webpack-dependency-suite.svg)](https://greenkeeper.io/) 6 | A set of loaders, plugins and utilities designed to help with adding custom dependencies to your project. 7 | 8 | ## Usage 9 | 10 | TODO. 11 | 12 | ### Plugins 13 | 14 | #### ConventionInvalidatePlugin 15 | 16 | This plugin allows customisation of the modules that are invalidated as a result 17 | of a change to another module. 18 | 19 | By default it will invalidate all modules that share the name as a changed 20 | module except for the extension, e.g. if `./index.js` was changed and 21 | `./index.html` was watched, then `./index.html` would be invalidated and 22 | rebuild. 23 | 24 | It is possible to pass in a function to implement a custom invalidation rule 25 | instead; the function will be given two arguments, an array of changed modules 26 | and an array of all watched modules and should return an array of _additional_ 27 | modules which should be invalidated (which should not generally include the 28 | changed modules, as they will already be rebuild). 29 | 30 | The plugin is used by adding it to the [webpack config 31 | plugins](https://webpack.js.org/concepts/plugins/#configuration), e.g. 32 | 33 | ```javascript 34 | const { ConventionInvalidatePlugin } = require('webpack-dependency-suite'); 35 | 36 | const config = { 37 | // rest of the config somewhere in here 38 | plugins: { 39 | // Default behaviour 40 | new ConventionInvalidatePlugin(), 41 | 42 | // Customised behaviour 43 | new ConventionInvalidatePlugin((changed, watched) => { 44 | return [/* list of additional invalidated modules */]; 45 | }), 46 | }, 47 | }; 48 | ``` 49 | 50 | #### Other Plugins 51 | 52 | TODO 53 | 54 | ## Parts of the Suite 55 | 56 | ### `require.include` loaders 57 | 58 | These are a bit like [baggage-loader](https://github.com/deepsweet/baggage-loader) but more configurable and advanced. 59 | 60 | - comment-include-loader: 61 | ```js 62 | /* @import */ 'module' 63 | /* @import @lazy @ */ 'module' 64 | /* @import('thing\/*\/also\/works') @lazy @ */ 'module' // <<- globs will not work in comments cause of /**/ unless you escape slashes 65 | ``` 66 | - conventional-include-loader (include related files according to passed in function(fs)) [eg. like-named require loader for .html files] 67 | - template require loader 68 | (and others - configurable?) 69 | ${} globbing by: 70 | - splitting path by '/' 71 | - find first component where * is 72 | - resolve previous one || contextDir 73 | - get all files recursively 74 | - split their paths '/' 75 | - add all that match the regex 76 | - explicit loader: 77 | adds all dependencies listed in a JSON file to a given, individual file (entry?) 78 | expose a method to check if a path should override/add loaders by query configuration 79 | - note: globbed paths MUST include extensions 80 | 81 | ### Resolve Plugins 82 | 83 | - resolve plugin for trying nested directories auto-resolve stuff (e.g. Aurelia's `/dist/es2015`) 84 | - resolve plugin to use root module from node_modules if version range satisfied 85 | 86 | ### Normal Use Plugins 87 | 88 | - mapped relative moduleId plugin 89 | sets ModuleID: 90 | - use relative to any of config.modules (node_modules, app) 91 | - no JS extensions 92 | - rewrite paths for aurelia (strip /dist/node_modules/) 93 | - strip nested node_modules/.../node_modules 94 | - just do: package_name/request 95 | - for /index do package_name 96 | - name loader-based modules with a prefix: LOADER!NAME 97 | - aurelia loader checks cache for normal module name, then for async!NAME 98 | sets module.id relative to configured directory 99 | optionally keeps extension (.js .ts) 100 | 101 | ## Development / Debugging 102 | There are two scripts that are setup already: 103 | 104 | * `npm run dev` 105 | * will run the same configuration instead with webpack-dev-server for live reload 106 | 107 | * `npm run build` 108 | * will simply execute a webpack build in the repo 109 | 110 | * `npm run debug` 111 | * will run the same build with node debugger. 112 | * paste provided link in Chrome (or Canary), and you will have the super incredible ChromeDevTools to step through your code for learning, exploration, and debugging. 113 | 114 | ## Helpful resources: 115 | * [How to write a webpack loader](https://webpack.github.io/docs/how-to-write-a-loader.html) 116 | * [How to write a plugin](https://github.com/webpack/docs/wiki/How-to-write-a-plugin) 117 | * [Webpack Plugin API](https://webpack.github.io/docs/plugins.html) 118 | * [webpack-sources](https://github.com/webpack/webpack-sources) 119 | * [enhanced-resolve](https://github.com/webpack/enhanced-resolve) 120 | 121 | ## Recognition 122 | The repository is based on the fantastic [webpack-developer-kit](https://github.com/TheLarkInn/webpack-developer-kit) by TheLarkInn, inspired by blacksonics. 123 | -------------------------------------------------------------------------------- /temp/tmp.ts: -------------------------------------------------------------------------------- 1 | // import { WebpackConfig } from '@easy-webpack/core'; 2 | 3 | import * as webpack from 'webpack' 4 | import * as path from 'path' 5 | import * as acorn from 'acorn' 6 | import * as walk from 'acorn/dist/walk' 7 | import * as debugPkg from 'debug' 8 | 9 | const debug = debugPkg('custom-plugin') 10 | 11 | class CustomPlugin { 12 | apply(compiler) { 13 | compiler.plugin('context-module-factory', function (cmf) { 14 | debug('context-module-factory') 15 | 16 | cmf.plugin('before-resolve', (result, callback) => { 17 | debug('cmf before-resolve') 18 | return callback(undefined, result) 19 | }) 20 | cmf.plugin('after-resolve', (result, callback) => { 21 | debug('cmf after-resolve') 22 | if (!result) return callback() 23 | return callback(null, result) 24 | }) 25 | }) 26 | 27 | compiler.plugin('compilation', function(compilation, data) { 28 | debug('compilation') 29 | // debug('compilation', compilation, data) 30 | compilation.plugin('finish-modules', function(modules) { 31 | debug('finish-modules', modules) 32 | }) 33 | // compilation.plugin('normal-module-loader', function(loaderContext, module) { 34 | // debug('normal-module-loader', module) 35 | // }) 36 | data.normalModuleFactory.plugin('parser', function(parser, options) { 37 | parser.plugin('program', function(ast, comments) { 38 | // debug('program ast', ast) 39 | // debug('program body', ast.body[2].expression.arguments[0]) 40 | // debug('program comments', comments) 41 | comments 42 | .filter(comment => comment.type === 'Block' && comment.value.trim() === 'import') 43 | .forEach(comment => { 44 | let result = walk.findNodeAfter(ast, comment.end) 45 | if (result.node && result.node.type === 'Literal') { 46 | debug('found', result.node.value) 47 | } 48 | }) 49 | // debug('this', this) 50 | // this.state.current / module 51 | }) 52 | }) 53 | }) 54 | 55 | // compiler.parser.plugin("evaluate Literal", function (expr) { 56 | // debug('literal', expr) 57 | // //if you original module has 'var rewrite' 58 | // //you now have a handle on the expresssion object 59 | // return true 60 | // }) 61 | /* 62 | compiler.plugin('normal-module-factory', function (nmf) { 63 | nmf.plugin('before-resolve', (result, callback) => { 64 | debug('nmf before-resolve') 65 | return callback(undefined, result) 66 | }) 67 | nmf.plugin('after-resolve', (result, callback) => { 68 | debug('nmf after-resolve') 69 | if (!result) return callback() 70 | return callback(null, result) 71 | }) 72 | }) 73 | */ 74 | } 75 | } 76 | 77 | export = function(environment: string) { 78 | console.log(environment) 79 | return { 80 | entry: path.resolve('test/index'), 81 | output: { 82 | filename: 'test/output/index.[name].js', 83 | devtoolModuleFilenameTemplate: '[resource-path]' 84 | }, 85 | plugins: [ 86 | new CustomPlugin() 87 | ], 88 | devtool: 'source-map', 89 | // resolve: { 90 | 91 | // }, 92 | module: { 93 | rules: [ 94 | { 95 | test: /\.js$/, 96 | include: [path.resolve('test')], 97 | exclude: [path.resolve('test/output')], 98 | // include: [path.resolve('test/included')], 99 | loaders: ['./explicit-loader'] 100 | } 101 | ] 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /temp/useful.ts: -------------------------------------------------------------------------------- 1 | 2 | // this._module.id = 'something-else' 3 | -------------------------------------------------------------------------------- /test-fixtures/app-extra/extra.js: -------------------------------------------------------------------------------- 1 | console.log(`Extra!`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app-extra/sub/extra-sub.js: -------------------------------------------------------------------------------- 1 | console.log(`I'm mr Extra Sub!`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app-extra/sub/hello.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niieani/webpack-dependency-suite/c2dbf343d8f52b733f496c869e5e98b700d8e800/test-fixtures/app-extra/sub/hello.html -------------------------------------------------------------------------------- /test-fixtures/app-extra/sub/hello.js: -------------------------------------------------------------------------------- 1 | console.log('app-extra/sub/hello!') 2 | -------------------------------------------------------------------------------- /test-fixtures/app/app.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /test-fixtures/app/app.js: -------------------------------------------------------------------------------- 1 | export class App { 2 | configureRouter(config, router) { 3 | config.title = 'Aurelia'; 4 | config.map([ 5 | { route: ['', 'welcome'], name: 'welcome', moduleId: /* @import */ './welcome', nav: true, title: 'Welcome' }, 6 | { route: 'car', name: 'car', moduleId: /* @import */ 'car', nav: true, title: 'Car' }, 7 | { route: 'double', name: 'double', moduleId: /* @import */ "sub/double", nav: true, title: 'double' } 8 | ]); 9 | 10 | this.router = router; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test-fixtures/app/car.js: -------------------------------------------------------------------------------- 1 | import { V8Engine } from './engine'; 2 | 3 | export class SportsCar { 4 | constructor(engine) { 5 | this.engine = engine; 6 | } 7 | 8 | toString() { 9 | return this.engine.toString() + ' Sports Car'; 10 | } 11 | } 12 | 13 | console.log( 14 | new SportsCar(new V8Engine()).toString() 15 | ); 16 | -------------------------------------------------------------------------------- /test-fixtures/app/car.spec.js: -------------------------------------------------------------------------------- 1 | import { V6Engine, V8Engine, getVersion } from './engine'; 2 | import { SportsCar } from './car'; 3 | 4 | describe('Car - ',() => { 5 | it('should have a V8 Engine',() => { 6 | let car = new SportsCar(new V8Engine()); 7 | expect(car.toString()).toBe('V8 Sports Car'); 8 | }); 9 | 10 | it('should have a V6 Engine',() => { 11 | let car = new SportsCar(new V6Engine()); 12 | expect(car.toString()).toBe('V6 Sports Car'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test-fixtures/app/engine.js: -------------------------------------------------------------------------------- 1 | export class V6Engine { 2 | toString() { 3 | return 'V6'; 4 | } 5 | } 6 | 7 | export class V8Engine { 8 | toString() { 9 | return 'V8'; 10 | } 11 | } 12 | 13 | export function getVersion() { 14 | return '1.0'; 15 | } 16 | -------------------------------------------------------------------------------- /test-fixtures/app/engine.spec.js: -------------------------------------------------------------------------------- 1 | import { V6Engine, V8Engine, getVersion } from './engine'; 2 | 3 | describe('Engine - ',() => { 4 | it('should have a v6 engine', () => { 5 | let v6 = new V6Engine(); 6 | expect(v6.toString()).toBe('V6'); 7 | }); 8 | 9 | it('should have a v8 engine', () => { 10 | let v8 = new V8Engine(); 11 | expect(v8.toString()).toBe('V8'); 12 | }); 13 | 14 | it('should get version', () => { 15 | expect(getVersion()).toBe('1.0'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /test-fixtures/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Webpack Developer Kit 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test-fixtures/app/main.js: -------------------------------------------------------------------------------- 1 | export function configure(aurelia) { 2 | aurelia.use 3 | .standardConfiguration() 4 | .developmentLogging(); 5 | 6 | aurelia.start().then(aurelia.setRoot(/* @import */ 'app')); 7 | } 8 | 9 | // const context = require.context('./resources', true, /\.(ts|js)/); 10 | // console.log(context.keys()) 11 | -------------------------------------------------------------------------------- /test-fixtures/app/nav-bar.html: -------------------------------------------------------------------------------- 1 | 31 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/double.js: -------------------------------------------------------------------------------- 1 | console.log(`I'm mr Double!`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/glooob/subdir/test-c.js: -------------------------------------------------------------------------------- 1 | console.log(`HelloC`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/glooob/test-a.js: -------------------------------------------------------------------------------- 1 | console.log(`HelloA`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/glooob/test-b.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niieani/webpack-dependency-suite/c2dbf343d8f52b733f496c869e5e98b700d8e800/test-fixtures/app/resources/glooob/test-b.html -------------------------------------------------------------------------------- /test-fixtures/app/resources/glooob/test-b.js: -------------------------------------------------------------------------------- 1 | console.log(`HelloB`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/hello.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niieani/webpack-dependency-suite/c2dbf343d8f52b733f496c869e5e98b700d8e800/test-fixtures/app/resources/hello.html -------------------------------------------------------------------------------- /test-fixtures/app/resources/hello.js: -------------------------------------------------------------------------------- 1 | console.log('sub/hello!') 2 | -------------------------------------------------------------------------------- /test-fixtures/app/resources/triple.js: -------------------------------------------------------------------------------- 1 | console.log(`I'm mr Triple!`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/root-most.js: -------------------------------------------------------------------------------- 1 | // require('aurelia-templating-resources') 2 | // require('aurelia-templating-resources/css-resource') 3 | require('aurelia-framework') 4 | -------------------------------------------------------------------------------- /test-fixtures/app/sub/double.js: -------------------------------------------------------------------------------- 1 | console.log(`I'm mr Double!`) 2 | -------------------------------------------------------------------------------- /test-fixtures/app/sub/hello.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/niieani/webpack-dependency-suite/c2dbf343d8f52b733f496c869e5e98b700d8e800/test-fixtures/app/sub/hello.html -------------------------------------------------------------------------------- /test-fixtures/app/sub/hello.js: -------------------------------------------------------------------------------- 1 | console.log('sub/hello!') 2 | -------------------------------------------------------------------------------- /test-fixtures/app/welcome.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /test-fixtures/app/welcome.js: -------------------------------------------------------------------------------- 1 | //import {computedFrom} from 'aurelia-framework'; 2 | require('aurelia-templating-resources/css-resource') 3 | 4 | export class Welcome { 5 | constructor() { 6 | this.heading = 'Welcome to the Aurelia Navigation App!'; 7 | this.firstName = 'John'; 8 | this.lastName = 'Doe'; 9 | this.previousValue = this.fullName; 10 | } 11 | 12 | //Getters can't be directly observed, so they must be dirty checked. 13 | //However, if you tell Aurelia the dependencies, it no longer needs to dirty check the property. 14 | //To optimize by declaring the properties that this getter is computed from, uncomment the line below 15 | //as well as the corresponding import above. 16 | //@computedFrom('firstName', 'lastName') 17 | get fullName() { 18 | return `${this.firstName} ${this.lastName}`; 19 | } 20 | 21 | submit() { 22 | this.previousValue = this.fullName; 23 | alert(`Welcome, ${this.fullName}!`); 24 | } 25 | 26 | canDeactivate() { 27 | if (this.fullName !== this.previousValue) { 28 | return confirm('Are you sure you want to leave?'); 29 | } 30 | } 31 | } 32 | 33 | export class UpperValueConverter { 34 | toView(value) { 35 | return value && value.toUpperCase(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/e2e.spec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | // const assert = require('yeoman-assert'); 3 | const child = require('child_process'); 4 | const webdriver = require('selenium-webdriver'); 5 | const cheerio = require('cheerio'); 6 | 7 | const driver = new webdriver.Builder() 8 | .forBrowser('chrome') 9 | .build(); 10 | 11 | const url = 'http://localhost:8080'; 12 | 13 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 15000; //increase timeout to allow webpack finish its thing 14 | 15 | let $; 16 | let npmTask; 17 | 18 | describe('Webpack Dev Kit - Dev Script', () => { 19 | beforeAll((done) => { 20 | //start the npm script 21 | npmTask = child.spawn('npm', ['run', 'dev']); 22 | let run = false; //make sure to only call done once 23 | npmTask.stdout.on('data', (data) => { 24 | //search for 'bundle valid' string to make sure it's finished running 25 | let str = data.toString(); 26 | if (str.indexOf('webpack: bundle is now VALID.') !== -1) { 27 | if (!run) { 28 | run = true; 29 | driver.get(url); 30 | driver.getPageSource() 31 | .then(page => { 32 | $ = cheerio.load(page); 33 | done(); 34 | }); 35 | } 36 | } 37 | }); 38 | }); 39 | 40 | afterAll((done) => { 41 | driver.quit().then(() => { 42 | // make sure to kill npm child process 43 | // otherwise it will keep running 44 | npmTask.kill(); 45 | done(); 46 | }); 47 | }); 48 | 49 | it('should have the title "Webpack Developer Kit"', (done) => { 50 | expect($('title').text()).toBe('Webpack Developer Kit'); 51 | done(); 52 | }); 53 | 54 | it('should have a car.bundle.js', (done) => { 55 | expect($('script').attr('src')).toBe('car.bundle.js'); 56 | done(); 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /test/preprocessor.js: -------------------------------------------------------------------------------- 1 | const tsc = require('typescript'); 2 | const tsConfig = require('../tsconfig.json'); 3 | 4 | module.exports = { 5 | process(src, path) { 6 | if (path.endsWith('.ts') || path.endsWith('.tsx')) { 7 | return tsc.transpile( 8 | src, 9 | Object.assign(tsConfig.compilerOptions, { target: 'es5' }), 10 | path, 11 | [] 12 | ); 13 | } 14 | return src; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2017", 5 | "noImplicitAny": false, 6 | "inlineSourceMap": false, 7 | "sourceMap": true, 8 | "declaration": true, 9 | "strict": true, 10 | "lib": [ 11 | "dom", "esnext" 12 | ] 13 | }, 14 | "exclude": [ 15 | ".idea", 16 | ".vscode", 17 | "node_modules", 18 | "temp", 19 | "test-fixtures" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /typings/definitions.d.ts: -------------------------------------------------------------------------------- 1 | import * as SourceMap from 'source-map' 2 | import * as fs from 'fs' 3 | import * as Webpack from '../custom_typings/webpack' 4 | 5 | export interface CommentLoaderOptions extends AddLoadersOptions { 6 | alwaysUseCommentBundles?: boolean 7 | enableGlobbing?: boolean 8 | } 9 | 10 | export type ConventionFunction = (fullPath: string, query?: ConventionOptions, loaderInstance?: Webpack.Core.LoaderContext) => string | string[] | Promise 11 | export type Convention = 'extension-swap' | ConventionFunction 12 | 13 | export interface ConventionOptions extends AddLoadersOptions { 14 | convention: Convention | Array 15 | extension?: string | string[] 16 | [customSetting: string]: any 17 | } 18 | 19 | export type SelectorAndAttribute = { selector: string, attribute: string } 20 | 21 | export interface HtmlRequireOptions extends AddLoadersOptions { 22 | selectorsAndAttributes?: Array 23 | globReplaceRegex?: RegExp | undefined 24 | enableGlobbing?: boolean 25 | } 26 | 27 | export interface ListBasedRequireOptions extends AddLoadersOptions { 28 | packagePropertyPath: string 29 | // recursiveProcessing?: boolean | undefined 30 | // processDependencies?: boolean | undefined 31 | enableGlobbing?: boolean 32 | rootDir?: string 33 | /** 34 | * Useful setting to true when using linked modules 35 | */ 36 | fallbackToMainContext?: boolean 37 | 38 | /** 39 | * only add dependencies to the FIRST file of the given compilation, per each module 40 | * TODO: add cache for when this is false (otherwise it can get really slow!) 41 | */ 42 | requireInFirstFileOnly?: boolean 43 | } 44 | 45 | export interface PathWithLoaders { 46 | path: string 47 | /** 48 | * strings of loaders with their queries without the '!' 49 | * (if want to cancel out all previous loaders, use '!!' at the beginning) 50 | */ 51 | loaders?: Array | undefined 52 | } 53 | 54 | export type AddLoadersMethod = (files: Array, loaderInstance?: Webpack.Core.LoaderContext) => Array | Promise> 55 | 56 | export interface RequireData extends RequireDataBaseResolved { 57 | loaders?: Array | undefined 58 | fallbackLoaders?: Array | undefined 59 | } 60 | export interface RequireDataBaseResolved extends RequireDataBase { 61 | resolve: EnhancedResolve.ResolveResult 62 | } 63 | export interface RequireDataBaseMaybeResolved extends RequireDataBase { 64 | resolve: EnhancedResolve.ResolveResult | undefined 65 | } 66 | export interface RequireDataBase { 67 | literal: string 68 | lazy: boolean 69 | chunk?: string 70 | } 71 | 72 | export interface AddLoadersOptions { 73 | addLoadersCallback?: AddLoadersMethod 74 | [customSetting: string]: any 75 | } 76 | -------------------------------------------------------------------------------- /typings/promisify.d.ts: -------------------------------------------------------------------------------- 1 | import * as util from 'util'; 2 | 3 | interface NodeCallback { 4 | (err: any, result?: T): void; 5 | } 6 | interface NodeCallback2 { 7 | (result: T): void; 8 | } 9 | 10 | declare module "util" { 11 | export function promisify(f: (callback?: NodeCallback) => void): () => Promise; 12 | export function promisify(f: (arg1: S, callback: NodeCallback) => void): (arg1: S) => Promise; 13 | export function promisify(f: (arg1: S, arg2: U, callback: NodeCallback) => void): (arg1: S, arg2: U) => Promise; 14 | export function promisify(f: (arg1: S, arg2: U, arg3: W, callback: NodeCallback) => void): (arg1: S, arg2: U, arg3: W) => Promise; 15 | export function promisify(f: (callback: NodeCallback2) => void): () => Promise; 16 | export function promisify(f: (arg1: S, callback: NodeCallback2) => void): (arg1: S) => Promise; 17 | export function promisify(f: (arg1: S, arg2: U, callback: NodeCallback2) => void): (arg1: S, arg2: U) => Promise; 18 | export function promisify(f: (arg1: S, arg2: U, arg3: W, callback: NodeCallback2) => void): (arg1: S, arg2: U, arg3: W) => Promise; 19 | } 20 | -------------------------------------------------------------------------------- /utils/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { expandGlobBase } from './inject'; 2 | import { getResourcesFromList } from './index'; 3 | 4 | describe('Resouce handling - ', () => { 5 | it(`loading`, () => { 6 | // dummy 7 | }) 8 | // it(`loading`, () => { 9 | // const resources = getResourcesFromList(require(`../package.json`), 'aurelia.build.resources') 10 | // // console.log(resources) 11 | // expect(resources).toBeTruthy() 12 | // expect(resources.length).toBe(6) 13 | // }) 14 | // it(`globbing`, (done) => { 15 | // expandGlobBase 16 | // const resources = getResourcesFromList(require(`../package.json`), '_test.resources') 17 | // // console.log(resources) 18 | // expect(resources).toBeTruthy() 19 | // expect(resources.length).toBe(4) 20 | // }) 21 | }) 22 | -------------------------------------------------------------------------------- /utils/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs' 3 | import * as cheerio from 'cheerio' 4 | import {memoize, MapCache} from 'lodash' 5 | import { AddLoadersOptions, AddLoadersMethod, RequireData, RequireDataBase, PathWithLoaders, SelectorAndAttribute } from '../typings/definitions' 6 | import { 7 | appendCodeAndCallback, 8 | expandAllRequiresForGlob, 9 | getRequireStrings, 10 | splitRequest, 11 | wrapInRequireInclude 12 | } from './inject'; 13 | import {get} from 'lodash' 14 | import * as debug from 'debug' 15 | const log = debug('utils') 16 | 17 | const invalidationDebounceDirectory = new WeakMap>() 18 | export function cacheInvalidationDebounce(cacheKey: string, cache: MapCache, dictionaryKey: any, debounceMs = 10000) { 19 | let invalidationDebounce = invalidationDebounceDirectory.get(dictionaryKey) 20 | if (!invalidationDebounce) { 21 | invalidationDebounce = new Map() 22 | invalidationDebounceDirectory.set(dictionaryKey, invalidationDebounce) 23 | } 24 | const previousTimeout = invalidationDebounce.get(cacheKey) 25 | invalidationDebounce.delete(cacheKey) 26 | if (previousTimeout) clearTimeout(previousTimeout) 27 | const timeout = setTimeout(() => cache.delete(cacheKey), debounceMs) 28 | timeout.unref() // do not require the Node.js event loop to remain active 29 | invalidationDebounce.set(cacheKey, timeout) 30 | } 31 | 32 | export const getFilesInDir = memoize(getFilesInDirBase, (directory: string, { 33 | skipHidden = true, recursive = false, regexFilter = undefined, emitWarning = console.warn.bind(console), emitError = console.error.bind(console), fileSystem = fs, regexIgnore = [/node_modules/], returnRelativeTo = directory 34 | }: GetFilesInDirOptions = {}) => { 35 | /** valid for 10 seconds before invalidating cache **/ 36 | const cacheKey = `${directory}::${skipHidden}::${recursive}::${regexFilter}::${regexIgnore.join('::')}` 37 | cacheInvalidationDebounce(cacheKey, getFilesInDir.cache, fileSystem) 38 | return cacheKey 39 | }) 40 | 41 | export interface GetFilesInDirOptions { 42 | skipHidden?: boolean 43 | recursive?: boolean 44 | regexFilter?: RegExp 45 | emitWarning?: (warn: string) => void 46 | emitError?: (warn: string) => void 47 | fileSystem?: { readdir: Function, stat: Function } 48 | regexIgnore?: Array 49 | /** 50 | * If set to a path, additionally returns the part of the path 51 | * starting from the directory base without the leading './' 52 | */ 53 | returnRelativeTo?: string 54 | ignoreIfNotExists?: boolean 55 | } 56 | 57 | export async function getFilesInDirBase(directory: string, { 58 | 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 59 | }: GetFilesInDirOptions = {} 60 | ): Promise> { 61 | 62 | if (!directory) { 63 | emitError(`No directory supplied`) 64 | return [] 65 | } 66 | 67 | const exists = await new Promise((resolve, reject) => 68 | fileSystem.stat(directory, (err, stat) => 69 | err ? resolve() : 70 | resolve(stat) 71 | ) 72 | ) 73 | 74 | if (!exists || !exists.isDirectory()) { 75 | if (!ignoreIfNotExists) { 76 | emitError(`The supplied directory does not exist ${directory}`) 77 | } 78 | return [] 79 | } 80 | 81 | let files = await new Promise((resolve, reject) => 82 | fileSystem.readdir(directory, (err, value) => err ? resolve([]) || emitWarning(`Error when trying to load ${directory}: ${err.message}`) : resolve(value))) 83 | 84 | if (regexIgnore && regexIgnore.length) { 85 | files = files 86 | .filter(filePath => !regexIgnore.some(regex => regex.test(filePath))) 87 | } 88 | 89 | if (skipHidden) { 90 | files = files 91 | .filter(filePath => path.basename(filePath)[0] !== '.') 92 | } 93 | 94 | files = files.map(filePath => path.join(directory, filePath)) 95 | 96 | let stats = (await Promise.all( 97 | files 98 | .map(filePath => new Promise<{ filePath: string, stat: fs.Stats, relativePath: string }>((resolve, reject) => 99 | fileSystem.stat(filePath, (err, stat) => 100 | err ? resolve({filePath, stat, relativePath: ''}) : 101 | resolve({filePath, stat, relativePath: path.relative(returnRelativeTo, filePath)}) 102 | ) 103 | )) 104 | )).filter(stat => !!stat.stat) 105 | 106 | if (regexFilter) { 107 | stats = stats 108 | .filter(file => 109 | !(file.stat.isFile() && !file.filePath.match(regexFilter)) 110 | ) 111 | } 112 | 113 | if (!recursive) 114 | return stats.filter(file => file.stat.isFile()) 115 | 116 | const subDirectoryStats = await Promise.all( 117 | stats.filter(file => file.stat.isDirectory()).map( 118 | file => getFilesInDir(file.filePath, { 119 | skipHidden, recursive, regexFilter, emitWarning, emitError, fileSystem, regexIgnore, returnRelativeTo 120 | }) 121 | ) 122 | ) 123 | 124 | return stats.filter(file => file.stat.isFile()).concat( 125 | ...subDirectoryStats 126 | ) 127 | } 128 | 129 | // export async function concatPromiseResults(values: (Array | PromiseLike>)[]): Promise { 130 | export async function concatPromiseResults(values: Array>>): Promise> { 131 | return ([] as Array).concat(...(await Promise.all>(values))) 132 | } 133 | 134 | export interface ResourcesInput { 135 | path: Array | string 136 | lazy?: boolean 137 | bundle?: string 138 | chunk?: string 139 | } 140 | 141 | export function getResourcesFromList(json: Object, propertyPath: string) { 142 | const resources = get(json, propertyPath, [] as Array) 143 | if (!resources.length) return [] 144 | 145 | const allResources = [] as Array 146 | 147 | resources.forEach(input => { 148 | const r = input instanceof Object && !Array.isArray(input) ? input as ResourcesInput : { path: input } 149 | const paths = Array.isArray(r.path) ? r.path : [r.path] 150 | paths.forEach( 151 | literal => allResources.push({ literal, lazy: r.lazy || false, chunk: r.bundle || r.chunk }) 152 | ) 153 | }) 154 | 155 | return allResources 156 | } 157 | 158 | /** 159 | * Generates list of dependencies based on the passed in selectors, e.g.: 160 | * - 161 | * - 162 | * - 163 | */ 164 | export function getTemplateResourcesData(html: string, selectorsAndAttributes: Array, globRegex: RegExp | undefined) { 165 | const $ = cheerio.load(html) // { decodeEntities: false } 166 | 167 | function extractRequire(context: Cheerio, fromAttribute = 'from') { 168 | const resources: Array = [] 169 | context.each(index => { 170 | let path = context[index].attribs[fromAttribute] as string | undefined 171 | if (!path) return 172 | 173 | if (globRegex && globRegex.test(path)) { 174 | path = path.replace(globRegex, `*`) 175 | } 176 | const lazy = context[index].attribs.hasOwnProperty('lazy') 177 | const chunk = context[index].attribs['bundle'] || context[index].attribs['chunk'] 178 | resources.push({ literal: path, lazy, chunk }) 179 | }) 180 | return resources 181 | } 182 | 183 | const resourcesArray = selectorsAndAttributes 184 | .map(saa => extractRequire($(saa.selector), saa.attribute)) 185 | 186 | const resources = ([] as RequireDataBase[]).concat(...resourcesArray) 187 | return resources 188 | } 189 | -------------------------------------------------------------------------------- /utils/inject.ts: -------------------------------------------------------------------------------- 1 | import { AddLoadersMethod, PathWithLoaders, RequireData, RequireDataBase } from '../typings/definitions' 2 | import * as path from 'path' 3 | import * as loaderUtils from 'loader-utils' 4 | import * as SourceMap from 'source-map' 5 | import { getFilesInDir, concatPromiseResults, cacheInvalidationDebounce } from './index' 6 | import ModuleDependency = require('webpack/lib/dependencies/ModuleDependency') 7 | import escapeStringForRegex = require('escape-string-regexp') 8 | import {memoize, uniqBy} from 'lodash' 9 | import * as debug from 'debug' 10 | const log = debug('utils') 11 | 12 | export function appendCodeAndCallback(loader: Webpack.Core.LoaderContext, source: string, inject: string, sourceMap?: SourceMap.RawSourceMap, synchronousIfPossible = false) { 13 | inject += (!source.trim().endsWith(';')) ? ';\n' : '\n' 14 | 15 | // support existing SourceMap 16 | // https://github.com/mozilla/source-map#sourcenode 17 | // https://github.com/webpack/imports-loader/blob/master/index.js#L34-L44 18 | // https://webpack.github.io/docs/loaders.html#writing-a-loader 19 | if (sourceMap) { 20 | const currentRequest = loaderUtils.getCurrentRequest(loader) 21 | const SourceNode = SourceMap.SourceNode 22 | const SourceMapConsumer = SourceMap.SourceMapConsumer 23 | const sourceMapConsumer = new SourceMapConsumer(sourceMap) 24 | const node = SourceNode.fromStringWithSourceMap(source, sourceMapConsumer) 25 | 26 | node.add(inject) 27 | 28 | const result = node.toStringWithSourceMap({ 29 | file: currentRequest 30 | }) 31 | 32 | loader.callback(null, result.code, result.map.toJSON()) 33 | } else { 34 | if (synchronousIfPossible) { 35 | return inject ? source + inject : source 36 | } else { 37 | loader.callback(null, source + inject) 38 | } 39 | } 40 | } 41 | 42 | export async function splitRequest(literal: string, loaderInstance?: Webpack.Core.LoaderContext) { 43 | // log(`Split Request: ${literal}`) 44 | let pathBits = literal.split(`/`) 45 | let remainingRequestBits = pathBits.slice() 46 | const literalIsRelative = literal[0] === '.' 47 | if (!literalIsRelative) { 48 | const fullPathNdIdx = pathBits.lastIndexOf('node_modules') 49 | if (fullPathNdIdx >= 0) { 50 | // conform full hard disk path /.../node_modules/MODULE_NAME/... to just MODULE_NAME/... 51 | pathBits = pathBits.slice(fullPathNdIdx + 1) 52 | } 53 | const moduleNameLength = pathBits[0].startsWith(`@`) ? 2 : 1 54 | const moduleName = pathBits.slice(0, moduleNameLength).join(`/`) 55 | // remainingRequest may be globbed: 56 | let ifModuleRemainingRequestBits = pathBits.slice(moduleNameLength) 57 | const remainingRequest = ifModuleRemainingRequestBits.join(`/`) 58 | let moduleRoot = '' 59 | let tryModule: { 60 | resolve: EnhancedResolve.ResolveResult | undefined; 61 | } = { resolve: undefined } 62 | if (loaderInstance && !moduleName.includes(`*`)) { 63 | // TODO: test this 64 | tryModule = await resolveLiteral({ literal: `${moduleName}` }, loaderInstance, undefined, false) 65 | if (tryModule.resolve && tryModule.resolve.descriptionFileRoot) { 66 | moduleRoot = tryModule.resolve.descriptionFileRoot 67 | } 68 | log(`does module '${moduleName}' exist?: ${tryModule.resolve && 'true' || 'false'}`) 69 | } 70 | if (!loaderInstance || tryModule.resolve) { 71 | return { 72 | moduleName, moduleRoot, remainingRequest, pathBits, remainingRequestBits: ifModuleRemainingRequestBits 73 | } 74 | } 75 | } 76 | return { remainingRequest: literal, remainingRequestBits, pathBits, moduleName: '', moduleRoot: '' } 77 | } 78 | 79 | export async function expandGlobBase(literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath)) { 80 | const { pathBits, remainingRequest, remainingRequestBits, moduleName, moduleRoot } = await splitRequest(literal, loaderInstance) 81 | let possibleRoots = loaderInstance.options.resolve.modules.filter((m: string) => path.isAbsolute(m)) as Array 82 | 83 | const nextGlobAtIndex = remainingRequestBits.findIndex(pb => pb.includes(`*`)) 84 | const relativePathUntilFirstGlob = remainingRequestBits.slice(0, nextGlobAtIndex).join(`/`) 85 | const relativePathFromFirstGlob = remainingRequestBits.slice(nextGlobAtIndex).join(`/`) 86 | 87 | if (moduleName && moduleRoot) { 88 | // TODO: add support for aliases when they point to a subdirectory 89 | // Or maybe the resolve will already include it? 90 | possibleRoots = [moduleRoot] 91 | } else if (rootForRelativeResolving) { 92 | possibleRoots = [rootForRelativeResolving, ...possibleRoots] 93 | } 94 | 95 | let possiblePaths = await concatPromiseResults( 96 | possibleRoots.map(async directory => await getFilesInDir(path.join(directory, relativePathUntilFirstGlob), { 97 | recursive: true, emitWarning: loaderInstance.emitWarning, emitError: loaderInstance.emitError, 98 | fileSystem: loaderInstance.fs, skipHidden: true 99 | })) 100 | ) 101 | 102 | possiblePaths = uniqBy(possiblePaths, 'filePath') 103 | 104 | // test case: escape('werwer/**/werwer/*.html').replace(/\//g, '[\\/]+').replace(/\\\*\\\*/g, '\.*?').replace(/\\\*/g, '[^/\\\\]*?') 105 | const globRegexString = escapeStringForRegex(relativePathFromFirstGlob) 106 | .replace(/\//g, '[\\/]+') // accept Windows and Unix slashes 107 | .replace(/\\\*\\\*/g, '\.*?') // multi glob ** => any number of subdirectories 108 | .replace(/\\\*/g, '[^/\\\\]*?') // single glob * => one directory (stops at first slash/backslash) 109 | const globRegex = new RegExp(`^${globRegexString}$`) // (?:\.\w+) 110 | const correctPaths = possiblePaths.filter(p => p.stat.isFile() && globRegex.test(p.relativePath)) 111 | 112 | return correctPaths.map(p => p.filePath) 113 | } 114 | 115 | const expandGlob = memoize(expandGlobBase, (literal: string, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving = path.dirname(loaderInstance.resourcePath)) => { 116 | /** valid for 10 seconds for the same literal and resoucePath */ 117 | const cacheKey = `${literal}::${path.dirname(loaderInstance.resourcePath)}::${rootForRelativeResolving}` 118 | // invalidate every 10 seconds based on each unique Webpack compilation 119 | cacheInvalidationDebounce(cacheKey, expandGlob.cache, loaderInstance._compilation) 120 | return cacheKey 121 | }) 122 | 123 | function fixWindowsPath(windowsPath: string) { 124 | return windowsPath.replace(/\\/g, '/') 125 | } 126 | 127 | export async function expandAllRequiresForGlob(requires: Array, loaderInstance: Webpack.Core.LoaderContext, rootForRelativeResolving: string | false = path.dirname(loaderInstance.resourcePath), returnRelativeLiteral = false) { 128 | const needDeglobbing = requires.filter(r => r.literal.includes(`*`)) 129 | const deglobbed = requires.filter(r => !r.literal.includes(`*`)) 130 | const allDeglobbed = deglobbed.concat(await concatPromiseResults(needDeglobbing.map(async r => 131 | (await expandGlob(r.literal, loaderInstance, rootForRelativeResolving)) 132 | .map(correctPath => Object.assign({}, r, { 133 | literal: returnRelativeLiteral ? 134 | `./${fixWindowsPath( 135 | path.relative(path.dirname(loaderInstance.resourcePath), correctPath) 136 | )}` : correctPath 137 | })) 138 | ))) 139 | return uniqBy(allDeglobbed, 'literal') 140 | } 141 | 142 | // TODO: function cleanUpPath 143 | // this func does: makes a relative path from absolute 144 | // OR strips all node_modules and makes a 'module' request path instead 145 | // USE IT in the above glob expansion or better yet, in the below getRequireString, so we have nice requests instead of full paths! 146 | 147 | export async function getRequireStrings(maybeResolvedRequires: Array, addLoadersMethod: AddLoadersMethod | undefined, loaderInstance: Webpack.Core.LoaderContext, forceFallbackLoaders = false): Promise> { 148 | const requires = (await Promise.all(maybeResolvedRequires.map( 149 | async r => !r.resolve ? await resolveLiteral(r, loaderInstance) : r 150 | )) as Array).filter(r => !!r.resolve) 151 | 152 | type PathsAndLoadersWithLiterals = PathWithLoaders & {removed?: boolean, literal: string} 153 | let pathsAndLoaders: Array 154 | 155 | if (typeof addLoadersMethod === 'function') { 156 | const maybePromise = addLoadersMethod(requires, loaderInstance) 157 | const pathsAndLoadersReturnValue = (maybePromise as Promise>).then ? await maybePromise : maybePromise as Array 158 | pathsAndLoaders = pathsAndLoadersReturnValue.map(p => { 159 | const rq = requires.find(r => r.resolve.path === p.path) 160 | if (!rq) return Object.assign(p, {removed: true, literal: undefined}) 161 | return Object.assign(p, { loaders: (p.loaders && !forceFallbackLoaders) ? p.loaders : (rq.loaders || rq.fallbackLoaders || []), literal: rq.literal, removed: false }) 162 | }).filter(r => !r.removed) as Array 163 | } else { 164 | pathsAndLoaders = requires.map(r => ({ literal: r.literal, loaders: r.loaders || r.fallbackLoaders || [], path: r.resolve.path })) 165 | } 166 | 167 | return pathsAndLoaders.map(p => 168 | (p.loaders && p.loaders.length) ? 169 | `!${p.loaders.join('!')}!${p.literal}` : 170 | p.literal 171 | ) 172 | } 173 | 174 | export function wrapInRequireInclude(toRequire: string) { 175 | return `require.include('${toRequire}');` 176 | } 177 | 178 | // TODO: memoize: 179 | export function resolveLiteral(toRequire: T, loaderInstance: Webpack.Core.LoaderContext, contextPath = path.dirname(loaderInstance.resourcePath) /* TODO: could this simply be loaderInstance.context ? */, sendWarning = true) { 180 | debug('resolve')(`Resolving: ${toRequire.literal}`) 181 | return new Promise<{resolve: EnhancedResolve.ResolveResult | undefined} & T>((resolve, reject) => 182 | loaderInstance.resolve(contextPath, toRequire.literal, 183 | (err, result, value) => err ? resolve(Object.assign({resolve: value}, toRequire)) || (sendWarning && loaderInstance.emitWarning(err.message)) : 184 | resolve(Object.assign({resolve: value}, toRequire)) 185 | ) 186 | ) 187 | } 188 | 189 | export function addBundleLoader(resources: Array, property = 'fallbackLoaders') { 190 | return resources.map(toRequire => { 191 | const lazy = toRequire.lazy && 'lazy' || '' 192 | const chunkName = (toRequire.chunk && `name=${toRequire.chunk}`) || '' 193 | const and = lazy && chunkName && '&' || '' 194 | const bundleLoaderPrefix = (lazy || chunkName) ? 'bundle?' : '' 195 | const bundleLoaderQuery = `${bundleLoaderPrefix}${lazy}${and}${chunkName}` 196 | 197 | return bundleLoaderQuery ? Object.assign({ [property]: [bundleLoaderQuery] }, toRequire) : toRequire 198 | }) as Array, fallbackLoaders?: Array }> 199 | } 200 | 201 | // TODO: use custom ModuleDependency instead of injecting code 202 | export class SimpleDependencyClass extends ModuleDependency { 203 | module: Webpack.Core.NormalModule 204 | type = 'simple-dependency' 205 | constructor(request: string) { 206 | super(request) 207 | debugger 208 | } 209 | } 210 | 211 | export class SimpleDependencyTemplate { 212 | apply(parentDependency: SimpleDependencyClass, source: Webpack.WebpackSources.ReplaceSource, outputOptions: { pathinfo }, requestShortener: { shorten: (request: string) => string }) { 213 | debugger 214 | if (outputOptions.pathinfo && parentDependency.module) { 215 | const comment = ("/*! simple-dependency " + requestShortener.shorten(parentDependency.request) + " */") 216 | source.insert(source.size(), comment) 217 | } 218 | } 219 | } 220 | 221 | export const SimpleDependency = Object.assign(SimpleDependencyClass, { Template: SimpleDependencyTemplate }) 222 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | require('ts-node').register(); 3 | const webpack = require('webpack'); 4 | const webpackSources = require('webpack-sources'); 5 | const enhancedResolve = require('enhanced-resolve'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const path = require('path'); 8 | const log = require('debug')('config') 9 | const RewriteModuleSubdirectoryPlugin = require('./plugins/rewrite-module-subdirectory-plugin').RewriteModuleSubdirectoryPlugin 10 | const RootMostResolvePlugin = require('./plugins/root-most-resolve-plugin').RootMostResolvePlugin 11 | const MappedModuleIdsPlugin = require('./plugins/mapped-module-ids-plugin').MappedModuleIdsPlugin 12 | const AureliaAddLoadersCallback = require('./example/aurelia').addLoadersMethod 13 | const ConventionInvalidatePlugin = require('./plugins/convention-invalidate-plugin').ConventionInvalidatePlugin 14 | const rootDir = path.resolve() 15 | const appDir = path.resolve(`test-fixtures/app`) 16 | 17 | const addLoadersCallback = async (list, loaderInstance) => { 18 | return await AureliaAddLoadersCallback(rootDir, list, loaderInstance) 19 | } 20 | 21 | module.exports = { 22 | entry: { 23 | 'main': ['./test-fixtures/app/main.js'], 24 | }, 25 | output: { 26 | path: path.resolve('test-fixtures/webpack-dist'), 27 | filename: '[name].bundle.js', 28 | }, 29 | module: { 30 | rules: [ 31 | { 32 | test: /\.html$/, 33 | include: [appDir, path.resolve('test-fixtures/app-extra')], 34 | use: [ 35 | { 36 | loader: 'html-require-loader', 37 | options: { 38 | addLoadersCallback 39 | } 40 | } 41 | ] 42 | }, 43 | /* 44 | // this would add all files matching a regex under a given directory as dependencies to the given file: 45 | { 46 | test: /\.js$/, 47 | include: [path.resolve('app/main.js')], 48 | use: [ 49 | { 50 | loader: 'convention-loader', 51 | query: { 52 | convention: 'all-files-matching-regex', 53 | regex: /\.js$/, 54 | directory: path.resolve('test-fixtures/app-extra') 55 | } 56 | }, 57 | ], 58 | }, 59 | */ 60 | { 61 | test: /\.js$/, 62 | include: [/*appDir, *//node_modules\/aurelia-/], 63 | use: [ 64 | { 65 | loader: 'list-based-require-loader', 66 | options: { 67 | addLoadersCallback, 68 | packagePropertyPath: 'aurelia.build.resources', 69 | enableGlobbing: true, 70 | rootDir: path.resolve() 71 | } 72 | } 73 | ], 74 | }, 75 | // We are chianing the custom loader to babel loader. 76 | // Purely optional but know that the `first` loader in the chain (babel in this case) 77 | // must always return JavaScript (as it is then processed into the compilation) 78 | { 79 | test: /\.js$/, 80 | include: [appDir, path.resolve('test-fixtures/app-extra')], 81 | loaders: [ 82 | { 83 | loader: 'comment-loader', 84 | options: { 85 | addLoadersCallback 86 | } 87 | }, 88 | { 89 | loader: 'convention-loader', 90 | options: { 91 | addLoadersCallback, 92 | convention: 'extension-swap' 93 | // convention: function(fullPath) { 94 | // const path = require('path') 95 | // const basename = path.basename(fullPath) 96 | // const noExtension = basename.substr(0, basename.lastIndexOf('.')) || basename 97 | // const basepath = path.dirname(fullPath) 98 | // return path.join(basepath, noExtension + '.html') 99 | // } 100 | } 101 | }, 102 | ], 103 | }, 104 | ], 105 | }, 106 | // This allows us to add resolving functionality for our custom loader 107 | // It's used just like the resolve property and we are referencing the 108 | // custom loader file. 109 | resolveLoader: { 110 | alias: { 111 | 'comment-loader': require.resolve('./loaders/comment-loader'), 112 | 'convention-loader': require.resolve('./loaders/convention-loader'), 113 | 'html-require-loader': require.resolve('./loaders/html-require-loader'), 114 | 'list-based-require-loader': require.resolve('./loaders/list-based-require-loader'), 115 | }, 116 | extensions: [".ts", ".webpack-loader.js", ".web-loader.js", ".loader.js", ".js"] 117 | }, 118 | resolve: { 119 | modules: [ 120 | path.resolve("test-fixtures/app"), 121 | "node_modules" 122 | ], 123 | extensions: ['.js'], 124 | plugins: [ 125 | new RewriteModuleSubdirectoryPlugin((moduleName, remainingRequest, request) => { 126 | if (moduleName.startsWith('aurelia-')) 127 | return `${moduleName}/dist/native-modules/${remainingRequest || moduleName}` 128 | }), 129 | new RewriteModuleSubdirectoryPlugin((moduleName, remainingRequest, request) => { 130 | if (moduleName.startsWith('aurelia-')) 131 | return `${moduleName}/dist/commonjs/${remainingRequest || moduleName}` 132 | }), 133 | new RootMostResolvePlugin(__dirname) 134 | ], 135 | }, 136 | plugins: [ 137 | new MappedModuleIdsPlugin({ 138 | appDir: appDir, 139 | prefixLoaders: [{loader: 'bundle-loader', prefix: 'async'}], 140 | logWhenRawRequestDiffers: true, 141 | dotSlashWhenRelativeToAppDir: false, 142 | beforeLoadersTransform: (moduleId) => { 143 | if (!moduleId.startsWith('aurelia-')) return moduleId 144 | return moduleId 145 | .replace('/dist/native-modules', '') 146 | .replace('/dist/commonjs', '') 147 | }, 148 | afterExtensionTrimmingTransform: (moduleId) => { 149 | if (!moduleId.startsWith('aurelia-')) return moduleId 150 | const split = moduleId.split('/') 151 | if (split.length === 2 && split[0] === split[1]) { 152 | // aurelia uses custom main path 153 | return split[0] 154 | } 155 | return moduleId 156 | } 157 | }), 158 | new HtmlWebpackPlugin({ 159 | template: './test-fixtures/app/index.html', 160 | }), 161 | new ConventionInvalidatePlugin((watchResult) => { 162 | return watchResult 163 | }) 164 | ], 165 | devtool: false, 166 | }; 167 | --------------------------------------------------------------------------------