├── test-fixtures
├── app
│ ├── sub
│ │ ├── hello.html
│ │ ├── hello.js
│ │ └── double.js
│ ├── resources
│ │ ├── hello.html
│ │ ├── glooob
│ │ │ ├── test-b.html
│ │ │ ├── test-a.js
│ │ │ ├── test-b.js
│ │ │ └── subdir
│ │ │ │ └── test-c.js
│ │ ├── hello.js
│ │ ├── double.js
│ │ └── triple.js
│ ├── root-most.js
│ ├── app.html
│ ├── index.html
│ ├── engine.js
│ ├── car.js
│ ├── main.js
│ ├── car.spec.js
│ ├── engine.spec.js
│ ├── app.js
│ ├── welcome.html
│ ├── welcome.js
│ └── nav-bar.html
└── app-extra
│ ├── sub
│ ├── hello.html
│ ├── extra-sub.js
│ └── hello.js
│ └── extra.js
├── temp
├── useful.ts
└── tmp.ts
├── .npmignore
├── custom_typings
├── enhanced-resolve.files.d.ts
├── any.d.ts
├── webpack.files.d.ts
├── enhanced-resolve.d.ts
└── webpack.d.ts
├── .vscode
├── settings.json
└── tasks.json
├── .gitignore
├── .editorconfig
├── test
├── preprocessor.js
└── e2e.spec.js
├── tsconfig.json
├── .travis.yml
├── index.html
├── index.ts
├── utils
├── index.spec.ts
├── index.ts
└── inject.ts
├── LICENSE.md
├── typings
├── promisify.d.ts
└── definitions.d.ts
├── DEV_NOTES.md
├── plugins
├── rewrite-module-subdirectory-plugin.ts
├── convention-invalidate-plugin.ts
├── root-most-resolve-plugin.ts
└── mapped-module-ids-plugin.ts
├── loaders
├── html-require-loader.ts
├── comment-loader.ts
├── convention-loader.ts
└── list-based-require-loader.ts
├── example
└── aurelia.ts
├── package.json
├── readme.md
└── webpack.config.js
/test-fixtures/app/sub/hello.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test-fixtures/app-extra/sub/hello.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/hello.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/glooob/test-b.html:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test-fixtures/app-extra/extra.js:
--------------------------------------------------------------------------------
1 | console.log(`Extra!`)
2 |
--------------------------------------------------------------------------------
/temp/useful.ts:
--------------------------------------------------------------------------------
1 |
2 | // this._module.id = 'something-else'
3 |
--------------------------------------------------------------------------------
/test-fixtures/app/sub/hello.js:
--------------------------------------------------------------------------------
1 | console.log('sub/hello!')
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/hello.js:
--------------------------------------------------------------------------------
1 | console.log('sub/hello!')
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/sub/double.js:
--------------------------------------------------------------------------------
1 | console.log(`I'm mr Double!`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/double.js:
--------------------------------------------------------------------------------
1 | console.log(`I'm mr Double!`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/glooob/test-a.js:
--------------------------------------------------------------------------------
1 | console.log(`HelloA`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/glooob/test-b.js:
--------------------------------------------------------------------------------
1 | console.log(`HelloB`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/triple.js:
--------------------------------------------------------------------------------
1 | console.log(`I'm mr Triple!`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app-extra/sub/extra-sub.js:
--------------------------------------------------------------------------------
1 | console.log(`I'm mr Extra Sub!`)
2 |
--------------------------------------------------------------------------------
/test-fixtures/app-extra/sub/hello.js:
--------------------------------------------------------------------------------
1 | console.log('app-extra/sub/hello!')
2 |
--------------------------------------------------------------------------------
/test-fixtures/app/resources/glooob/subdir/test-c.js:
--------------------------------------------------------------------------------
1 | console.log(`HelloC`)
2 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | .vscode
4 | *.log
5 | test-fixtures/webpack-dist
6 |
--------------------------------------------------------------------------------
/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/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test-fixtures/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Webpack Developer Kit
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Webpack Developer Kit
7 |
8 |
9 |
12 |
13 |
17 |
18 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/test-fixtures/app/welcome.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | ${heading < heading2}
4 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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-fixtures/app/nav-bar.html:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # NO LONGER MAINTAINED. PLEASE DO NOT USE OR FORK.
2 |
3 | # Webpack Dependency Suite
4 |
5 | [](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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------