├── .gitattributes ├── test ├── fixtures │ ├── bom.bin │ ├── res#ource.bin │ ├── resource.bin │ ├── module-exports-object-loader.js │ ├── module-exports-string-loader.js │ ├── esm-loader.mjs │ ├── keys-loader.js │ ├── simple-loader.js │ ├── pitch-promise-undef-loader.js │ ├── throws-error-loader.js │ ├── simple-promise-loader.js │ ├── pitch-dependencies-loader.js │ ├── promise-error-loader.js │ ├── pitching-loader.js │ ├── pitch-async-undef-loader.js │ ├── identity-loader.js │ ├── set-empty-resource-loader.js │ ├── pitch-async-undef-some-loader.js │ ├── simple-async-loader.js │ ├── raw-loader.js │ ├── set-resource-loader.js │ ├── change-stuff-loader.js │ └── dependencies-loader.js └── runLoaders.js ├── .gitignore ├── prettier.config.mjs ├── eslint.config.mjs ├── .editorconfig ├── lib ├── LoaderLoadingError.js ├── loadLoader.js └── LoaderRunner.js ├── LICENSE ├── README.md ├── package.json └── .github └── workflows └── test.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /test/fixtures/bom.bin: -------------------------------------------------------------------------------- 1 | böm -------------------------------------------------------------------------------- /test/fixtures/res#ource.bin: -------------------------------------------------------------------------------- 1 | resource -------------------------------------------------------------------------------- /test/fixtures/resource.bin: -------------------------------------------------------------------------------- 1 | resource -------------------------------------------------------------------------------- /test/fixtures/module-exports-object-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/fixtures/module-exports-string-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = ""; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /coverage 3 | .eslintcache 4 | .nyc_output 5 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | export { default } from "eslint-config-webpack/prettier-config-es5.js"; 2 | -------------------------------------------------------------------------------- /test/fixtures/esm-loader.mjs: -------------------------------------------------------------------------------- 1 | export default function (source) { 2 | return source + "-esm"; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/keys-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | return JSON.stringify(this); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/simple-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | return source + "-simple"; 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/pitch-promise-undef-loader.js: -------------------------------------------------------------------------------- 1 | exports.pitch = function () { 2 | return Promise.resolve(); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/throws-error-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | throw new Error(source); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/simple-promise-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | return Promise.resolve(source + "-promise-simple"); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/pitch-dependencies-loader.js: -------------------------------------------------------------------------------- 1 | exports.pitch = function (remainingRequest) { 2 | this.addDependency("remainingRequest:" + remainingRequest); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/promise-error-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | return Promise.resolve().then(() => { 3 | throw new Error(source); 4 | }); 5 | }; 6 | -------------------------------------------------------------------------------- /test/fixtures/pitching-loader.js: -------------------------------------------------------------------------------- 1 | exports.pitch = function (remainingRequest, previousRequest, data) { 2 | return [remainingRequest, previousRequest].join(":"); 3 | }; 4 | -------------------------------------------------------------------------------- /test/fixtures/pitch-async-undef-loader.js: -------------------------------------------------------------------------------- 1 | exports.pitch = function () { 2 | var done = this.async(); 3 | 4 | setTimeout(function () { 5 | done(null, undefined); 6 | }, 0); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/identity-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | return source; 3 | }; 4 | module.exports.pitch = function (rem, prev, data) { 5 | data.identity = true; 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/set-empty-resource-loader.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = function (source) { 4 | this.resource = ""; 5 | 6 | return source + "-simple"; 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/pitch-async-undef-some-loader.js: -------------------------------------------------------------------------------- 1 | exports.pitch = function () { 2 | var done = this.async(); 3 | 4 | setTimeout(function () { 5 | done(null, undefined, "not undefined"); 6 | }, 0); 7 | }; 8 | -------------------------------------------------------------------------------- /test/fixtures/simple-async-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | var callback = this.async(); 3 | setTimeout(function () { 4 | callback(null, source + "-async-simple"); 5 | }, 50); 6 | }; 7 | -------------------------------------------------------------------------------- /test/fixtures/raw-loader.js: -------------------------------------------------------------------------------- 1 | exports.__es6Module = true; 2 | exports.default = function (source) { 3 | return Buffer.from( 4 | source.toString("hex") + source.toString("utf-8"), 5 | "utf-8" 6 | ); 7 | }; 8 | exports.raw = true; 9 | -------------------------------------------------------------------------------- /test/fixtures/set-resource-loader.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | module.exports = function (source) { 4 | this.resource = `${path.resolve(__dirname, "./resource.bin")}?foo=bar#hash`; 5 | 6 | return source + "-simple"; 7 | }; 8 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "eslint/config"; 2 | import config from "eslint-config-webpack"; 3 | 4 | export default defineConfig([ 5 | { 6 | extends: [config], 7 | rules: { 8 | "prefer-spread": "off", 9 | "unicorn/prefer-spread": "off", 10 | "prefer-rest-params": "off", 11 | }, 12 | }, 13 | ]); 14 | -------------------------------------------------------------------------------- /test/fixtures/change-stuff-loader.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | exports.pitch = function pitch(rem, prev, data) { 4 | this.loaders[this.loaderIndex + 2].request = path.resolve( 5 | __dirname, 6 | "identity-loader.js" 7 | ); 8 | this.resource = path.resolve(__dirname, "resource.bin"); 9 | this.loaderIndex += 2; 10 | this.cacheable(false); 11 | }; 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 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 | [*.{yml,yaml,json}] 12 | indent_style = space 13 | indent_size = 2 14 | 15 | [*.md] 16 | trim_trailing_whitespace = false 17 | 18 | [*.snap] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /lib/LoaderLoadingError.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | class LoadingLoaderError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.name = "LoaderRunnerError"; 7 | // For old Node.js engines remove it then we drop them support 8 | // eslint-disable-next-line unicorn/no-useless-error-capture-stack-trace 9 | Error.captureStackTrace(this, this.constructor); 10 | } 11 | } 12 | 13 | module.exports = LoadingLoaderError; 14 | -------------------------------------------------------------------------------- /test/fixtures/dependencies-loader.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | this.clearDependencies(); 3 | this.addDependency("a"); 4 | this.addDependency("b"); 5 | this.addContextDependency("c"); 6 | this.addMissingDependency("d"); 7 | return ( 8 | source + 9 | "\n" + 10 | JSON.stringify(this.getDependencies()) + 11 | JSON.stringify(this.getContextDependencies()) + 12 | JSON.stringify(this.getMissingDependencies()) 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) Tobias Koppers @sokra 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # loader-runner 2 | 3 | ```js 4 | import { runLoaders } from "loader-runner"; 5 | 6 | runLoaders( 7 | { 8 | resource: "/abs/path/to/file.txt?query", 9 | // String: Absolute path to the resource (optionally including query string) 10 | 11 | loaders: ["/abs/path/to/loader.js?query"], 12 | // String[]: Absolute paths to the loaders (optionally including query string) 13 | // {loader, options}[]: Absolute paths to the loaders with options object 14 | 15 | context: { minimize: true }, 16 | // Additional loader context which is used as base context 17 | 18 | processResource: (loaderContext, resourcePath, callback) => { 19 | // ... 20 | }, 21 | // Optional: A function to process the resource 22 | // Must have signature function(context, path, function(err, buffer)) 23 | // By default readResource is used and the resource is added a fileDependency 24 | 25 | readResource: fs.readFile.bind(fs), 26 | // Optional: A function to read the resource 27 | // Only used when 'processResource' is not provided 28 | // Must have signature function(path, function(err, buffer)) 29 | // By default fs.readFile is used 30 | }, 31 | (err, result) => { 32 | // err: Error? 33 | // result.result: Buffer | String 34 | // The result 35 | // only available when no error occurred 36 | // result.resourceBuffer: Buffer 37 | // The raw resource as Buffer (useful for SourceMaps) 38 | // only available when no error occurred 39 | // result.cacheable: Bool 40 | // Is the result cacheable or do it require reexecution? 41 | // result.fileDependencies: String[] 42 | // An array of paths (existing files) on which the result depends on 43 | // result.missingDependencies: String[] 44 | // An array of paths (not existing files) on which the result depends on 45 | // result.contextDependencies: String[] 46 | // An array of paths (directories) on which the result depends on 47 | } 48 | ); 49 | ``` 50 | 51 | More documentation following... 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loader-runner", 3 | "version": "4.3.1", 4 | "description": "Runs (webpack) loaders", 5 | "keywords": ["webpack", "loader"], 6 | "homepage": "https://github.com/webpack/loader-runner#readme", 7 | "bugs": { 8 | "url": "https://github.com/webpack/loader-runner/issues" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/webpack/loader-runner.git" 13 | }, 14 | "funding": { 15 | "type": "opencollective", 16 | "url": "https://opencollective.com/webpack" 17 | }, 18 | "license": "MIT", 19 | "author": "Tobias Koppers @sokra", 20 | "main": "lib/LoaderRunner.js", 21 | "files": ["lib/", "bin/", "hot/", "web_modules/", "schemas/"], 22 | "scripts": { 23 | "lint": "npm run lint:code && npm run fmt:check", 24 | "lint:code": "eslint --cache .", 25 | "fmt": "npm run fmt:base -- --log-level warn --write", 26 | "fmt:check": "npm run fmt:base -- --check", 27 | "fmt:base": "prettier --cache --ignore-unknown .", 28 | "fix": "npm run fix:code && npm run fmt", 29 | "fix:code": "npm run lint:code -- --fix", 30 | "pretest": "npm run lint", 31 | "test": "npm run test:basic", 32 | "test:basic": "mocha --reporter spec", 33 | "test:cover": "nyc --reporter=lcov npm run test:basic" 34 | }, 35 | "devDependencies": { 36 | "@eslint/js": "^9.28.0", 37 | "@eslint/markdown": "^7.1.0", 38 | "@stylistic/eslint-plugin": "^5.2.3", 39 | "globals": "^16.2.0", 40 | "eslint": "^9.28.0", 41 | "eslint-config-webpack": "^4.6.1", 42 | "eslint-config-prettier": "^10.1.5", 43 | "eslint-plugin-import": "^2.31.0", 44 | "eslint-plugin-jest": "^28.12.0", 45 | "eslint-plugin-jsdoc": "^54.1.1", 46 | "eslint-plugin-n": "^17.19.0", 47 | "eslint-plugin-prettier": "^5.4.1", 48 | "eslint-plugin-unicorn": "^60.0.0", 49 | "prettier": "^3.5.3", 50 | "nyc": "^14.1.1", 51 | "mocha": "^3.2.0", 52 | "should": "^8.0.2" 53 | }, 54 | "engines": { 55 | "node": ">=6.11.5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/loadLoader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const LoaderLoadingError = require("./LoaderLoadingError"); 4 | 5 | let url; 6 | 7 | function handleResult(loader, module, callback) { 8 | if (typeof module !== "function" && typeof module !== "object") { 9 | return callback( 10 | new LoaderLoadingError( 11 | `Module '${ 12 | loader.path 13 | }' is not a loader (export function or es6 module)` 14 | ) 15 | ); 16 | } 17 | 18 | loader.normal = typeof module === "function" ? module : module.default; 19 | loader.pitch = module.pitch; 20 | loader.raw = module.raw; 21 | 22 | if ( 23 | typeof loader.normal !== "function" && 24 | typeof loader.pitch !== "function" 25 | ) { 26 | return callback( 27 | new LoaderLoadingError( 28 | `Module '${ 29 | loader.path 30 | }' is not a loader (must have normal or pitch function)` 31 | ) 32 | ); 33 | } 34 | callback(); 35 | } 36 | 37 | module.exports = function loadLoader(loader, callback) { 38 | if (loader.type === "module") { 39 | try { 40 | if (url === undefined) url = require("url"); 41 | 42 | // eslint-disable-next-line n/no-unsupported-features/node-builtins 43 | const loaderUrl = url.pathToFileURL(loader.path); 44 | // eslint-disable-next-line no-eval 45 | const modulePromise = eval( 46 | `import(${JSON.stringify(loaderUrl.toString())})` 47 | ); 48 | 49 | modulePromise.then((module) => { 50 | handleResult(loader, module, callback); 51 | }, callback); 52 | } catch (err) { 53 | callback(err); 54 | } 55 | } else { 56 | let loadedModule; 57 | 58 | try { 59 | loadedModule = require(loader.path); 60 | } catch (err) { 61 | // it is possible for node to choke on a require if the FD descriptor 62 | // limit has been reached. give it a chance to recover. 63 | if (err instanceof Error && err.code === "EMFILE") { 64 | const retry = loadLoader.bind(null, loader, callback); 65 | 66 | if (typeof setImmediate === "function") { 67 | // node >= 0.9.0 68 | return setImmediate(retry); 69 | } 70 | 71 | // node < 0.9.0 72 | return process.nextTick(retry); 73 | } 74 | 75 | return callback(err); 76 | } 77 | 78 | return handleResult(loader, loadedModule, callback); 79 | } 80 | }; 81 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - name: Use Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: lts/* 20 | cache: "npm" 21 | - run: yarn --frozen-lockfile 22 | - name: Cache eslint result 23 | uses: actions/cache@v4 24 | with: 25 | path: .eslintcache 26 | key: lint-eslint-${{ runner.os }}-node-${{ hashFiles('**/package-lock.json', '**/eslint.config.mjs') }} 27 | restore-keys: lint-eslint- 28 | - run: npm run lint 29 | test: 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | os: [ubuntu-latest, windows-latest, macos-latest] 34 | node-version: [6.x, 8.x, 10.x, 12.x, 14.x, 16.x, 18.x, 20.x, 22.x, 24.x] 35 | exclude: 36 | - os: windows-latest 37 | node-version: 6.x 38 | - os: macos-latest 39 | node-version: 6.x 40 | runs-on: ${{ matrix.os }} 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: actions/github-script@v7 44 | id: calculate_architecture 45 | with: 46 | result-encoding: string 47 | script: | 48 | if ('${{ matrix.os }}' === 'macos-latest' && ('${{ matrix['node-version'] }}' === '6.x' || '${{ matrix['node-version'] }}' === '8.x' || '${{ matrix['node-version'] }}' === '10.x' || '${{ matrix['node-version'] }}' === '12.x' || '${{ matrix['node-version'] }}' === '14.x')) { 49 | return "x64" 50 | } else { 51 | return '' 52 | } 53 | - name: Use Node.js ${{ matrix.node-version }} 54 | uses: actions/setup-node@v4 55 | with: 56 | node-version: ${{ matrix.node-version }} 57 | architecture: ${{ steps.calculate_architecture.outputs.result }} 58 | if: matrix.node-version == '6.x' 59 | - name: Use Node.js ${{ matrix.node-version }} 60 | uses: actions/setup-node@v4 61 | with: 62 | node-version: ${{ matrix.node-version }} 63 | architecture: ${{ steps.calculate_architecture.outputs.result }} 64 | cache: "npm" 65 | if: matrix.node-version != '6.x' 66 | - run: | 67 | sudo npm i npm@6.x 68 | sudo npm install --ignore-engines 69 | sudo npm run test:cover 70 | if: matrix.node-version == '6.x' 71 | - run: npm install --ignore-engines 72 | if: matrix.node-version != '6.x' && matrix.node-version == '8.x' || matrix.node-version == '10.x' || matrix.node-version == '12.x' || matrix.node-version == '14.x' || matrix.node-version == '16.x' 73 | - run: npm ci 74 | if: matrix.node-version != '6.x' && matrix.node-version != '8.x' && matrix.node-version != '10.x' && matrix.node-version != '12.x' && matrix.node-version != '14.x' && matrix.node-version != '16.x' 75 | - run: npm run test:cover 76 | if: matrix.node-version != '6.x' 77 | - uses: codecov/codecov-action@v5 78 | with: 79 | flags: integration 80 | token: ${{ secrets.CODECOV_TOKEN }} 81 | -------------------------------------------------------------------------------- /lib/LoaderRunner.js: -------------------------------------------------------------------------------- 1 | /* 2 | MIT License http://www.opensource.org/licenses/mit-license.php 3 | Author Tobias Koppers @sokra 4 | */ 5 | 6 | "use strict"; 7 | 8 | const fs = require("fs"); 9 | 10 | const readFile = fs.readFile.bind(fs); 11 | 12 | const loadLoader = require("./loadLoader"); 13 | 14 | function utf8BufferToString(buf) { 15 | const str = buf.toString("utf8"); 16 | if (str.charCodeAt(0) === 0xfeff) { 17 | return str.slice(1); 18 | } 19 | return str; 20 | } 21 | 22 | const PATH_QUERY_FRAGMENT_REGEXP = 23 | /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/; 24 | const ZERO_ESCAPE_REGEXP = /\0(.)/g; 25 | 26 | /** 27 | * @param {string} identifier identifier 28 | * @returns {[string, string, string]} parsed identifier 29 | */ 30 | function parseIdentifier(identifier) { 31 | // Fast path for inputs that don't use \0 escaping. 32 | const firstEscape = identifier.indexOf("\0"); 33 | 34 | if (firstEscape < 0) { 35 | const queryStart = identifier.indexOf("?"); 36 | const fragmentStart = identifier.indexOf("#"); 37 | 38 | if (fragmentStart < 0) { 39 | if (queryStart < 0) { 40 | // No fragment, no query 41 | return [identifier, "", ""]; 42 | } 43 | 44 | // Query, no fragment 45 | return [ 46 | identifier.slice(0, queryStart), 47 | identifier.slice(queryStart), 48 | "", 49 | ]; 50 | } 51 | 52 | if (queryStart < 0 || fragmentStart < queryStart) { 53 | // Fragment, no query 54 | return [ 55 | identifier.slice(0, fragmentStart), 56 | "", 57 | identifier.slice(fragmentStart), 58 | ]; 59 | } 60 | 61 | // Query and fragment 62 | return [ 63 | identifier.slice(0, queryStart), 64 | identifier.slice(queryStart, fragmentStart), 65 | identifier.slice(fragmentStart), 66 | ]; 67 | } 68 | 69 | const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier); 70 | 71 | return [ 72 | match[1].replace(ZERO_ESCAPE_REGEXP, "$1"), 73 | match[2] ? match[2].replace(ZERO_ESCAPE_REGEXP, "$1") : "", 74 | match[3] || "", 75 | ]; 76 | } 77 | 78 | function dirname(path) { 79 | if (path === "/") return "/"; 80 | const i = path.lastIndexOf("/"); 81 | const j = path.lastIndexOf("\\"); 82 | const i2 = path.indexOf("/"); 83 | const j2 = path.indexOf("\\"); 84 | const idx = i > j ? i : j; 85 | const idx2 = i > j ? i2 : j2; 86 | if (idx < 0) return path; 87 | if (idx === idx2) return path.slice(0, idx + 1); 88 | return path.slice(0, idx); 89 | } 90 | 91 | function createLoaderObject(loader) { 92 | const obj = { 93 | path: null, 94 | query: null, 95 | fragment: null, 96 | options: null, 97 | ident: null, 98 | normal: null, 99 | pitch: null, 100 | raw: null, 101 | data: null, 102 | pitchExecuted: false, 103 | normalExecuted: false, 104 | }; 105 | Object.defineProperty(obj, "request", { 106 | enumerable: true, 107 | get() { 108 | return ( 109 | obj.path.replace(/#/g, "\0#") + 110 | obj.query.replace(/#/g, "\0#") + 111 | obj.fragment 112 | ); 113 | }, 114 | set(value) { 115 | if (typeof value === "string") { 116 | const [path, query, fragment] = parseIdentifier(value); 117 | obj.path = path; 118 | obj.query = query; 119 | obj.fragment = fragment; 120 | obj.options = undefined; 121 | obj.ident = undefined; 122 | } else { 123 | if (!value.loader) { 124 | throw new Error( 125 | `request should be a string or object with loader and options (${JSON.stringify( 126 | value 127 | )})` 128 | ); 129 | } 130 | obj.path = value.loader; 131 | obj.fragment = value.fragment || ""; 132 | obj.type = value.type; 133 | obj.options = value.options; 134 | obj.ident = value.ident; 135 | if (obj.options === null) { 136 | obj.query = ""; 137 | } else if (obj.options === undefined) { 138 | obj.query = ""; 139 | } else if (typeof obj.options === "string") { 140 | obj.query = `?${obj.options}`; 141 | } else if (obj.ident) { 142 | obj.query = `??${obj.ident}`; 143 | } else if (typeof obj.options === "object" && obj.options.ident) { 144 | obj.query = `??${obj.options.ident}`; 145 | } else { 146 | obj.query = `?${JSON.stringify(obj.options)}`; 147 | } 148 | } 149 | }, 150 | }); 151 | obj.request = loader; 152 | if (Object.preventExtensions) { 153 | Object.preventExtensions(obj); 154 | } 155 | return obj; 156 | } 157 | 158 | function runSyncOrAsync(fn, context, args, callback) { 159 | let isSync = true; 160 | let isDone = false; 161 | let isError = false; // internal error 162 | let reportedError = false; 163 | 164 | // eslint-disable-next-line func-name-matching 165 | const innerCallback = (context.callback = function innerCallback() { 166 | if (isDone) { 167 | if (reportedError) return; // ignore 168 | throw new Error("callback(): The callback was already called."); 169 | } 170 | 171 | isDone = true; 172 | isSync = false; 173 | 174 | try { 175 | callback.apply(null, arguments); 176 | } catch (err) { 177 | isError = true; 178 | throw err; 179 | } 180 | }); 181 | 182 | context.async = function async() { 183 | if (isDone) { 184 | if (reportedError) return; // ignore 185 | throw new Error("async(): The callback was already called."); 186 | } 187 | 188 | isSync = false; 189 | 190 | return innerCallback; 191 | }; 192 | 193 | try { 194 | const result = (function LOADER_EXECUTION() { 195 | return fn.apply(context, args); 196 | })(); 197 | if (isSync) { 198 | isDone = true; 199 | if (result === undefined) return callback(); 200 | if ( 201 | result && 202 | typeof result === "object" && 203 | typeof result.then === "function" 204 | ) { 205 | return result.then((r) => { 206 | callback(null, r); 207 | }, callback); 208 | } 209 | return callback(null, result); 210 | } 211 | } catch (err) { 212 | if (isError) throw err; 213 | if (isDone) { 214 | // loader is already "done", so we cannot use the callback function 215 | // for better debugging we print the error on the console 216 | if (typeof err === "object" && err.stack) { 217 | // eslint-disable-next-line no-console 218 | console.error(err.stack); 219 | } else { 220 | // eslint-disable-next-line no-console 221 | console.error(err); 222 | } 223 | return; 224 | } 225 | isDone = true; 226 | reportedError = true; 227 | callback(err); 228 | } 229 | } 230 | 231 | function convertArgs(args, raw) { 232 | if (!raw && Buffer.isBuffer(args[0])) { 233 | args[0] = utf8BufferToString(args[0]); 234 | } else if (raw && typeof args[0] === "string") { 235 | args[0] = Buffer.from(args[0], "utf8"); 236 | } 237 | } 238 | 239 | function iterateNormalLoaders(options, loaderContext, args, callback) { 240 | if (loaderContext.loaderIndex < 0) return callback(null, args); 241 | 242 | const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; 243 | 244 | // iterate 245 | if (currentLoaderObject.normalExecuted) { 246 | loaderContext.loaderIndex--; 247 | return iterateNormalLoaders(options, loaderContext, args, callback); 248 | } 249 | 250 | const fn = currentLoaderObject.normal; 251 | currentLoaderObject.normalExecuted = true; 252 | if (!fn) { 253 | return iterateNormalLoaders(options, loaderContext, args, callback); 254 | } 255 | 256 | convertArgs(args, currentLoaderObject.raw); 257 | 258 | runSyncOrAsync(fn, loaderContext, args, function runSyncOrAsyncCallback(err) { 259 | if (err) return callback(err); 260 | 261 | const args = Array.prototype.slice.call(arguments, 1); 262 | iterateNormalLoaders(options, loaderContext, args, callback); 263 | }); 264 | } 265 | 266 | function processResource(options, loaderContext, callback) { 267 | // set loader index to last loader 268 | loaderContext.loaderIndex = loaderContext.loaders.length - 1; 269 | 270 | const { resourcePath } = loaderContext; 271 | 272 | if (resourcePath) { 273 | options.processResource( 274 | loaderContext, 275 | resourcePath, 276 | function processResourceCallback(err) { 277 | if (err) return callback(err); 278 | const args = Array.prototype.slice.call(arguments, 1); 279 | 280 | [options.resourceBuffer] = args; 281 | 282 | iterateNormalLoaders(options, loaderContext, args, callback); 283 | } 284 | ); 285 | } else { 286 | iterateNormalLoaders(options, loaderContext, [null], callback); 287 | } 288 | } 289 | 290 | function iteratePitchingLoaders(options, loaderContext, callback) { 291 | // abort after last loader 292 | if (loaderContext.loaderIndex >= loaderContext.loaders.length) { 293 | return processResource(options, loaderContext, callback); 294 | } 295 | 296 | const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; 297 | 298 | // iterate 299 | if (currentLoaderObject.pitchExecuted) { 300 | loaderContext.loaderIndex++; 301 | return iteratePitchingLoaders(options, loaderContext, callback); 302 | } 303 | 304 | // load loader module 305 | loadLoader(currentLoaderObject, (err) => { 306 | if (err) { 307 | loaderContext.cacheable(false); 308 | return callback(err); 309 | } 310 | const fn = currentLoaderObject.pitch; 311 | currentLoaderObject.pitchExecuted = true; 312 | if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); 313 | 314 | runSyncOrAsync( 315 | fn, 316 | loaderContext, 317 | [ 318 | loaderContext.remainingRequest, 319 | loaderContext.previousRequest, 320 | (currentLoaderObject.data = {}), 321 | ], 322 | function runSyncOrAsyncCallback(err) { 323 | if (err) return callback(err); 324 | const args = Array.prototype.slice.call(arguments, 1); 325 | // Determine whether to continue the pitching process based on 326 | // argument values (as opposed to argument presence) in order 327 | // to support synchronous and asynchronous usages. 328 | const hasArg = args.some((value) => value !== undefined); 329 | if (hasArg) { 330 | loaderContext.loaderIndex--; 331 | iterateNormalLoaders(options, loaderContext, args, callback); 332 | } else { 333 | iteratePitchingLoaders(options, loaderContext, callback); 334 | } 335 | } 336 | ); 337 | }); 338 | } 339 | 340 | module.exports.getContext = function getContext(resource) { 341 | const [path] = parseIdentifier(resource); 342 | return dirname(path); 343 | }; 344 | 345 | module.exports.runLoaders = function runLoaders(options, callback) { 346 | // read options 347 | const resource = options.resource || ""; 348 | let loaders = options.loaders || []; 349 | const loaderContext = options.context || {}; 350 | const processResource = 351 | options.processResource || 352 | ((readResource, context, resource, callback) => { 353 | context.addDependency(resource); 354 | readResource(resource, callback); 355 | }).bind(null, options.readResource || readFile); 356 | 357 | const splittedResource = resource && parseIdentifier(resource); 358 | const resourcePath = splittedResource ? splittedResource[0] : ""; 359 | const resourceQuery = splittedResource ? splittedResource[1] : ""; 360 | const resourceFragment = splittedResource ? splittedResource[2] : ""; 361 | const contextDirectory = resourcePath ? dirname(resourcePath) : null; 362 | 363 | // execution state 364 | let requestCacheable = true; 365 | const fileDependencies = []; 366 | const contextDependencies = []; 367 | const missingDependencies = []; 368 | 369 | // prepare loader objects 370 | loaders = loaders.map(createLoaderObject); 371 | 372 | loaderContext.context = contextDirectory; 373 | loaderContext.loaderIndex = 0; 374 | loaderContext.loaders = loaders; 375 | loaderContext.resourcePath = resourcePath; 376 | loaderContext.resourceQuery = resourceQuery; 377 | loaderContext.resourceFragment = resourceFragment; 378 | loaderContext.async = null; 379 | loaderContext.callback = null; 380 | loaderContext.cacheable = function cacheable(flag) { 381 | if (flag === false) { 382 | requestCacheable = false; 383 | } 384 | }; 385 | loaderContext.dependency = loaderContext.addDependency = 386 | function addDependency(file) { 387 | fileDependencies.push(file); 388 | }; 389 | loaderContext.addContextDependency = function addContextDependency(context) { 390 | contextDependencies.push(context); 391 | }; 392 | loaderContext.addMissingDependency = function addMissingDependency(context) { 393 | missingDependencies.push(context); 394 | }; 395 | loaderContext.getDependencies = function getDependencies() { 396 | return [...fileDependencies]; 397 | }; 398 | loaderContext.getContextDependencies = function getContextDependencies() { 399 | return [...contextDependencies]; 400 | }; 401 | loaderContext.getMissingDependencies = function getMissingDependencies() { 402 | return [...missingDependencies]; 403 | }; 404 | loaderContext.clearDependencies = function clearDependencies() { 405 | fileDependencies.length = 0; 406 | contextDependencies.length = 0; 407 | missingDependencies.length = 0; 408 | requestCacheable = true; 409 | }; 410 | Object.defineProperty(loaderContext, "resource", { 411 | enumerable: true, 412 | get() { 413 | return ( 414 | loaderContext.resourcePath.replace(/#/g, "\0#") + 415 | loaderContext.resourceQuery.replace(/#/g, "\0#") + 416 | loaderContext.resourceFragment 417 | ); 418 | }, 419 | set(value) { 420 | const splittedResource = value && parseIdentifier(value); 421 | loaderContext.resourcePath = splittedResource ? splittedResource[0] : ""; 422 | loaderContext.resourceQuery = splittedResource ? splittedResource[1] : ""; 423 | loaderContext.resourceFragment = splittedResource 424 | ? splittedResource[2] 425 | : ""; 426 | }, 427 | }); 428 | Object.defineProperty(loaderContext, "request", { 429 | enumerable: true, 430 | get() { 431 | return loaderContext.loaders 432 | .map((loader) => loader.request) 433 | .concat(loaderContext.resource || "") 434 | .join("!"); 435 | }, 436 | }); 437 | Object.defineProperty(loaderContext, "remainingRequest", { 438 | enumerable: true, 439 | get() { 440 | if ( 441 | loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && 442 | !loaderContext.resource 443 | ) { 444 | return ""; 445 | } 446 | return loaderContext.loaders 447 | .slice(loaderContext.loaderIndex + 1) 448 | .map((loader) => loader.request) 449 | .concat(loaderContext.resource || "") 450 | .join("!"); 451 | }, 452 | }); 453 | Object.defineProperty(loaderContext, "currentRequest", { 454 | enumerable: true, 455 | get() { 456 | return loaderContext.loaders 457 | .slice(loaderContext.loaderIndex) 458 | .map((loader) => loader.request) 459 | .concat(loaderContext.resource || "") 460 | .join("!"); 461 | }, 462 | }); 463 | Object.defineProperty(loaderContext, "previousRequest", { 464 | enumerable: true, 465 | get() { 466 | return loaderContext.loaders 467 | .slice(0, loaderContext.loaderIndex) 468 | .map((loader) => loader.request) 469 | .join("!"); 470 | }, 471 | }); 472 | Object.defineProperty(loaderContext, "query", { 473 | enumerable: true, 474 | get() { 475 | const entry = loaderContext.loaders[loaderContext.loaderIndex]; 476 | return entry.options && typeof entry.options === "object" 477 | ? entry.options 478 | : entry.query; 479 | }, 480 | }); 481 | Object.defineProperty(loaderContext, "data", { 482 | enumerable: true, 483 | get() { 484 | return loaderContext.loaders[loaderContext.loaderIndex].data; 485 | }, 486 | }); 487 | 488 | // finish loader context 489 | if (Object.preventExtensions) { 490 | Object.preventExtensions(loaderContext); 491 | } 492 | 493 | const processOptions = { 494 | resourceBuffer: null, 495 | processResource, 496 | }; 497 | iteratePitchingLoaders(processOptions, loaderContext, (err, result) => { 498 | if (err) { 499 | return callback(err, { 500 | cacheable: requestCacheable, 501 | fileDependencies, 502 | contextDependencies, 503 | missingDependencies, 504 | }); 505 | } 506 | callback(null, { 507 | result, 508 | resourceBuffer: processOptions.resourceBuffer, 509 | cacheable: requestCacheable, 510 | fileDependencies, 511 | contextDependencies, 512 | missingDependencies, 513 | }); 514 | }); 515 | }; 516 | -------------------------------------------------------------------------------- /test/runLoaders.js: -------------------------------------------------------------------------------- 1 | /* globals describe it */ 2 | 3 | "use strict"; 4 | 5 | require("should"); 6 | 7 | const fs = require("fs"); 8 | const path = require("path"); 9 | const { runLoaders } = require("../"); 10 | const { getContext } = require("../"); 11 | 12 | const fixtures = path.resolve(__dirname, "fixtures"); 13 | 14 | describe("runLoaders", () => { 15 | it("should process only a resource", (done) => { 16 | runLoaders( 17 | { 18 | resource: path.resolve(fixtures, "resource.bin"), 19 | }, 20 | (err, result) => { 21 | if (err) return done(err); 22 | result.result.should.be.eql([Buffer.from("resource", "utf8")]); 23 | result.cacheable.should.be.eql(true); 24 | result.fileDependencies.should.be.eql([ 25 | path.resolve(fixtures, "resource.bin"), 26 | ]); 27 | result.contextDependencies.should.be.eql([]); 28 | done(); 29 | } 30 | ); 31 | }); 32 | it("should process a simple sync loader", (done) => { 33 | runLoaders( 34 | { 35 | resource: path.resolve(fixtures, "resource.bin"), 36 | loaders: [path.resolve(fixtures, "simple-loader.js")], 37 | }, 38 | (err, result) => { 39 | if (err) return done(err); 40 | result.result.should.be.eql(["resource-simple"]); 41 | result.cacheable.should.be.eql(true); 42 | result.fileDependencies.should.be.eql([ 43 | path.resolve(fixtures, "resource.bin"), 44 | ]); 45 | result.contextDependencies.should.be.eql([]); 46 | done(); 47 | } 48 | ); 49 | }); 50 | it("should process a simple async loader", (done) => { 51 | runLoaders( 52 | { 53 | resource: path.resolve(fixtures, "resource.bin"), 54 | loaders: [path.resolve(fixtures, "simple-async-loader.js")], 55 | }, 56 | (err, result) => { 57 | if (err) return done(err); 58 | result.result.should.be.eql(["resource-async-simple"]); 59 | result.cacheable.should.be.eql(true); 60 | result.fileDependencies.should.be.eql([ 61 | path.resolve(fixtures, "resource.bin"), 62 | ]); 63 | result.contextDependencies.should.be.eql([]); 64 | done(); 65 | } 66 | ); 67 | }); 68 | it("should process a simple promise loader", (done) => { 69 | runLoaders( 70 | { 71 | resource: path.resolve(fixtures, "resource.bin"), 72 | loaders: [path.resolve(fixtures, "simple-promise-loader.js")], 73 | }, 74 | (err, result) => { 75 | if (err) return done(err); 76 | result.result.should.be.eql(["resource-promise-simple"]); 77 | result.cacheable.should.be.eql(true); 78 | result.fileDependencies.should.be.eql([ 79 | path.resolve(fixtures, "resource.bin"), 80 | ]); 81 | result.contextDependencies.should.be.eql([]); 82 | done(); 83 | } 84 | ); 85 | }); 86 | it("should process multiple simple loaders", (done) => { 87 | runLoaders( 88 | { 89 | resource: path.resolve(fixtures, "resource.bin"), 90 | loaders: [ 91 | path.resolve(fixtures, "simple-async-loader.js"), 92 | path.resolve(fixtures, "simple-loader.js"), 93 | path.resolve(fixtures, "simple-async-loader.js"), 94 | path.resolve(fixtures, "simple-async-loader.js"), 95 | path.resolve(fixtures, "simple-loader.js"), 96 | ], 97 | }, 98 | (err, result) => { 99 | if (err) return done(err); 100 | result.result.should.be.eql([ 101 | "resource-simple-async-simple-async-simple-simple-async-simple", 102 | ]); 103 | result.cacheable.should.be.eql(true); 104 | result.fileDependencies.should.be.eql([ 105 | path.resolve(fixtures, "resource.bin"), 106 | ]); 107 | result.contextDependencies.should.be.eql([]); 108 | done(); 109 | } 110 | ); 111 | }); 112 | it("should process pitching loaders", (done) => { 113 | runLoaders( 114 | { 115 | resource: path.resolve(fixtures, "resource.bin"), 116 | loaders: [ 117 | path.resolve(fixtures, "simple-loader.js"), 118 | path.resolve(fixtures, "pitching-loader.js"), 119 | path.resolve(fixtures, "simple-async-loader.js"), 120 | ], 121 | }, 122 | (err, result) => { 123 | if (err) return done(err); 124 | result.result.should.be.eql([ 125 | `${path.resolve(fixtures, "simple-async-loader.js")}!${path.resolve( 126 | fixtures, 127 | "resource.bin" 128 | )}:${path.resolve(fixtures, "simple-loader.js")}-simple`, 129 | ]); 130 | result.cacheable.should.be.eql(true); 131 | result.fileDependencies.should.be.eql([]); 132 | result.contextDependencies.should.be.eql([]); 133 | done(); 134 | } 135 | ); 136 | }); 137 | it("should interpret explicit `undefined` values from async 'pitch' loaders", (done) => { 138 | runLoaders( 139 | { 140 | resource: path.resolve(fixtures, "resource.bin"), 141 | loaders: [ 142 | path.resolve(fixtures, "simple-loader.js"), 143 | path.resolve(fixtures, "pitch-async-undef-loader.js"), 144 | path.resolve(fixtures, "pitch-promise-undef-loader.js"), 145 | ], 146 | }, 147 | (err, result) => { 148 | if (err) return done(err); 149 | result.result.should.be.eql(["resource-simple"]); 150 | result.cacheable.should.be.eql(true); 151 | result.fileDependencies.should.be.eql([ 152 | path.resolve(fixtures, "resource.bin"), 153 | ]); 154 | result.contextDependencies.should.be.eql([]); 155 | done(); 156 | } 157 | ); 158 | }); 159 | it("should interrupt pitching when async loader completes with any additional non-undefined values", (done) => { 160 | runLoaders( 161 | { 162 | resource: path.resolve(fixtures, "resource.bin"), 163 | loaders: [ 164 | path.resolve(fixtures, "simple-loader.js"), 165 | path.resolve(fixtures, "pitch-async-undef-some-loader.js"), 166 | ], 167 | }, 168 | (err, result) => { 169 | if (err) return done(err); 170 | result.result.should.be.eql(["undefined-simple"]); 171 | result.cacheable.should.be.eql(true); 172 | result.fileDependencies.should.be.eql([]); 173 | result.contextDependencies.should.be.eql([]); 174 | done(); 175 | } 176 | ); 177 | }); 178 | it("should be possible to add dependencies", (done) => { 179 | runLoaders( 180 | { 181 | resource: path.resolve(fixtures, "resource.bin"), 182 | loaders: [path.resolve(fixtures, "dependencies-loader.js")], 183 | }, 184 | (err, result) => { 185 | if (err) return done(err); 186 | result.cacheable.should.be.eql(true); 187 | result.fileDependencies.should.be.eql(["a", "b"]); 188 | result.contextDependencies.should.be.eql(["c"]); 189 | result.missingDependencies.should.be.eql(["d"]); 190 | result.result.should.be.eql([ 191 | `resource\n${JSON.stringify(["a", "b"])}${JSON.stringify([ 192 | "c", 193 | ])}${JSON.stringify(["d"])}`, 194 | ]); 195 | done(); 196 | } 197 | ); 198 | }); 199 | it("should have to correct keys in context", (done) => { 200 | runLoaders( 201 | { 202 | resource: `${path.resolve(fixtures, "resource.bin")}?query#frag`, 203 | loaders: [ 204 | `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 205 | path.resolve(fixtures, "simple-loader.js"), 206 | ], 207 | }, 208 | (err, result) => { 209 | if (err) return done(err); 210 | try { 211 | JSON.parse(result.result[0]).should.be.eql({ 212 | context: fixtures, 213 | resource: `${path.resolve(fixtures, "resource.bin")}?query#frag`, 214 | resourcePath: path.resolve(fixtures, "resource.bin"), 215 | resourceQuery: "?query", 216 | resourceFragment: "#frag", 217 | loaderIndex: 0, 218 | query: "?loader-query", 219 | currentRequest: `${path.resolve( 220 | fixtures, 221 | "keys-loader.js" 222 | )}?loader-query!${path.resolve( 223 | fixtures, 224 | "simple-loader.js" 225 | )}!${path.resolve(fixtures, "resource.bin")}?query#frag`, 226 | remainingRequest: `${path.resolve( 227 | fixtures, 228 | "simple-loader.js" 229 | )}!${path.resolve(fixtures, "resource.bin")}?query#frag`, 230 | previousRequest: "", 231 | request: `${path.resolve( 232 | fixtures, 233 | "keys-loader.js" 234 | )}?loader-query!${path.resolve( 235 | fixtures, 236 | "simple-loader.js" 237 | )}!${path.resolve(fixtures, "resource.bin")}?query#frag`, 238 | data: null, 239 | loaders: [ 240 | { 241 | request: `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 242 | path: path.resolve(fixtures, "keys-loader.js"), 243 | query: "?loader-query", 244 | fragment: "", 245 | data: null, 246 | pitchExecuted: true, 247 | normalExecuted: true, 248 | }, 249 | { 250 | request: path.resolve(fixtures, "simple-loader.js"), 251 | path: path.resolve(fixtures, "simple-loader.js"), 252 | query: "", 253 | fragment: "", 254 | data: null, 255 | pitchExecuted: true, 256 | normalExecuted: true, 257 | }, 258 | ], 259 | }); 260 | } catch (err_) { 261 | return done(err_); 262 | } 263 | done(); 264 | } 265 | ); 266 | }); 267 | it("should have to correct keys in context (with options)", (done) => { 268 | runLoaders( 269 | { 270 | resource: `${path.resolve(fixtures, "resource.bin")}?query`, 271 | loaders: [ 272 | { 273 | loader: path.resolve(fixtures, "keys-loader.js"), 274 | options: { 275 | ident: "ident", 276 | loader: "query", 277 | }, 278 | }, 279 | ], 280 | }, 281 | (err, result) => { 282 | if (err) return done(err); 283 | try { 284 | JSON.parse(result.result[0]).should.be.eql({ 285 | context: fixtures, 286 | resource: `${path.resolve(fixtures, "resource.bin")}?query`, 287 | resourcePath: path.resolve(fixtures, "resource.bin"), 288 | resourceQuery: "?query", 289 | resourceFragment: "", 290 | loaderIndex: 0, 291 | query: { 292 | ident: "ident", 293 | loader: "query", 294 | }, 295 | currentRequest: `${path.resolve( 296 | fixtures, 297 | "keys-loader.js" 298 | )}??ident!${path.resolve(fixtures, "resource.bin")}?query`, 299 | remainingRequest: `${path.resolve(fixtures, "resource.bin")}?query`, 300 | previousRequest: "", 301 | request: `${path.resolve( 302 | fixtures, 303 | "keys-loader.js" 304 | )}??ident!${path.resolve(fixtures, "resource.bin")}?query`, 305 | data: null, 306 | loaders: [ 307 | { 308 | request: `${path.resolve(fixtures, "keys-loader.js")}??ident`, 309 | path: path.resolve(fixtures, "keys-loader.js"), 310 | query: "??ident", 311 | fragment: "", 312 | options: { 313 | ident: "ident", 314 | loader: "query", 315 | }, 316 | data: null, 317 | pitchExecuted: true, 318 | normalExecuted: true, 319 | }, 320 | ], 321 | }); 322 | } catch (err_) { 323 | return done(err_); 324 | } 325 | done(); 326 | } 327 | ); 328 | }); 329 | it("should process raw loaders", (done) => { 330 | runLoaders( 331 | { 332 | resource: path.resolve(fixtures, "bom.bin"), 333 | loaders: [path.resolve(fixtures, "raw-loader.js")], 334 | }, 335 | (err, result) => { 336 | if (err) return done(err); 337 | result.result[0].toString("utf8").should.be.eql("efbbbf62c3b66dböm"); 338 | done(); 339 | } 340 | ); 341 | }); 342 | it("should process omit BOM on string conversion", (done) => { 343 | runLoaders( 344 | { 345 | resource: path.resolve(fixtures, "bom.bin"), 346 | loaders: [ 347 | path.resolve(fixtures, "raw-loader.js"), 348 | path.resolve(fixtures, "simple-loader.js"), 349 | ], 350 | }, 351 | (err, result) => { 352 | if (err) return done(err); 353 | result.result[0] 354 | .toString("utf8") 355 | .should.be.eql("62c3b66d2d73696d706c65böm-simple"); 356 | done(); 357 | } 358 | ); 359 | }); 360 | it("should have to correct keys in context without resource", (done) => { 361 | runLoaders( 362 | { 363 | loaders: [ 364 | path.resolve(fixtures, "identity-loader.js"), 365 | path.resolve(fixtures, "keys-loader.js"), 366 | ], 367 | }, 368 | (err, result) => { 369 | if (err) return done(err); 370 | try { 371 | JSON.parse(result.result[0]).should.be.eql({ 372 | context: null, 373 | resource: "", 374 | resourcePath: "", 375 | resourceQuery: "", 376 | resourceFragment: "", 377 | loaderIndex: 1, 378 | query: "", 379 | currentRequest: `${path.resolve(fixtures, "keys-loader.js")}!`, 380 | remainingRequest: "", 381 | previousRequest: path.resolve(fixtures, "identity-loader.js"), 382 | request: `${path.resolve( 383 | fixtures, 384 | "identity-loader.js" 385 | )}!${path.resolve(fixtures, "keys-loader.js")}!`, 386 | data: null, 387 | loaders: [ 388 | { 389 | request: path.resolve(fixtures, "identity-loader.js"), 390 | path: path.resolve(fixtures, "identity-loader.js"), 391 | query: "", 392 | fragment: "", 393 | data: { 394 | identity: true, 395 | }, 396 | pitchExecuted: true, 397 | normalExecuted: false, 398 | }, 399 | { 400 | request: path.resolve(fixtures, "keys-loader.js"), 401 | path: path.resolve(fixtures, "keys-loader.js"), 402 | query: "", 403 | fragment: "", 404 | data: null, 405 | pitchExecuted: true, 406 | normalExecuted: true, 407 | }, 408 | ], 409 | }); 410 | } catch (err_) { 411 | return done(err_); 412 | } 413 | done(); 414 | } 415 | ); 416 | }); 417 | it("should have to correct keys in context with only resource query", (done) => { 418 | runLoaders( 419 | { 420 | resource: "?query", 421 | loaders: [ 422 | { 423 | loader: path.resolve(fixtures, "keys-loader.js"), 424 | options: { 425 | ok: true, 426 | }, 427 | ident: "my-ident", 428 | }, 429 | ], 430 | }, 431 | (err, result) => { 432 | if (err) return done(err); 433 | try { 434 | JSON.parse(result.result[0]).should.be.eql({ 435 | context: null, 436 | resource: "?query", 437 | resourcePath: "", 438 | resourceQuery: "?query", 439 | resourceFragment: "", 440 | loaderIndex: 0, 441 | query: { 442 | ok: true, 443 | }, 444 | currentRequest: `${path.resolve(fixtures, "keys-loader.js")}??my-ident!?query`, 445 | remainingRequest: "?query", 446 | previousRequest: "", 447 | request: 448 | `${path.resolve(fixtures, "keys-loader.js")}??my-ident!` + 449 | "?query", 450 | data: null, 451 | loaders: [ 452 | { 453 | request: `${path.resolve(fixtures, "keys-loader.js")}??my-ident`, 454 | path: path.resolve(fixtures, "keys-loader.js"), 455 | query: "??my-ident", 456 | fragment: "", 457 | ident: "my-ident", 458 | options: { 459 | ok: true, 460 | }, 461 | data: null, 462 | pitchExecuted: true, 463 | normalExecuted: true, 464 | }, 465 | ], 466 | }); 467 | } catch (err_) { 468 | return done(err_); 469 | } 470 | done(); 471 | } 472 | ); 473 | }); 474 | it("should have to correct keys in context with only resource fragment", (done) => { 475 | runLoaders( 476 | { 477 | resource: "#fragment", 478 | loaders: [ 479 | { 480 | loader: path.resolve(fixtures, "keys-loader.js"), 481 | options: { 482 | ok: true, 483 | }, 484 | ident: "my-ident", 485 | }, 486 | ], 487 | }, 488 | (err, result) => { 489 | if (err) return done(err); 490 | try { 491 | JSON.parse(result.result[0]).should.be.eql({ 492 | context: null, 493 | resource: "#fragment", 494 | resourcePath: "", 495 | resourceQuery: "", 496 | resourceFragment: "#fragment", 497 | loaderIndex: 0, 498 | query: { 499 | ok: true, 500 | }, 501 | currentRequest: `${path.resolve(fixtures, "keys-loader.js")}??my-ident!#fragment`, 502 | remainingRequest: "#fragment", 503 | previousRequest: "", 504 | request: 505 | `${path.resolve(fixtures, "keys-loader.js")}??my-ident!` + 506 | "#fragment", 507 | data: null, 508 | loaders: [ 509 | { 510 | request: `${path.resolve(fixtures, "keys-loader.js")}??my-ident`, 511 | path: path.resolve(fixtures, "keys-loader.js"), 512 | query: "??my-ident", 513 | fragment: "", 514 | ident: "my-ident", 515 | options: { 516 | ok: true, 517 | }, 518 | data: null, 519 | pitchExecuted: true, 520 | normalExecuted: true, 521 | }, 522 | ], 523 | }); 524 | } catch (err_) { 525 | return done(err_); 526 | } 527 | done(); 528 | } 529 | ); 530 | }); 531 | it("should allow to change loader order and execution", (done) => { 532 | runLoaders( 533 | { 534 | resource: path.resolve(fixtures, "bom.bin"), 535 | loaders: [ 536 | path.resolve(fixtures, "change-stuff-loader.js"), 537 | path.resolve(fixtures, "simple-loader.js"), 538 | path.resolve(fixtures, "simple-loader.js"), 539 | ], 540 | }, 541 | (err, result) => { 542 | if (err) return done(err); 543 | result.result.should.be.eql(["resource"]); 544 | done(); 545 | } 546 | ); 547 | }); 548 | it("should return dependencies when resource not found", (done) => { 549 | runLoaders( 550 | { 551 | resource: path.resolve(fixtures, "missing.txt"), 552 | loaders: [path.resolve(fixtures, "pitch-dependencies-loader.js")], 553 | }, 554 | (err, result) => { 555 | err.should.be.instanceOf(Error); 556 | err.message.should.match(/ENOENT/i); 557 | result.fileDependencies.should.be.eql([ 558 | `remainingRequest:${path.resolve(fixtures, "missing.txt")}`, 559 | path.resolve(fixtures, "missing.txt"), 560 | ]); 561 | done(); 562 | } 563 | ); 564 | }); 565 | it("should not return dependencies when loader not found", (done) => { 566 | runLoaders( 567 | { 568 | resource: path.resolve(fixtures, "resource.bin"), 569 | loaders: [path.resolve(fixtures, "does-not-exist-loader.js")], 570 | }, 571 | (err, result) => { 572 | err.should.be.instanceOf(Error); 573 | err.code.should.be.eql("MODULE_NOT_FOUND"); 574 | err.message.should.match(/does-not-exist-loader\.js'($|\n)/i); 575 | result.should.be.eql({ 576 | cacheable: false, 577 | fileDependencies: [], 578 | contextDependencies: [], 579 | missingDependencies: [], 580 | }); 581 | done(); 582 | } 583 | ); 584 | }); 585 | it("should not return dependencies when loader is empty object", (done) => { 586 | runLoaders( 587 | { 588 | resource: path.resolve(fixtures, "resource.bin"), 589 | loaders: [path.resolve(fixtures, "module-exports-object-loader.js")], 590 | }, 591 | (err, result) => { 592 | err.should.be.instanceOf(Error); 593 | err.message.should.match( 594 | /module-exports-object-loader.js' is not a loader \(must have normal or pitch function\)$/ 595 | ); 596 | result.should.be.eql({ 597 | cacheable: false, 598 | fileDependencies: [], 599 | contextDependencies: [], 600 | missingDependencies: [], 601 | }); 602 | done(); 603 | } 604 | ); 605 | }); 606 | it("should not return dependencies when loader is otherwise invalid (string)", (done) => { 607 | runLoaders( 608 | { 609 | resource: path.resolve(fixtures, "resource.bin"), 610 | loaders: [path.resolve(fixtures, "module-exports-string-loader.js")], 611 | }, 612 | (err, result) => { 613 | err.should.be.instanceOf(Error); 614 | err.message.should.match( 615 | /module-exports-string-loader.js' is not a loader \(export function or es6 module\)$/ 616 | ); 617 | result.should.be.eql({ 618 | cacheable: false, 619 | fileDependencies: [], 620 | contextDependencies: [], 621 | missingDependencies: [], 622 | }); 623 | done(); 624 | } 625 | ); 626 | }); 627 | it("should return dependencies when loader throws error", (done) => { 628 | runLoaders( 629 | { 630 | resource: path.resolve(fixtures, "resource.bin"), 631 | loaders: [path.resolve(fixtures, "throws-error-loader.js")], 632 | }, 633 | (err, result) => { 634 | err.should.be.instanceOf(Error); 635 | err.message.should.match(/^resource$/i); 636 | result.fileDependencies.should.be.eql([ 637 | path.resolve(fixtures, "resource.bin"), 638 | ]); 639 | done(); 640 | } 641 | ); 642 | }); 643 | it("should return dependencies when loader rejects promise", (done) => { 644 | let once = true; 645 | runLoaders( 646 | { 647 | resource: path.resolve(fixtures, "resource.bin"), 648 | loaders: [path.resolve(fixtures, "promise-error-loader.js")], 649 | }, 650 | (err, result) => { 651 | if (!once) return done(new Error("should not be called twice")); 652 | once = false; 653 | err.should.be.instanceOf(Error); 654 | err.message.should.match(/^resource$/i); 655 | result.fileDependencies.should.be.eql([ 656 | path.resolve(fixtures, "resource.bin"), 657 | ]); 658 | done(); 659 | } 660 | ); 661 | }); 662 | it("should use an ident if passed", (done) => { 663 | runLoaders( 664 | { 665 | resource: path.resolve(fixtures, "resource.bin"), 666 | loaders: [ 667 | { 668 | loader: path.resolve(fixtures, "pitching-loader.js"), 669 | }, 670 | { 671 | loader: path.resolve(fixtures, "simple-loader.js"), 672 | options: { 673 | f() {}, 674 | }, 675 | ident: "my-ident", 676 | }, 677 | ], 678 | }, 679 | (err, result) => { 680 | if (err) return done(err); 681 | result.result.should.be.eql([ 682 | `${path.resolve( 683 | fixtures, 684 | "simple-loader.js" 685 | )}??my-ident!${path.resolve(fixtures, "resource.bin")}:`, 686 | ]); 687 | done(); 688 | } 689 | ); 690 | }); 691 | it("should load a loader using System.import and process", (done) => { 692 | global.System = { 693 | import(moduleId) { 694 | return Promise.resolve(require(moduleId)); 695 | }, 696 | }; 697 | runLoaders( 698 | { 699 | resource: path.resolve(fixtures, "resource.bin"), 700 | loaders: [path.resolve(fixtures, "simple-loader.js")], 701 | }, 702 | (err, result) => { 703 | if (err) return done(err); 704 | result.result.should.be.eql(["resource-simple"]); 705 | result.cacheable.should.be.eql(true); 706 | result.fileDependencies.should.be.eql([ 707 | path.resolve(fixtures, "resource.bin"), 708 | ]); 709 | result.contextDependencies.should.be.eql([]); 710 | done(); 711 | } 712 | ); 713 | delete global.System; 714 | }); 715 | 716 | if (Number(process.versions.modules) >= 83) { 717 | it("should load a loader using import()", (done) => { 718 | runLoaders( 719 | { 720 | resource: path.resolve(fixtures, "resource.bin"), 721 | loaders: [ 722 | { 723 | loader: path.resolve(fixtures, "esm-loader.mjs"), 724 | type: "module", 725 | }, 726 | ], 727 | }, 728 | (err, result) => { 729 | if (err) return done(err); 730 | result.result.should.be.eql(["resource-esm"]); 731 | result.cacheable.should.be.eql(true); 732 | result.fileDependencies.should.be.eql([ 733 | path.resolve(fixtures, "resource.bin"), 734 | ]); 735 | result.contextDependencies.should.be.eql([]); 736 | done(); 737 | } 738 | ); 739 | }); 740 | } 741 | it("should support escaping in resource", (done) => { 742 | runLoaders( 743 | { 744 | resource: path.resolve(fixtures, "res\0#ource.bin"), 745 | }, 746 | (err, result) => { 747 | if (err) return done(err); 748 | result.result.should.be.eql([Buffer.from("resource", "utf8")]); 749 | result.cacheable.should.be.eql(true); 750 | result.fileDependencies.should.be.eql([ 751 | path.resolve(fixtures, "res#ource.bin"), 752 | ]); 753 | result.contextDependencies.should.be.eql([]); 754 | done(); 755 | } 756 | ); 757 | }); 758 | it("should have to correct keys in context when using escaping", (done) => { 759 | runLoaders( 760 | { 761 | resource: `${path.resolve(fixtures, "res\0#ource.bin")}?query\0#frag`, 762 | loaders: [`${path.resolve(fixtures, "keys-loader.js")}?loader\0#query`], 763 | }, 764 | (err, result) => { 765 | if (err) return done(err); 766 | try { 767 | JSON.parse(result.result[0]).should.be.eql({ 768 | context: fixtures, 769 | resource: `${path.resolve(fixtures, "res\0#ource.bin")}?query\0#frag`, 770 | resourcePath: path.resolve(fixtures, "res#ource.bin"), 771 | resourceQuery: "?query#frag", 772 | resourceFragment: "", 773 | loaderIndex: 0, 774 | query: "?loader#query", 775 | currentRequest: `${path.resolve( 776 | fixtures, 777 | "keys-loader.js" 778 | )}?loader\0#query!${path.resolve( 779 | fixtures, 780 | "res\0#ource.bin" 781 | )}?query\0#frag`, 782 | remainingRequest: `${path.resolve(fixtures, "res\0#ource.bin")}?query\0#frag`, 783 | previousRequest: "", 784 | request: `${path.resolve( 785 | fixtures, 786 | "keys-loader.js" 787 | )}?loader\0#query!${path.resolve( 788 | fixtures, 789 | "res\0#ource.bin" 790 | )}?query\0#frag`, 791 | data: null, 792 | loaders: [ 793 | { 794 | request: `${path.resolve(fixtures, "keys-loader.js")}?loader\0#query`, 795 | path: path.resolve(fixtures, "keys-loader.js"), 796 | query: "?loader#query", 797 | fragment: "", 798 | data: null, 799 | pitchExecuted: true, 800 | normalExecuted: true, 801 | }, 802 | ], 803 | }); 804 | } catch (err_) { 805 | return done(err_); 806 | } 807 | done(); 808 | } 809 | ); 810 | }); 811 | 812 | it("should have to correct keys in context with empty resource", (done) => { 813 | runLoaders( 814 | { 815 | resource: "", 816 | loaders: [ 817 | `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 818 | path.resolve(fixtures, "simple-loader.js"), 819 | ], 820 | }, 821 | (err, result) => { 822 | if (err) return done(err); 823 | try { 824 | JSON.parse(result.result[0]).should.be.eql({ 825 | context: null, 826 | resource: "", 827 | resourcePath: "", 828 | resourceQuery: "", 829 | resourceFragment: "", 830 | loaderIndex: 0, 831 | query: "?loader-query", 832 | currentRequest: `${path.resolve( 833 | fixtures, 834 | "keys-loader.js" 835 | )}?loader-query!${path.resolve(fixtures, "simple-loader.js")}!`, 836 | remainingRequest: `${path.resolve(fixtures, "simple-loader.js")}!`, 837 | previousRequest: "", 838 | request: `${path.resolve( 839 | fixtures, 840 | "keys-loader.js" 841 | )}?loader-query!${path.resolve(fixtures, "simple-loader.js")}!`, 842 | data: null, 843 | loaders: [ 844 | { 845 | request: `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 846 | path: path.resolve(fixtures, "keys-loader.js"), 847 | query: "?loader-query", 848 | fragment: "", 849 | data: null, 850 | pitchExecuted: true, 851 | normalExecuted: true, 852 | }, 853 | { 854 | request: path.resolve(fixtures, "simple-loader.js"), 855 | path: path.resolve(fixtures, "simple-loader.js"), 856 | query: "", 857 | fragment: "", 858 | data: null, 859 | pitchExecuted: true, 860 | normalExecuted: true, 861 | }, 862 | ], 863 | }); 864 | } catch (err_) { 865 | return done(err_); 866 | } 867 | done(); 868 | } 869 | ); 870 | }); 871 | 872 | it("should have to correct keys in context with empty resource and set a new resource", (done) => { 873 | runLoaders( 874 | { 875 | resource: "", 876 | loaders: [ 877 | `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 878 | path.resolve(fixtures, "set-resource-loader.js"), 879 | ], 880 | }, 881 | (err, result) => { 882 | if (err) return done(err); 883 | try { 884 | JSON.parse(result.result[0]).should.be.eql({ 885 | context: null, 886 | resource: `${path.resolve(fixtures, "resource.bin")}?foo=bar#hash`, 887 | resourcePath: path.resolve(fixtures, "resource.bin"), 888 | resourceQuery: "?foo=bar", 889 | resourceFragment: "#hash", 890 | loaderIndex: 0, 891 | query: "?loader-query", 892 | currentRequest: `${path.resolve( 893 | fixtures, 894 | "keys-loader.js" 895 | )}?loader-query!${path.resolve(fixtures, "set-resource-loader.js")}!${path.resolve(fixtures, "resource.bin")}?foo=bar#hash`, 896 | remainingRequest: `${path.resolve(fixtures, "set-resource-loader.js")}!${path.resolve(fixtures, "resource.bin")}?foo=bar#hash`, 897 | previousRequest: "", 898 | request: `${path.resolve( 899 | fixtures, 900 | "keys-loader.js" 901 | )}?loader-query!${path.resolve(fixtures, "set-resource-loader.js")}!${path.resolve(fixtures, "resource.bin")}?foo=bar#hash`, 902 | data: null, 903 | loaders: [ 904 | { 905 | request: `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 906 | path: path.resolve(fixtures, "keys-loader.js"), 907 | query: "?loader-query", 908 | fragment: "", 909 | data: null, 910 | pitchExecuted: true, 911 | normalExecuted: true, 912 | }, 913 | { 914 | request: path.resolve(fixtures, "set-resource-loader.js"), 915 | path: path.resolve(fixtures, "set-resource-loader.js"), 916 | query: "", 917 | fragment: "", 918 | data: null, 919 | pitchExecuted: true, 920 | normalExecuted: true, 921 | }, 922 | ], 923 | }); 924 | } catch (err_) { 925 | return done(err_); 926 | } 927 | done(); 928 | } 929 | ); 930 | }); 931 | 932 | it("should have to correct keys in context with resource and set a new resource", (done) => { 933 | runLoaders( 934 | { 935 | resource: path.resolve(fixtures, "resource.bin"), 936 | loaders: [ 937 | `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 938 | path.resolve(fixtures, "set-empty-resource-loader.js"), 939 | ], 940 | }, 941 | (err, result) => { 942 | if (err) return done(err); 943 | try { 944 | JSON.parse(result.result[0]).should.be.eql({ 945 | context: fixtures, 946 | resource: "", 947 | resourcePath: "", 948 | resourceQuery: "", 949 | resourceFragment: "", 950 | loaderIndex: 0, 951 | query: "?loader-query", 952 | currentRequest: `${path.resolve( 953 | fixtures, 954 | "keys-loader.js" 955 | )}?loader-query!${path.resolve(fixtures, "set-empty-resource-loader.js")}!`, 956 | remainingRequest: `${path.resolve(fixtures, "set-empty-resource-loader.js")}!`, 957 | previousRequest: "", 958 | request: `${path.resolve( 959 | fixtures, 960 | "keys-loader.js" 961 | )}?loader-query!${path.resolve(fixtures, "set-empty-resource-loader.js")}!`, 962 | data: null, 963 | loaders: [ 964 | { 965 | request: `${path.resolve(fixtures, "keys-loader.js")}?loader-query`, 966 | path: path.resolve(fixtures, "keys-loader.js"), 967 | query: "?loader-query", 968 | fragment: "", 969 | data: null, 970 | pitchExecuted: true, 971 | normalExecuted: true, 972 | }, 973 | { 974 | request: path.resolve(fixtures, "set-empty-resource-loader.js"), 975 | path: path.resolve(fixtures, "set-empty-resource-loader.js"), 976 | query: "", 977 | fragment: "", 978 | data: null, 979 | pitchExecuted: true, 980 | normalExecuted: true, 981 | }, 982 | ], 983 | }); 984 | } catch (err_) { 985 | return done(err_); 986 | } 987 | done(); 988 | } 989 | ); 990 | }); 991 | 992 | describe("getContext", () => { 993 | const TESTS = [ 994 | ["/", "/"], 995 | ["/path/file.js", "/path"], 996 | ["/path/file.js#fragment", "/path"], 997 | ["/path/file.js?query", "/path"], 998 | ["/path/file.js?query#fragment", "/path"], 999 | ["/path/\0#/file.js", "/path/#"], 1000 | ["/some/longer/path/file.js", "/some/longer/path"], 1001 | ["/file.js", "/"], 1002 | ["C:\\", "C:\\"], 1003 | ["C:\\file.js", "C:\\"], 1004 | ["C:\\some\\path\\file.js", "C:\\some\\path"], 1005 | ["C:\\path\\file.js", "C:\\path"], 1006 | ["C:\\path\\file.js#fragment", "C:\\path"], 1007 | ["C:\\path\\file.js?query", "C:\\path"], 1008 | ["C:\\path\\file.js?query#fragment", "C:\\path"], 1009 | ["C:\\path\\\0#\\file.js", "C:\\path\\#"], 1010 | ]; 1011 | for (const testCase of TESTS) { 1012 | it(`should get the context of '${testCase[0]}'`, () => { 1013 | getContext(testCase[0]).should.be.eql(testCase[1]); 1014 | }); 1015 | } 1016 | }); 1017 | it("should pass arguments from processResource", (done) => { 1018 | runLoaders( 1019 | { 1020 | resource: path.resolve(fixtures, "resource.bin"), 1021 | processResource(loaderContext, resourcePath, callback) { 1022 | fs.readFile(resourcePath, (err, content) => { 1023 | if (err) return callback(err); 1024 | return callback(null, content, "source-map", "other-arg"); 1025 | }); 1026 | }, 1027 | }, 1028 | (err, result) => { 1029 | if (err) return done(err); 1030 | result.result.should.be.eql([ 1031 | Buffer.from("resource", "utf8"), 1032 | "source-map", 1033 | "other-arg", 1034 | ]); 1035 | done(); 1036 | } 1037 | ); 1038 | }); 1039 | }); 1040 | --------------------------------------------------------------------------------