diff --git a/README.md b/README.md index ef342341..b8476fa2 100644 --- a/README.md +++ b/README.md @@ -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 ? "}" : '+ "'); }, }, }, @@ -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); }, }, diff --git a/src/index.js b/src/index.js index f98efda2..f67c0c4a 100644 --- a/src/index.js +++ b/src/index.js @@ -6,6 +6,8 @@ import { getModuleCode, getExportCode, defaultMinimizerOptions, + supportTemplateLiteral, + convertToTemplateLiteral, } from "./utils"; import schema from "./options.json"; @@ -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", @@ -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}`; diff --git a/src/utils.js b/src/utils.js index 81dc9c0f..ea642f38 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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; @@ -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 "" 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`; } @@ -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)/; diff --git a/test/__snapshots__/esModule-option.test.js.snap b/test/__snapshots__/esModule-option.test.js.snap index e4a261f3..e9be371c 100644 --- a/test/__snapshots__/esModule-option.test.js.snap +++ b/test/__snapshots__/esModule-option.test.js.snap @@ -69,13 +69,7 @@ var ___HTML_LOADER_REPLACEMENT_32___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___( var ___HTML_LOADER_REPLACEMENT_33___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_24___); var ___HTML_LOADER_REPLACEMENT_34___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_25___); var ___HTML_LOADER_REPLACEMENT_35___ = ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___(___HTML_LOADER_IMPORT_26___); -var code = "\\n\\n

My First Heading

\\n

My first paragraph.

\\n

An Unordered HTML List

\\n\\n\\n\\n

An Ordered HTML List

\\n\\n
    \\n
  1. Coffee
  2. \\n
  3. Tea
  4. \\n
  5. Milk
  6. \\n
\\n\\n<" + "script>console.log({\\"json\\": \\"with \\\\\\"quotes\\\\\\" in value\\"})<" + "/script>\\n\\n\\n\\n
Foo
\\n\\n\\n
BAR
\\n\\n\\n\\n<" + "script> console.log(1 + 2 + \`\${3 + 3}\`) <" + "/script>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n<" + "script src=\\"" + ___HTML_LOADER_REPLACEMENT_4___ + "\\"><" + "/script>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\"Elva\\n\\n\\n \\n \\n \\"Flowers\\"\\n\\n\\n\\n\\n\\n\\n\\n\\n\\"Smiley\\n\\n
\\n First name:
\\n \\n
\\n\\n<" + "script src=\\"https://github.com/webpack-contrib/css-loader/blob/master/src/index.js\\"><" + "/script>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nT ex t \\n\\n
\\n\\n]]>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\nlink text\\n\\nCall me\\n\\n-->\\n-->\\n\\n\\n\\n\\n\\n
\\n
\\n\\n<div id = "character">\\n© 2007\\nor\\n© 2007\\n\\n
\\n\\n\\"Red\\n
\\n Written by Jon Doe.
\\n Visit us at:
\\n Example.com
\\n Box 564, Disneyland
\\n USA\\n
\\nlink\\nStart Chat\\nStart Chat\\nStart Chat\\n\\n\\n\\n\\"Elva\\n\\"Elva\\n\\n\\"Elva\\n\\"Test\\"\\n\\n\\n\\n Test \\n\\ntest\\ntest\\ntest\\n\\n\\n\\n\\n\\n

Text

\\n

Text

\\n

Text

\\n\\n\\n\\n<" + "script type=\\"module\\" src=\\"" + ___HTML_LOADER_REPLACEMENT_14___ + "\\"><" + "/script>\\n<" + "script nomodule src=\\"" + ___HTML_LOADER_REPLACEMENT_15___ + "\\"><" + "/script>\\n<" + "script type=\\"text/javascript\\" src=\\"" + ___HTML_LOADER_REPLACEMENT_15___ + "\\"><" + "/script>\\n<" + "script type=\\"application/javascript\\" src=\\"" + ___HTML_LOADER_REPLACEMENT_15___ + "\\"><" + "/script>\\n<" + "script type=\\"application/json\\" src=\\"fallback.file.json\\"><" + "/script>\\n<" + "script type=\\"text/x-handlebars-template\\" src=\\"preprocessor.hbs\\"><" + "/script>\\n<" + "script type=\\"module\\">\\n function test() {}\\n<" + "/script>\\n<" + "script>\\n function test() {}\\n<" + "/script>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\"Elva\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\"Elva\\n\\"test\\"/\\n\\"test\\"\\n\\"test\\"\\n\\"test\\"/\\n\\"test\\"/\\n\\n\\n\\"Elva\\n\\n\\n\\n\\n \\n\\n\\n\\n \\n\\n\\n\\n \\n\\n\\n\\n \\n\\n\\nfoo bar\\n\\nText\\nText\\n\\n\\n\\n
\\n\\nVisit our HTML tutorial\\nVisit our HTML tutorial\\n\\n<" + "script type=\\"text/javascript\\">\\n console.log('HERE')\\n<" + "/script>\\n\\n\\n\\n
text
\\n
text
\\n\\n\\n\\n\\n\\n\\"\\"\\n\\n\\"\\"\\n\\n\\"multi\\nline\\nalt\\"\\n\\n\\"Red\\n\\n\\" alt=\\"<%= name %>\\" src=\\"<%= imgsrc %>\\" />\\n\\n\\n <" + "script href=\\"" + ___HTML_LOADER_REPLACEMENT_4___ + "\\"><" + "/script>\\n\\n\\n <" + "script type=\\"application/json\\" href=\\"./script.file.js\\"><" + "/script>\\n\\n\\n <" + "script xlink:href=\\"" + ___HTML_LOADER_REPLACEMENT_4___ + "\\"><" + "/script>\\n\\n\\n <" + "script type=\\"application/json\\" xlink:href=\\"./script.file.js\\"><" + "/script>\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n
\\n \\n
\\n\\n\\n\\n\\n"; -// Exports -export default code;" -`; - -exports[`'esModule' option should use a CommonJS export by default: result 1`] = ` -" +var code = \`

My First Heading

My first paragraph.

@@ -95,7 +89,7 @@ exports[`'esModule' option should use a CommonJS export by default: result 1`] =
  • Milk
  • - +\${"<" + "script"}>console.log({"json": "with \\\\"quotes\\\\" in value"})\${"<" + "/script"}> @@ -106,136 +100,136 @@ exports[`'esModule' option should use a CommonJS export by default: result 1`] = - - - - - - - - - - - - +\${"<" + "script"}> console.log(1 + 2 + \\\`\\\${3 + 3}\\\`) \${"<" + "/script"}> + + + + + + + + + + + - +\${"<" + "script"} src="\${___HTML_LOADER_REPLACEMENT_4___}">\${"<" + "/script"}> - + -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy -Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy +Elva dressed as a fairy Elva dressed as a fairy +" src="\${___HTML_LOADER_REPLACEMENT_0___}" alt="Elva dressed as a fairy"> - - - Flowers + + + Flowers -