diff --git a/.cspell.json b/.cspell.json index 67e53a6e..3797e9d7 100644 --- a/.cspell.json +++ b/.cspell.json @@ -36,9 +36,10 @@ "vspace", "jsbeautify", "Gitter", - "commitlint" + "commitlint", + "postprocessor", + "eslintcache" ], - "ignorePaths": [ "CHANGELOG.md", "package.json", diff --git a/.gitignore b/.gitignore index 76df9327..32ba562b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ logs *.log npm-debug.log* .eslintcache +.cspellcache /coverage /dist /local diff --git a/README.md b/README.md index 5a0279eb..d1130dee 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ module.exports = { - **[`sources`](#sources)** - **[`preprocessor`](#preprocessor)** +- **[`postprocessor`](#postprocessor)** - **[`minimize`](#minimize)** - **[`esModule`](#esmodule)** @@ -490,10 +491,7 @@ module.exports = { Type: ```ts -type preprocessor = ( - content: string | Buffer, - loaderContext: LoaderContext, -) => HTMLElement; +type preprocessor = (content: string, loaderContext: LoaderContext) => string; ``` Default: `undefined` @@ -591,6 +589,85 @@ module.exports = { }; ``` +### `postprocessor` + +Type: + +```ts +type postprocessor = (content: string, loaderContext: LoaderContext) => string; +``` + +Default: `undefined` + +Allows post-processing of content after replacing all attributes (like `src`/`srcset`/etc). + +**file.html** + +```html + + + + +
<%= require('./gallery.html').default %>
+``` + +#### `function` + +You can set the `postprocessor` option as a `function` instance. + +**webpack.config.js** + +```js +const Handlebars = require("handlebars"); + +module.exports = { + module: { + rules: [ + { + test: /\.html$/i, + loader: "html-loader", + options: { + postprocessor: (content, loaderContext) => { + return content.replace(/<%=/g, '" +').replace(/%>/g, '+ "'); + }, + }, + }, + ], + }, +}; +``` + +You can also set the `postprocessor` option as an asynchronous function instance. + +For example: + +**webpack.config.js** + +```js +const Handlebars = require("handlebars"); + +module.exports = { + module: { + rules: [ + { + test: /\.hbs$/i, + loader: "html-loader", + options: { + postprocessor: async (content, loaderContext) => { + const value = await getValue(); + + return content + .replace(/<%=/g, '" +') + .replace(/%>/g, '+ "') + .replace("my-value", value); + }, + }, + }, + ], + }, +}; +``` + ### `minimize` Type: diff --git a/src/index.js b/src/index.js index cea9e359..36920f01 100644 --- a/src/index.js +++ b/src/index.js @@ -41,7 +41,18 @@ export default async function loader(content) { plugins.push(minimizerPlugin({ minimize: options.minimize, errors })); } - const { html } = await pluginRunner(plugins).process(content); + let { html } = await pluginRunner(plugins).process(content); + + html = JSON.stringify(html) + // Invalid in JavaScript but valid HTML + .replace(/[\u2028\u2029]/g, (str) => + str === "\u2029" ? "\\u2029" : "\\u2028", + ); + + if (options.postprocessor) { + // eslint-disable-next-line no-param-reassign + html = await options.postprocessor(html, this); + } for (const error of errors) { this.emitError(error instanceof Error ? error : new Error(error)); diff --git a/src/options.json b/src/options.json index 2f802435..1af321ca 100644 --- a/src/options.json +++ b/src/options.json @@ -45,6 +45,11 @@ "description": "Allows pre-processing of content before handling.", "link": "https://github.com/webpack-contrib/html-loader#preprocessor" }, + "postprocessor": { + "instanceof": "Function", + "description": "Allows post-processing of content before handling.", + "link": "https://github.com/webpack-contrib/html-loader#postprocessor" + }, "sources": { "anyOf": [ { "type": "boolean" }, diff --git a/src/utils.js b/src/utils.js index a4f1e576..41a2a361 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1174,6 +1174,7 @@ function getSourcesOption(rawOptions) { export function normalizeOptions(rawOptions, loaderContext) { return { preprocessor: rawOptions.preprocessor, + postprocessor: rawOptions.postprocessor, sources: getSourcesOption(rawOptions), minimize: getMinimizeOption(rawOptions, loaderContext), esModule: @@ -1251,12 +1252,7 @@ export function getImportCode(html, loaderContext, imports, options) { } export function getModuleCode(html, replacements) { - let code = JSON.stringify(html) - // Invalid in JavaScript but valid HTML - .replace(/[\u2028\u2029]/g, (str) => - str === "\u2029" ? "\\u2029" : "\\u2028", - ); - + let code = html; let replacersCode = ""; for (const item of replacements) { diff --git a/test/__snapshots__/postprocessor-option.test.js.snap b/test/__snapshots__/postprocessor-option.test.js.snap new file mode 100644 index 00000000..0a157052 --- /dev/null +++ b/test/__snapshots__/postprocessor-option.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`'postprocess' option should work with async "postprocessor" function option: errors 1`] = `[]`; + +exports[`'postprocess' option should work with async "postprocessor" function option: 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 = "
\\n

{{firstname}} {{lastname}}

\\n \\"alt\\"\\n
\\n"; +// Exports +export default code;" +`; + +exports[`'postprocess' option should work with async "postprocessor" function option: result 1`] = ` +"
+

{{firstname}} {{lastname}}

+ alt +
+" +`; + +exports[`'postprocess' option should work with async "postprocessor" function option: warnings 1`] = `[]`; + +exports[`'postprocess' option should work with the "postprocessor" option: errors 1`] = `[]`; + +exports[`'postprocess' option should work with the "postprocessor" option: 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 = "\\n\\n\\n\\n
" + require('./gallery.html').default + "
\\n\\n"; +// Exports +export default code;" +`; + +exports[`'postprocess' option should work with the "postprocessor" option: result 1`] = ` +" + + + +

Gallery

+ +" +`; + +exports[`'postprocess' option should work with the "postprocessor" option: warnings 1`] = `[]`; diff --git a/test/__snapshots__/preprocessor-option.test.js.snap b/test/__snapshots__/preprocessor-option.test.js.snap index 021b4657..3fd78155 100644 --- a/test/__snapshots__/preprocessor-option.test.js.snap +++ b/test/__snapshots__/preprocessor-option.test.js.snap @@ -1,8 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`'process' option should work with Async "preprocessor" Function option: errors 1`] = `[]`; +exports[`'preprocess' option should work with async "preprocessor" function option: errors 1`] = `[]`; -exports[`'process' option should work with Async "preprocessor" Function option: module 1`] = ` +exports[`'preprocess' option should work with async "preprocessor" function option: 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); @@ -13,7 +13,7 @@ var code = "
\\n

Alexander Krasnoyarov

\\n

Alexander Krasnoyarov

alt @@ -21,11 +21,11 @@ exports[`'process' option should work with Async "preprocessor" Function option: " `; -exports[`'process' option should work with Async "preprocessor" Function option: warnings 1`] = `[]`; +exports[`'preprocess' option should work with async "preprocessor" function option: warnings 1`] = `[]`; -exports[`'process' option should work with the "preprocessor" option #2: errors 1`] = `[]`; +exports[`'preprocess' option should work with the "preprocessor" option #2: errors 1`] = `[]`; -exports[`'process' option should work with the "preprocessor" option #2: module 1`] = ` +exports[`'preprocess' option should work with the "preprocessor" option #2: module 1`] = ` "// Imports import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.js"; var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png.webp", import.meta.url); @@ -38,16 +38,16 @@ var code = " " `; -exports[`'process' option should work with the "preprocessor" option #2: warnings 1`] = `[]`; +exports[`'preprocess' option should work with the "preprocessor" option #2: warnings 1`] = `[]`; -exports[`'process' option should work with the "preprocessor" option: errors 1`] = `[]`; +exports[`'preprocess' option should work with the "preprocessor" option: errors 1`] = `[]`; -exports[`'process' option should work with the "preprocessor" option: module 1`] = ` +exports[`'preprocess' option should work with the "preprocessor" option: 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); @@ -58,7 +58,7 @@ var code = "
\\n

Alexander Krasnoyarov

\\n

Alexander Krasnoyarov

alt @@ -66,11 +66,11 @@ exports[`'process' option should work with the "preprocessor" option: result 1`] " `; -exports[`'process' option should work with the "preprocessor" option: warnings 1`] = `[]`; +exports[`'preprocess' option should work with the "preprocessor" option: warnings 1`] = `[]`; -exports[`'process' option should work with the Async "preprocessor" Function option #2: errors 1`] = `[]`; +exports[`'preprocess' option should work with the async "preprocessor" function option #2: errors 1`] = `[]`; -exports[`'process' option should work with the Async "preprocessor" Function option #2: module 1`] = ` +exports[`'preprocess' option should work with the async "preprocessor" function option #2: module 1`] = ` "// Imports import ___HTML_LOADER_GET_SOURCE_FROM_IMPORT___ from "../../src/runtime/getUrl.js"; var ___HTML_LOADER_IMPORT_0___ = new URL("./image.png.webp", import.meta.url); @@ -83,9 +83,9 @@ var code = " " `; -exports[`'process' option should work with the Async "preprocessor" Function option #2: warnings 1`] = `[]`; +exports[`'preprocess' option should work with the async "preprocessor" function option #2: warnings 1`] = `[]`; diff --git a/test/__snapshots__/validate-options.test.js.snap b/test/__snapshots__/validate-options.test.js.snap index 3cae344e..9db8c393 100644 --- a/test/__snapshots__/validate-options.test.js.snap +++ b/test/__snapshots__/validate-options.test.js.snap @@ -95,47 +95,47 @@ exports[`validate options should throw an error on the "sources" option with "tr exports[`validate options should throw an error on the "unknown" option with "/test/" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "[]" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "{"foo":"bar"}" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "{}" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "1" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "false" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "test" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; exports[`validate options should throw an error on the "unknown" option with "true" value 1`] = ` "Invalid options object. HTML Loader has been initialized using an options object that does not match the API schema. - options has an unknown property 'unknown'. These properties are valid: - object { preprocessor?, sources?, minimize?, esModule? }" + object { preprocessor?, postprocessor?, sources?, minimize?, esModule? }" `; diff --git a/test/fixtures/gallery.html b/test/fixtures/gallery.html new file mode 100644 index 00000000..279b5da4 --- /dev/null +++ b/test/fixtures/gallery.html @@ -0,0 +1 @@ +

Gallery

\ No newline at end of file diff --git a/test/fixtures/postprocessor.html b/test/fixtures/postprocessor.html new file mode 100644 index 00000000..513d2721 --- /dev/null +++ b/test/fixtures/postprocessor.html @@ -0,0 +1,6 @@ + + + + +
<%= require('./gallery.html').default %>
+ diff --git a/test/postprocessor-option.test.js b/test/postprocessor-option.test.js new file mode 100644 index 00000000..e73f4ea8 --- /dev/null +++ b/test/postprocessor-option.test.js @@ -0,0 +1,53 @@ +import { + compile, + execute, + getCompiler, + getErrors, + getModuleSource, + getWarnings, + readAsset, +} from "./helpers"; + +describe("'postprocess' option", () => { + it('should work with the "postprocessor" option', async () => { + const compiler = getCompiler("postprocessor.html", { + postprocessor: (content, loaderContext) => { + expect(typeof content).toBe("string"); + expect(loaderContext).toBeDefined(); + + return content.replace(/<%=/g, '" +').replace(/%>/g, '+ "'); + }, + }); + 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 stats = await compile(compiler); + + expect(getModuleSource("./preprocessor.hbs", stats)).toMatchSnapshot( + "module", + ); + expect( + execute(readAsset("main.bundle.js", compiler, stats)), + ).toMatchSnapshot("result"); + expect(getWarnings(stats)).toMatchSnapshot("warnings"); + expect(getErrors(stats)).toMatchSnapshot("errors"); + }); +}); diff --git a/test/preprocessor-option.test.js b/test/preprocessor-option.test.js index 30a09062..8754846b 100644 --- a/test/preprocessor-option.test.js +++ b/test/preprocessor-option.test.js @@ -12,7 +12,7 @@ import { readAsset, } from "./helpers"; -describe("'process' option", () => { +describe("'preprocess' option", () => { it('should work with the "preprocessor" option', async () => { const compiler = getCompiler("preprocessor.hbs", { preprocessor: (content, loaderContext) => { @@ -47,7 +47,7 @@ describe("'process' option", () => { expect(getErrors(stats)).toMatchSnapshot("errors"); }); - it('should work with Async "preprocessor" Function option', async () => { + it('should work with async "preprocessor" function option', async () => { const compiler = getCompiler("preprocessor.hbs", { preprocessor: async (content, loaderContext) => { await expect(typeof content).toBe("string"); @@ -110,7 +110,8 @@ describe("'process' option", () => { expect(getWarnings(stats)).toMatchSnapshot("warnings"); expect(getErrors(stats)).toMatchSnapshot("errors"); }); - it('should work with the Async "preprocessor" Function option #2', async () => { + + it('should work with the async "preprocessor" function option #2', async () => { const plugin = posthtmlWebp(); const compiler = getCompiler("posthtml.html", { preprocessor: async (content, loaderContext) => {