Skip to content

Commit

Permalink
fix: support utils.createHash (#222)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait authored Sep 18, 2024
1 parent c490b38 commit 6bfc93c
Show file tree
Hide file tree
Showing 10 changed files with 475 additions and 11 deletions.
6 changes: 5 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@
"Koppers",
"sokra",
"lifecycles",
"absolutify"
"absolutify",
"filebase",
"chunkhash",
"moduleid",
"modulehash"
],
"ignorePaths": [
"CHANGELOG.md",
Expand Down
355 changes: 355 additions & 0 deletions src/template.js
Original file line number Diff line number Diff line change
@@ -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<string, Function>} */
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;
Loading

0 comments on commit 6bfc93c

Please sign in to comment.