From 603a120a6e325a480fdc709993ba7f65b3d4fb63 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Wed, 18 Sep 2024 23:30:43 +0300 Subject: [PATCH 1/6] fix: support `utils.createHash` --- src/worker.js | 13 ++++++++++++- test/__snapshots__/webpack.test.js.snap | 4 +++- test/basic-loader-test/test-loader.js | 11 ++++++++--- test/webpack.test.js | 2 +- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/worker.js b/src/worker.js index 6f1c6d8..b5d24e3 100644 --- a/src/worker.js +++ b/src/worker.js @@ -205,7 +205,7 @@ const queue = asyncQueue(({ id, data }, taskCallback) => { let { options } = loader; if (typeof options === 'string') { - if (options.substr(0, 1) === '{' && options.substr(-1) === '}') { + if (options.startsWith('{') && options.endsWith('}')) { try { options = parseJson(options); } catch (e) { @@ -359,6 +359,17 @@ const queue = asyncQueue(({ id, data }, taskCallback) => { options: { context: data.optionsContext, }, + utils: { + createHash: (type) => { + // eslint-disable-next-line global-require + const { createHash } = require('webpack').util; + + return createHash( + // eslint-disable-next-line no-underscore-dangle + type || data._compilation.outputOptions.hashFunction, + ); + }, + }, webpack: true, 'thread-loader': true, mode: data.mode, diff --git a/test/__snapshots__/webpack.test.js.snap b/test/__snapshots__/webpack.test.js.snap index 44a3205..d585503 100644 --- a/test/__snapshots__/webpack.test.js.snap +++ b/test/__snapshots__/webpack.test.js.snap @@ -127,7 +127,9 @@ module.exports = new URL('./style.less', import.meta.url); "utils": { "absolutify": "undefined", "contextify": "undefined", - "createHash": "undefined", + "createHash": "function", + "createHashResult": "db346d691d7acc4dc2625db19f9e3f52", + "createHashResult1": "4fdcca5ddb678139", }, "version": 2, "webpack": true, diff --git a/test/basic-loader-test/test-loader.js b/test/basic-loader-test/test-loader.js index 2b9b3e0..a1795bf 100644 --- a/test/basic-loader-test/test-loader.js +++ b/test/basic-loader-test/test-loader.js @@ -71,9 +71,14 @@ module.exports = async function testLoader() { emitFile: typeof this.emitFile, addBuildDependency: typeof this.addBuildDependency, utils: { - absolutify: typeof this.absolutify, - contextify: typeof this.contextify, - createHash: typeof this.createHash, + absolutify: typeof this.utils.absolutify, + contextify: typeof this.utils.contextify, + createHash: typeof this.utils.createHash, + createHashResult: this.utils.createHash().update('test').digest('hex'), + createHashResult1: this.utils + .createHash('xxhash64') + .update('test') + .digest('hex'), }, loadModule: typeof this.loadModule, loadModuleResult: await new Promise((resolve, reject) => diff --git a/test/webpack.test.js b/test/webpack.test.js index 7be5011..fa0155d 100644 --- a/test/webpack.test.js +++ b/test/webpack.test.js @@ -47,7 +47,7 @@ test('Works with less-loader', (done) => { }); }, 30000); -test('Works with test-loader', (done) => { +test.only('Works with test-loader', (done) => { const config = basicLoaderConfig({ threads: 1 }); webpack(config, (err, stats) => { From 6cec88afcbc611071569cedc3cdf897fb796d23d Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 19 Sep 2024 00:17:59 +0300 Subject: [PATCH 2/6] fix: support `utils.createHash` --- src/index.js | 1 + src/template.js | 355 ++++++++++++++++++++++ src/worker.js | 15 + test/css-loader-example/index.js | 4 + test/css-loader-example/style.css | 3 + test/css-loader-example/style.modules.css | 3 + test/css-loader-example/webpack.config.js | 43 +++ test/webpack.test.js | 31 +- 8 files changed, 449 insertions(+), 6 deletions(-) create mode 100644 src/template.js create mode 100644 test/css-loader-example/index.js create mode 100644 test/css-loader-example/style.css create mode 100644 test/css-loader-example/style.modules.css create mode 100644 test/css-loader-example/webpack.config.js diff --git a/src/index.js b/src/index.js index 7bc5fee..9595a58 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ function pitch() { options: { plugins: [] }, }, _compilation: { + hash: "tsst", outputOptions: { hashSalt: this._compilation.outputOptions.hashSalt, hashFunction: this._compilation.outputOptions.hashFunction, diff --git a/src/template.js b/src/template.js new file mode 100644 index 0000000..992e7c8 --- /dev/null +++ b/src/template.js @@ -0,0 +1,355 @@ +// TODO export it from webpack + +const { basename, extname } = require('path'); +const util = require('util'); + +const { Chunk } = require('webpack'); +const { Module } = require('webpack'); +const { parseResource } = require('webpack/lib/util/identifier'); + +const REGEXP = /\[\\*([\w:]+)\\*\]/gi; + +/** + * @param {string | number} id id + * @returns {string | number} result + */ +const prepareId = (id) => { + if (typeof id !== 'string') return id; + + if (/^"\s\+*.*\+\s*"$/.test(id)) { + const match = /^"\s\+*\s*(.*)\s*\+\s*"$/.exec(id); + + return `" + (${ + /** @type {string[]} */ (match)[1] + } + "").replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, "_") + "`; + } + + return id.replace(/(^[.-]|[^a-zA-Z0-9_-])+/g, '_'); +}; + +/** + * @callback ReplacerFunction + * @param {string} match + * @param {string | undefined} arg + * @param {string} input + */ + +/** + * @param {ReplacerFunction} replacer replacer + * @param {((arg0: number) => string) | undefined} handler handler + * @param {AssetInfo | undefined} assetInfo asset info + * @param {string} hashName hash name + * @returns {ReplacerFunction} hash replacer function + */ +const hashLength = (replacer, handler, assetInfo, hashName) => { + /** @type {ReplacerFunction} */ + const fn = (match, arg, input) => { + let result; + const length = arg && Number.parseInt(arg, 10); + + if (length && handler) { + result = handler(length); + } else { + const hash = replacer(match, arg, input); + + result = length ? hash.slice(0, length) : hash; + } + if (assetInfo) { + // eslint-disable-next-line no-param-reassign + assetInfo.immutable = true; + if (Array.isArray(assetInfo[hashName])) { + // eslint-disable-next-line no-param-reassign + assetInfo[hashName] = [...assetInfo[hashName], result]; + } else if (assetInfo[hashName]) { + // eslint-disable-next-line no-param-reassign + assetInfo[hashName] = [assetInfo[hashName], result]; + } else { + // eslint-disable-next-line no-param-reassign + assetInfo[hashName] = result; + } + } + return result; + }; + + return fn; +}; + +/** @typedef {(match: string, arg?: string, input?: string) => string} Replacer */ + +/** + * @param {string | number | null | undefined | (() => string | number | null | undefined)} value value + * @param {boolean=} allowEmpty allow empty + * @returns {Replacer} replacer + */ +const replacer = (value, allowEmpty) => { + /** @type {Replacer} */ + const fn = (match, arg, input) => { + if (typeof value === 'function') { + // eslint-disable-next-line no-param-reassign + value = value(); + } + // eslint-disable-next-line no-undefined + if (value === null || value === undefined) { + if (!allowEmpty) { + throw new Error( + `Path variable ${match} not implemented in this context: ${input}`, + ); + } + + return ''; + } + + return `${value}`; + }; + + return fn; +}; + +const deprecationCache = new Map(); +const deprecatedFunction = (() => () => {})(); +/** + * @param {Function} fn function + * @param {string} message message + * @param {string} code code + * @returns {function(...any[]): void} function with deprecation output + */ +const deprecated = (fn, message, code) => { + let d = deprecationCache.get(message); + // eslint-disable-next-line no-undefined + if (d === undefined) { + d = util.deprecate(deprecatedFunction, message, code); + deprecationCache.set(message, d); + } + return (...args) => { + d(); + return fn(...args); + }; +}; + +/** @typedef {string | function(PathData, AssetInfo=): string} TemplatePath */ + +/** + * @param {TemplatePath} path the raw path + * @param {PathData} data context data + * @param {AssetInfo | undefined} assetInfo extra info about the asset (will be written to) + * @returns {string} the interpolated path + */ +const replacePathVariables = (path, data, assetInfo) => { + const { chunkGraph } = data; + + /** @type {Map} */ + const replacements = new Map(); + + // Filename context + // + // Placeholders + // + // for /some/path/file.js?query#fragment: + // [file] - /some/path/file.js + // [query] - ?query + // [fragment] - #fragment + // [base] - file.js + // [path] - /some/path/ + // [name] - file + // [ext] - .js + if (typeof data.filename === 'string') { + const { path: file, query, fragment } = parseResource(data.filename); + + const ext = extname(file); + const base = basename(file); + const name = base.slice(0, base.length - ext.length); + // eslint-disable-next-line no-shadow + const path = file.slice(0, file.length - base.length); + + replacements.set('file', replacer(file)); + replacements.set('query', replacer(query, true)); + replacements.set('fragment', replacer(fragment, true)); + replacements.set('path', replacer(path, true)); + replacements.set('base', replacer(base)); + replacements.set('name', replacer(name)); + replacements.set('ext', replacer(ext, true)); + // Legacy + replacements.set( + 'filebase', + deprecated( + replacer(base), + '[filebase] is now [base]', + 'DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_FILENAME', + ), + ); + } + + // Compilation context + // + // Placeholders + // + // [fullhash] - data.hash (3a4b5c6e7f) + // + // Legacy Placeholders + // + // [hash] - data.hash (3a4b5c6e7f) + if (data.hash) { + const hashReplacer = hashLength( + replacer(data.hash), + data.hashWithLength, + assetInfo, + 'fullhash', + ); + + replacements.set('fullhash', hashReplacer); + + // Legacy + replacements.set( + 'hash', + deprecated( + hashReplacer, + '[hash] is now [fullhash] (also consider using [chunkhash] or [contenthash], see documentation for details)', + 'DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_HASH', + ), + ); + } + + // Chunk Context + // + // Placeholders + // + // [id] - chunk.id (0.js) + // [name] - chunk.name (app.js) + // [chunkhash] - chunk.hash (7823t4t4.js) + // [contenthash] - chunk.contentHash[type] (3256u3zg.js) + if (data.chunk) { + const { chunk } = data; + + const { contentHashType } = data; + + const idReplacer = replacer(chunk.id); + const nameReplacer = replacer(chunk.name || chunk.id); + const chunkhashReplacer = hashLength( + replacer(chunk instanceof Chunk ? chunk.renderedHash : chunk.hash), + // eslint-disable-next-line no-undefined + 'hashWithLength' in chunk ? chunk.hashWithLength : undefined, + assetInfo, + 'chunkhash', + ); + const contenthashReplacer = hashLength( + replacer( + data.contentHash || + (contentHashType && + chunk.contentHash && + chunk.contentHash[contentHashType]), + ), + data.contentHashWithLength || + ('contentHashWithLength' in chunk && chunk.contentHashWithLength + ? chunk.contentHashWithLength[/** @type {string} */ (contentHashType)] + : // eslint-disable-next-line no-undefined + undefined), + assetInfo, + 'contenthash', + ); + + replacements.set('id', idReplacer); + replacements.set('name', nameReplacer); + replacements.set('chunkhash', chunkhashReplacer); + replacements.set('contenthash', contenthashReplacer); + } + + // Module Context + // + // Placeholders + // + // [id] - module.id (2.png) + // [hash] - module.hash (6237543873.png) + // + // Legacy Placeholders + // + // [moduleid] - module.id (2.png) + // [modulehash] - module.hash (6237543873.png) + if (data.module) { + const { module } = data; + + const idReplacer = replacer(() => + prepareId( + module instanceof Module + ? /** @type {ModuleId} */ + (/** @type {ChunkGraph} */ (chunkGraph).getModuleId(module)) + : module.id, + ), + ); + const moduleHashReplacer = hashLength( + replacer(() => + module instanceof Module + ? /** @type {ChunkGraph} */ + (chunkGraph).getRenderedModuleHash(module, data.runtime) + : module.hash, + ), + // eslint-disable-next-line no-undefined + 'hashWithLength' in module ? module.hashWithLength : undefined, + assetInfo, + 'modulehash', + ); + const contentHashReplacer = hashLength( + replacer(/** @type {string} */ (data.contentHash)), + // eslint-disable-next-line no-undefined + undefined, + assetInfo, + 'contenthash', + ); + + replacements.set('id', idReplacer); + replacements.set('modulehash', moduleHashReplacer); + replacements.set('contenthash', contentHashReplacer); + replacements.set( + 'hash', + data.contentHash ? contentHashReplacer : moduleHashReplacer, + ); + // Legacy + replacements.set( + 'moduleid', + deprecated( + idReplacer, + '[moduleid] is now [id]', + 'DEP_WEBPACK_TEMPLATE_PATH_PLUGIN_REPLACE_PATH_VARIABLES_MODULE_ID', + ), + ); + } + + // Other things + if (data.url) { + replacements.set('url', replacer(data.url)); + } + if (typeof data.runtime === 'string') { + replacements.set( + 'runtime', + replacer(() => prepareId(/** @type {string} */ (data.runtime))), + ); + } else { + replacements.set('runtime', replacer('_')); + } + + if (typeof path === 'function') { + // eslint-disable-next-line no-param-reassign + path = path(data, assetInfo); + } + + // eslint-disable-next-line no-param-reassign + path = path.replace(REGEXP, (match, content) => { + if (content.length + 2 === match.length) { + const contentMatch = /^(\w+)(?::(\w+))?$/.exec(content); + if (!contentMatch) return match; + const [, kind, arg] = contentMatch; + // eslint-disable-next-line no-shadow + const replacer = replacements.get(kind); + // eslint-disable-next-line no-undefined + if (replacer !== undefined) { + return replacer(match, arg, path); + } + } else if (match.startsWith('[\\') && match.endsWith('\\]')) { + return `[${match.slice(2, -2)}]`; + } + return match; + }); + + return path; +}; + +module.exports = replacePathVariables; diff --git a/src/worker.js b/src/worker.js index b5d24e3..f68d667 100644 --- a/src/worker.js +++ b/src/worker.js @@ -133,6 +133,21 @@ const queue = asyncQueue(({ id, data }, taskCallback) => { const buildDependencies = []; + // eslint-disable-next-line no-underscore-dangle + data._compilation.getPath = function getPath(filename, extraData = {}) { + if (!extraData.hash) { + extraData = { + // eslint-disable-next-line no-underscore-dangle + hash: data._compilation.hash, + ...extraData + }; + } + + const template = require("./template"); + + return template(filename, extraData); + } + loaderRunner.runLoaders( { loaders: data.loaders, diff --git a/test/css-loader-example/index.js b/test/css-loader-example/index.js new file mode 100644 index 0000000..d8a5e44 --- /dev/null +++ b/test/css-loader-example/index.js @@ -0,0 +1,4 @@ +/* eslint-disable import/no-unresolved */ +// some file +import './style.css'; +import './style.modules.css'; diff --git a/test/css-loader-example/style.css b/test/css-loader-example/style.css new file mode 100644 index 0000000..67ce83e --- /dev/null +++ b/test/css-loader-example/style.css @@ -0,0 +1,3 @@ +body { + background: red; +} diff --git a/test/css-loader-example/style.modules.css b/test/css-loader-example/style.modules.css new file mode 100644 index 0000000..19fce73 --- /dev/null +++ b/test/css-loader-example/style.modules.css @@ -0,0 +1,3 @@ +.class { + color: red; +} diff --git a/test/css-loader-example/webpack.config.js b/test/css-loader-example/webpack.config.js new file mode 100644 index 0000000..13e941a --- /dev/null +++ b/test/css-loader-example/webpack.config.js @@ -0,0 +1,43 @@ +const path = require('path'); + +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +const threadLoader = require('../../dist'); // eslint-disable-line import/no-extraneous-dependencies + +module.exports = (env) => { + const workerPool = { + workers: +env.threads, + workerParallelJobs: 1, + poolTimeout: env.watch ? Infinity : 2000, + }; + if (+env.threads > 0) { + threadLoader.warmup(workerPool, ['css-loader']); + } + return { + mode: 'none', + context: __dirname, + devtool: false, + entry: ['./index.js'], + output: { + path: path.resolve('dist'), + filename: 'bundle.js', + }, + module: { + rules: [ + { + test: /\.css$/, + use: [ + env.threads !== 0 && { + loader: path.resolve(__dirname, '../../dist/index.js'), + options: workerPool, + }, + 'css-loader', + ].filter(Boolean), + }, + ], + }, + stats: { + children: false, + }, + }; +}; diff --git a/test/webpack.test.js b/test/webpack.test.js index fa0155d..fc0c52b 100644 --- a/test/webpack.test.js +++ b/test/webpack.test.js @@ -4,13 +4,15 @@ import basicLoaderConfig from './basic-loader-test/webpack.config'; import sassLoaderConfig from './sass-loader-example/webpack.config'; import tsLoaderConfig from './ts-loader-example/webpack.config'; import lessLoaderConfig from './less-loader-example/webpack.config'; +import cssLoaderConfig from './css-loader-example/webpack.config'; test("Processes sass-loader's @import correctly", (done) => { const config = sassLoaderConfig({ threads: 1 }); webpack(config, (err, stats) => { if (err) { - throw err; + done(err); + return; } expect(err).toBe(null); @@ -24,7 +26,8 @@ test('Processes ts-loader correctly', (done) => { webpack(config, (err, stats) => { if (err) { - throw err; + done(err); + return; } expect(err).toBe(null); @@ -38,7 +41,8 @@ test('Works with less-loader', (done) => { webpack(config, (err, stats) => { if (err) { - throw err; + done(err); + return; } expect(err).toBe(null); @@ -47,13 +51,28 @@ test('Works with less-loader', (done) => { }); }, 30000); -test.only('Works with test-loader', (done) => { +test('Works with css-loader', (done) => { + const config = cssLoaderConfig({ }); + + webpack(config, (err, stats) => { + if (err) { + done(err); + return; + } + + expect(err).toBe(null); + expect(stats.hasErrors()).toBe(false); + done(); + }); +}, 30000); + +test('Works with test-loader', (done) => { const config = basicLoaderConfig({ threads: 1 }); webpack(config, (err, stats) => { if (err) { - // eslint-disable-next-line no-console - console.error(err); + done(err); + return; } expect(stats.compilation.errors).toMatchSnapshot('errors'); From 6779d8777c3eb934828a72dea35d6d933bf81991 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 19 Sep 2024 00:18:39 +0300 Subject: [PATCH 3/6] refactor: code --- src/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/index.js b/src/index.js index 9595a58..7bc5fee 100644 --- a/src/index.js +++ b/src/index.js @@ -24,7 +24,6 @@ function pitch() { options: { plugins: [] }, }, _compilation: { - hash: "tsst", outputOptions: { hashSalt: this._compilation.outputOptions.hashSalt, hashFunction: this._compilation.outputOptions.hashFunction, From ab8a71b79e0d1df42d9bcce7aaccd7b73a31af40 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 19 Sep 2024 00:20:45 +0300 Subject: [PATCH 4/6] test: fix --- src/worker.js | 10 ++++++---- test/css-loader-example/webpack.config.js | 2 -- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/worker.js b/src/worker.js index f68d667..903a2f5 100644 --- a/src/worker.js +++ b/src/worker.js @@ -133,20 +133,22 @@ const queue = asyncQueue(({ id, data }, taskCallback) => { const buildDependencies = []; - // eslint-disable-next-line no-underscore-dangle + // eslint-disable-next-line no-underscore-dangle, no-param-reassign data._compilation.getPath = function getPath(filename, extraData = {}) { if (!extraData.hash) { + // eslint-disable-next-line no-param-reassign extraData = { // eslint-disable-next-line no-underscore-dangle hash: data._compilation.hash, - ...extraData + ...extraData, }; } - const template = require("./template"); + // eslint-disable-next-line global-require + const template = require('./template'); return template(filename, extraData); - } + }; loaderRunner.runLoaders( { diff --git a/test/css-loader-example/webpack.config.js b/test/css-loader-example/webpack.config.js index 13e941a..aa67973 100644 --- a/test/css-loader-example/webpack.config.js +++ b/test/css-loader-example/webpack.config.js @@ -1,7 +1,5 @@ const path = require('path'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); - const threadLoader = require('../../dist'); // eslint-disable-line import/no-extraneous-dependencies module.exports = (env) => { From 9848e89b7050bdc5c1fe3e639bd2fee785f7924c Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 19 Sep 2024 00:24:59 +0300 Subject: [PATCH 5/6] test: fix --- test/webpack.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/webpack.test.js b/test/webpack.test.js index fc0c52b..ab13bad 100644 --- a/test/webpack.test.js +++ b/test/webpack.test.js @@ -52,7 +52,7 @@ test('Works with less-loader', (done) => { }, 30000); test('Works with css-loader', (done) => { - const config = cssLoaderConfig({ }); + const config = cssLoaderConfig({}); webpack(config, (err, stats) => { if (err) { From 2a8d4ce85d3115f979b9ab164b75f0b8872a37b6 Mon Sep 17 00:00:00 2001 From: "alexander.akait" Date: Thu, 19 Sep 2024 00:29:52 +0300 Subject: [PATCH 6/6] test: fix --- .cspell.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index 09aa9f1..c3dcce6 100644 --- a/.cspell.json +++ b/.cspell.json @@ -19,7 +19,11 @@ "Koppers", "sokra", "lifecycles", - "absolutify" + "absolutify", + "filebase", + "chunkhash", + "moduleid", + "modulehash" ], "ignorePaths": [ "CHANGELOG.md",