Skip to content

Commit

Permalink
feat: using template literals in code when it supported (#520)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexander-akait authored Jul 25, 2024
1 parent cc34b06 commit 6fa80d5
Show file tree
Hide file tree
Showing 10 changed files with 16,118 additions and 4,663 deletions.
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -628,7 +628,12 @@ module.exports = {
loader: "html-loader",
options: {
postprocessor: (content, loaderContext) => {
return content.replace(/<%=/g, '" +').replace(/%>/g, '+ "');
// When you environment supports template literals (using browserslist or options) we will generate code using them
const isTemplateLiteralSupported = content[0] === "`";

return content
.replace(/<%=/g, isTemplateLiteralSupported ? `\${` : '" +')
.replace(/%>/g, isTemplateLiteralSupported ? "}" : '+ "');
},
},
},
Expand All @@ -655,10 +660,12 @@ module.exports = {
options: {
postprocessor: async (content, loaderContext) => {
const value = await getValue();
// When you environment supports template literals (using browserslist or options) we will generate code using them
const isTemplateLiteralSupported = content[0] === "`";

return content
.replace(/<%=/g, '" +')
.replace(/%>/g, '+ "')
.replace(/<%=/g, isTemplateLiteralSupported ? `\${` : '" +')
.replace(/%>/g, isTemplateLiteralSupported ? "}" : '+ "')
.replace("my-value", value);
},
},
Expand Down
22 changes: 16 additions & 6 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
getModuleCode,
getExportCode,
defaultMinimizerOptions,
supportTemplateLiteral,
convertToTemplateLiteral,
} from "./utils";

import schema from "./options.json";
Expand Down Expand Up @@ -57,7 +59,17 @@ export default async function loader(content) {

let { html } = await pluginRunner(plugins).process(content);

html = JSON.stringify(html)
for (const error of errors) {
this.emitError(error instanceof Error ? error : new Error(error));
}

const isTemplateLiteralSupported = supportTemplateLiteral(this);

html = (
isTemplateLiteralSupported
? convertToTemplateLiteral(html)
: JSON.stringify(html)
)
// Invalid in JavaScript but valid HTML
.replace(/[\u2028\u2029]/g, (str) =>
str === "\u2029" ? "\\u2029" : "\\u2028",
Expand All @@ -68,12 +80,10 @@ export default async function loader(content) {
html = await options.postprocessor(html, this);
}

for (const error of errors) {
this.emitError(error instanceof Error ? error : new Error(error));
}

const importCode = getImportCode(html, this, imports, options);
const moduleCode = getModuleCode(html, replacements, options);
const moduleCode = getModuleCode(html, replacements, {
isTemplateLiteralSupported,
});
const exportCode = getExportCode(html, options);

return `${importCode}${moduleCode}${exportCode}`;
Expand Down
65 changes: 57 additions & 8 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1240,10 +1240,31 @@ export function getImportCode(html, loaderContext, imports, options) {
return `// Imports\n${code}`;
}

export function getModuleCode(html, replacements) {
const SLASH = "\\".charCodeAt(0);
const BACKTICK = "`".charCodeAt(0);
const DOLLAR = "$".charCodeAt(0);

export function convertToTemplateLiteral(str) {
let escapedString = "";

for (let i = 0; i < str.length; i++) {
const code = str.charCodeAt(i);

escapedString +=
code === SLASH || code === BACKTICK || code === DOLLAR
? `\\${str[i]}`
: str[i];
}

return `\`${escapedString}\``;
}

export function getModuleCode(html, replacements, options) {
let code = html;
let replacersCode = "";

const { isTemplateLiteralSupported } = options;

for (const item of replacements) {
const { runtime, importName, replacementName, isValueQuoted, hash } = item;

Expand All @@ -1256,20 +1277,24 @@ export function getModuleCode(html, replacements) {

replacersCode += `var ${replacementName} = ${GET_SOURCE_FROM_IMPORT_NAME}(${importName}${preparedOptions});\n`;

code = code.replace(
new RegExp(replacementName, "g"),
() => `" + ${replacementName} + "`,
code = code.replace(new RegExp(replacementName, "g"), () =>
isTemplateLiteralSupported
? `\${${replacementName}}`
: `" + ${replacementName} + "`,
);
} else {
code = code.replace(
new RegExp(replacementName, "g"),
() => `" + ${importName} + "`,
code = code.replace(new RegExp(replacementName, "g"), () =>
isTemplateLiteralSupported
? `\${${replacementName}}`
: `" + ${replacementName} + "`,
);
}
}

// Replaces "<script>" or "</script>" to "<" + "script>" or "<" + "/script>".
code = code.replace(/<(\/?script)/g, (_, s) => `<" + "${s}`);
code = code.replace(/<(\/?script)/g, (_, s) =>
isTemplateLiteralSupported ? `\${"<" + "${s}"}` : `<" + "${s}`,
);

return `// Module\n${replacersCode}var code = ${code};\n`;
}
Expand Down Expand Up @@ -1342,4 +1367,28 @@ export function traverse(root, callback) {
visit(root, null);
}

export function supportTemplateLiteral(loaderContext) {
if (loaderContext.environment && loaderContext.environment.templateLiteral) {
return true;
}

// TODO remove in the next major release
if (
// eslint-disable-next-line no-underscore-dangle
loaderContext._compilation &&
// eslint-disable-next-line no-underscore-dangle
loaderContext._compilation.options &&
// eslint-disable-next-line no-underscore-dangle
loaderContext._compilation.options.output &&
// eslint-disable-next-line no-underscore-dangle
loaderContext._compilation.options.output.environment &&
// eslint-disable-next-line no-underscore-dangle
loaderContext._compilation.options.output.environment.templateLiteral
) {
return true;
}

return false;
}

export const webpackIgnoreCommentRegexp = /webpackIgnore:(\s+)?(true|false)/;
2,370 changes: 1,860 additions & 510 deletions test/__snapshots__/esModule-option.test.js.snap

Large diffs are not rendered by default.

1,830 changes: 1,432 additions & 398 deletions test/__snapshots__/loader.test.js.snap

Large diffs are not rendered by default.

2,121 changes: 1,742 additions & 379 deletions test/__snapshots__/minimize-option.test.js.snap

Large diffs are not rendered by default.

39 changes: 37 additions & 2 deletions test/__snapshots__/postprocessor-option.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.j
var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var code = "<div>\\n <p>{{firstname}} {{lastname}}</p>\\n <img src=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\" alt=\\"alt\\" />\\n<div>\\n";
var code = \`<div>
<p>{{firstname}} {{lastname}}</p>
<img src="\${___HTML_LOADER_REPLACEMENT_0___}" alt="alt" />
<div>
\`;
// Exports
export default code;"
`;
Expand All @@ -23,6 +27,31 @@ exports[`'postprocess' option should work with async "postprocessor" function op
exports[`'postprocess' option should work with async "postprocessor" function option: warnings 1`] = `[]`;
exports[`'postprocess' option should work with the "postprocessor" option #1: errors 1`] = `[]`;
exports[`'postprocess' option should work with the "postprocessor" option #1: module 1`] = `
"// Imports
import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.js";
var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var code = "<img src=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\">\\n<img src=\\"" + 'Hello ' + (1+1) + "\\">\\n<img src=\\"" + require('./image.png') + "\\">\\n<img src=\\"" + new URL('./image.png', import.meta.url) + "\\">\\n<div>" + require('./gallery.html').default + "</div>\\n<!--Works fine, but need improve testing <div>< %= (await import('./gallery.html')).default % ></div>-->\\n";
// Exports
export default code;"
`;
exports[`'postprocess' option should work with the "postprocessor" option #1: result 1`] = `
"<img src="replaced_file_protocol_/webpack/public/path/image.png">
<img src="Hello 2">
<img src="/webpack/public/path/image.png">
<img src="replaced_file_protocol_/webpack/public/path/image.png">
<div><h2>Gallery</h2></div>
<!--Works fine, but need improve testing <div>< %= (await import('./gallery.html')).default % ></div>-->
"
`;
exports[`'postprocess' option should work with the "postprocessor" option #1: warnings 1`] = `[]`;
exports[`'postprocess' option should work with the "postprocessor" option: errors 1`] = `[]`;
exports[`'postprocess' option should work with the "postprocessor" option: module 1`] = `
Expand All @@ -31,7 +60,13 @@ import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.j
var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var code = "<img src=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\">\\n<img src=\\"" + 'Hello ' + (1+1) + "\\">\\n<img src=\\"" + require('./image.png') + "\\">\\n<img src=\\"" + new URL('./image.png', import.meta.url) + "\\">\\n<div>" + require('./gallery.html').default + "</div>\\n<!--Works fine, but need improve testing <div>< %= (await import('./gallery.html')).default % ></div>-->\\n";
var code = \`<img src="\${___HTML_LOADER_REPLACEMENT_0___}">
<img src="\${ 'Hello ' + (1+1) }">
<img src="\${ require('./image.png') }">
<img src="\${ new URL('./image.png', import.meta.url) }">
<div>\${ require('./gallery.html').default }</div>
<!--Works fine, but need improve testing <div>< %= (await import('./gallery.html')).default % ></div>-->
\`;
// Exports
export default code;"
`;
Expand Down
18 changes: 14 additions & 4 deletions test/__snapshots__/preprocessor-option.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.j
var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var code = "<div>\\n <p>Alexander Krasnoyarov</p>\\n <img src=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\" alt=\\"alt\\" />\\n<div>\\n";
var code = \`<div>
<p>Alexander Krasnoyarov</p>
<img src="\${___HTML_LOADER_REPLACEMENT_0___}" alt="alt" />
<div>
\`;
// Exports
export default code;"
`;
Expand All @@ -33,7 +37,8 @@ var ___HTML_LOADER_IMPORT_1___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var ___HTML_LOADER_REPLACEMENT_1___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_1___);
var code = "<picture><source type=\\"image/webp\\" srcset=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\"><img src=\\"" + ___HTML_LOADER_REPLACEMENT_1___ + "\\"></picture>\\n";
var code = \`<picture><source type="image/webp" srcset="\${___HTML_LOADER_REPLACEMENT_0___}"><img src="\${___HTML_LOADER_REPLACEMENT_1___}"></picture>
\`;
// Exports
export default code;"
`;
Expand All @@ -53,7 +58,11 @@ import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.j
var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var code = "<div>\\n <p>Alexander Krasnoyarov</p>\\n <img src=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\" alt=\\"alt\\" />\\n<div>\\n";
var code = \`<div>
<p>Alexander Krasnoyarov</p>
<img src="\${___HTML_LOADER_REPLACEMENT_0___}" alt="alt" />
<div>
\`;
// Exports
export default code;"
`;
Expand All @@ -78,7 +87,8 @@ var ___HTML_LOADER_IMPORT_1___ = new URL("./image.png", import.meta.url);
// Module
var ___HTML_LOADER_REPLACEMENT_0___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_0___);
var ___HTML_LOADER_REPLACEMENT_1___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_1___);
var code = "<picture><source type=\\"image/webp\\" srcset=\\"" + ___HTML_LOADER_REPLACEMENT_0___ + "\\"><img src=\\"" + ___HTML_LOADER_REPLACEMENT_1___ + "\\"></picture>\\n";
var code = \`<picture><source type="image/webp" srcset="\${___HTML_LOADER_REPLACEMENT_0___}"><img src="\${___HTML_LOADER_REPLACEMENT_1___}"></picture>
\`;
// Exports
export default code;"
`;
Expand Down
14,248 changes: 10,897 additions & 3,351 deletions test/__snapshots__/sources-option.test.js.snap

Large diffs are not rendered by default.

55 changes: 53 additions & 2 deletions test/postprocessor-option.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import path from "path";

import {
compile,
execute,
Expand All @@ -15,7 +17,11 @@ describe("'postprocess' option", () => {
expect(typeof content).toBe("string");
expect(loaderContext).toBeDefined();

return content.replace(/<%=/g, '" +').replace(/%>/g, '+ "');
const isTemplateLiteralSupported = content[0] === "`";

return content
.replace(/<%=/g, isTemplateLiteralSupported ? `\${` : '" +')
.replace(/%>/g, isTemplateLiteralSupported ? "}" : '+ "');
},
});
const stats = await compile(compiler);
Expand All @@ -30,13 +36,58 @@ describe("'postprocess' option", () => {
expect(getErrors(stats)).toMatchSnapshot("errors");
});

it('should work with the "postprocessor" option #1', async () => {
const compiler = getCompiler(
"postprocessor.html",
{
postprocessor: (content, loaderContext) => {
expect(typeof content).toBe("string");
expect(loaderContext).toBeDefined();

const isTemplateLiteralSupported = content[0] === "`";

return content
.replace(/<%=/g, isTemplateLiteralSupported ? `\${` : '" +')
.replace(/%>/g, isTemplateLiteralSupported ? "}" : '+ "');
},
},
{
output: {
path: path.resolve(__dirname, "./outputs"),
filename: "[name].bundle.js",
chunkFilename: "[name].chunk.js",
chunkLoading: "require",
publicPath: "/webpack/public/path/",
library: "___TEST___",
assetModuleFilename: "[name][ext]",
hashFunction: "xxhash64",
environment: { templateLiteral: false },
},
},
);
const stats = await compile(compiler);

expect(getModuleSource("./postprocessor.html", stats)).toMatchSnapshot(
"module",
);
expect(
execute(readAsset("main.bundle.js", compiler, stats)),
).toMatchSnapshot("result");
expect(getWarnings(stats)).toMatchSnapshot("warnings");
expect(getErrors(stats)).toMatchSnapshot("errors");
});

it('should work with async "postprocessor" function option', async () => {
const compiler = getCompiler("preprocessor.hbs", {
postprocessor: async (content, loaderContext) => {
await expect(typeof content).toBe("string");
await expect(loaderContext).toBeDefined();

return content.replace(/<%=/g, '" +').replace(/%>/g, '+ "');
const isTemplateLiteralSupported = content[0] === "`";

return content
.replace(/<%=/g, isTemplateLiteralSupported ? `\${` : '" +')
.replace(/%>/g, isTemplateLiteralSupported ? "}" : '+ "');
},
});
const stats = await compile(compiler);
Expand Down

0 comments on commit 6fa80d5

Please sign in to comment.