Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
bccba82
feat: add html-plugins-to-native-html codemod
bjohansebas Aug 2, 2026
ba91d13
feat: migrate html-loader rules in html-plugins-to-native-html
bjohansebas Aug 2, 2026
244b0ef
feat: add the entry script tag to the template file directly
bjohansebas Aug 2, 2026
03e42c7
docs: map html-webpack-plugin hooks to their native counterparts
bjohansebas Aug 2, 2026
39cac8f
feat: migrate html-webpack-plugin hook taps to the native hooks
bjohansebas Aug 2, 2026
a3d83bf
feat: map multi-page chunks setups to per-entry html descriptors
bjohansebas Aug 2, 2026
f4c3dbf
feat: migrate csp/sri/favicons companion plugins to output.html options
bjohansebas Aug 2, 2026
27514e0
feat: map scriptLoading module and structured meta values
bjohansebas Aug 2, 2026
6218f1c
feat: support inline require instantiation, cover ts/function/merge c…
bjohansebas Aug 2, 2026
e907771
fix: flag html-loader postprocessor and object minimize options
bjohansebas Aug 2, 2026
2d5ce89
docs: point minify hints at minimizer-webpack-plugin
bjohansebas Aug 2, 2026
a337ac2
debug: surface template injection failure reason (temporary)
bjohansebas Aug 2, 2026
eb0932d
fix: resolve template paths by segments, drop the runtime path module
bjohansebas Aug 2, 2026
9e9885d
debug: probe existsSync variants on windows (temporary)
bjohansebas Aug 2, 2026
30285c6
debug: probe readFileSync on windows (temporary)
bjohansebas Aug 2, 2026
ea8b556
fix: probe sandbox-accepted path forms with readFileSync
bjohansebas Aug 2, 2026
245a8e2
fix: make the template edit best-effort with a platform-stable comment
bjohansebas Aug 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/html-plugins-to-native-html.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@webpack/html-plugins-to-native-html": major
---

Add codemod migrating html-webpack-plugin setups to webpack's native HTML support.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ npx codemod run @webpack/<codemod-name>
| Codemod | Description |
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| [`css-plugins-to-native-css`](codemods/css-plugins-to-native-css) | Migrate `mini-css-extract-plugin` and `style-loader`/`css-loader` rules to webpack's native CSS support (`experiments.css`). |
| [`html-plugins-to-native-html`](codemods/html-plugins-to-native-html) | Migrate `html-webpack-plugin` and `html-loader` rules to webpack's native HTML support (`experiments.html`). |

## Contributing

Expand Down
91 changes: 91 additions & 0 deletions codemods/html-plugins-to-native-html/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# @webpack/html-plugins-to-native-html

Migrates webpack configurations from `html-webpack-plugin` and `html-loader` to webpack's native HTML support (`experiments.html` + `output.html`).

> Requires **webpack >= 5.109.0**: the transform relies on the `output.html` options (`title`, `meta`, `favicon`, `base`, `inject`, …) introduced there.

## What it does

- Removes `new HtmlWebpackPlugin(...)` from `plugins` (and the whole `plugins` entry when it becomes empty), and the `html-webpack-plugin` `require`/`import` once it is unused.
- Enables the native pipeline with `experiments: { html: true }` and `output.html` on each migrated configuration.
- Sets `output.htmlFilename` to the plugin's `filename` — or to `"index.html"`, the plugin's default, since the native default is `[name].html`.
- Maps plugin options to their `output.html` counterparts: `title`, `meta` (string values, plus `{ name | property, content }` objects — `og:*` keys included), `favicon`, `base`, `inject` (`"body"`/`"head"`/`false`; `true` is the native default), and `scriptLoading` — `"blocking"` maps directly, `"defer"` is the native default, and `"module"` becomes `output.module: true` + `experiments.outputModule: true` (native module scripts).
- Drops options the native pipeline covers on its own (`minify: true`/`"auto"`, `cache`, `showErrors`, `chunksSortMode`, `chunks: "all"`, `publicPath: "auto"`) silently — webpack's default `optimization.minimizer` (`minimizer-webpack-plugin`) already minifies the emitted HTML/CSS in production; custom `minify` objects are flagged towards it.
- **Multi-page setups**: several instances (or a `chunks: ["name"]` list) map to per-entry `html` descriptors — each listed entry becomes `{ import: …, html: <options> }`, unlisted entries get no page, and `output.htmlFilename: "[name].html"` covers the per-page filenames. Requires each instance to own exactly one entry via `chunks` and no `template`; instance filenames other than `<entry>.html`/`[name].html` are flagged.
- Options without a native equivalent (`hash`, a `minify` object, `chunks` arrays, `templateContent`, `templateParameters`, …) are dropped with a `// Removed html-webpack-plugin options without a native HTML equivalent: …` comment so you can review the behavior change; a manual migration path is appended where one exists.
- **Companion plugins** found next to a migrated `html-webpack-plugin` instance are migrated too: `csp-html-webpack-plugin` → `output.html.csp` (its policy argument becomes `csp.policy`), `webpack-subresource-integrity` → `output.html.integrity` (`hashFuncNames` becomes the algorithm list; `enabled: false` just removes it), and `favicons-webpack-plugin` → `output.html.favicon` (the logo path; the native option also emits the icon set). Options beyond that are dropped with a review comment; instances whose arguments can't be understood are left in place with a comment. In template/multi-page modes options that only apply to generated pages are flagged instead.
- Migrates `HtmlWebpackPlugin.getHooks(...)` taps to the native `webpack.html.HtmlModulesPlugin.getCompilationHooks(...)` stage covering the same moment: `alterAssetTags`/`alterAssetTagGroups` → `transformTags`, `beforeEmit` → `transformHtml`, `afterEmit` → `htmlEmitted`. The native stages take different arguments (`transformTags` hands you mutable tag descriptors instead of `data.assetTags`/head-body arrays; `transformHtml` is a waterfall on the HTML string instead of `data.html`), so each renamed tap gets a `// Review: …` comment describing the new signature — review the callback body.

### `html-loader`

- Removes rules that only wire up `html-loader` (cascading to empty `rules`/`module` entries): with no user rule matching `.html`, webpack's `experiments.html: "auto"` default enables native HTML by itself, and importing an `.html` file from JS natively yields the processed HTML string — the same shape `html-loader` exported.
- Rules with extra conditions or surviving options are kept with `type: "html"` instead — and since their presence disables the `"auto"` default, `experiments.html: true` is added to that configuration.
- Any other loader in the chain (template compilers, custom ones) keeps working in front of native HTML: it stays in `use` while `html-loader` is dropped.
- Loader options: boolean `sources` becomes the rule's `parser: { sources }`; `esModule` and `minimize` are dropped silently (native HTML covers them); a `sources` object or `preprocessor` function is flagged with a review comment (`preprocessor` maps manually to the rule's `parser.template`, which is synchronous and receives `(source, { module, resource })`).

### `template`

`output.html` generates each page from scratch, so an authored template maps to webpack's other native mode instead: the **HTML entry point**. The codemod turns the template into the entry, and — because the HTML file now drives the build — it must load the previous JS entry itself. The review comment always states the exact tag (`<script defer src="…"></script>`, relative to the template), and as a best effort the codemod also edits the template in place, inserting the tag before `</head>` unless it is already there (the in-place edit is skipped where the runtime sandbox blocks file access, e.g. on Windows — the comment still tells you what to add). Since head tags are only injected into webpack-generated pages, `title`/`meta`/`favicon`/`base` are flagged to be added to the template instead.

### What is left untouched

- Multi-page configurations the per-entry shape can't express: an instance whose `chunks` lists several entries (one page aggregating several chunks), combines `chunks` with `template`, or names an entry the config's `entry` object doesn't declare.
- Files that tap `beforeAssetTagGeneration` or `afterTemplateExecution` via `HtmlWebpackPlugin.getHooks(...)`: those stages have no native equivalent (webpack builds the tags and runs the parser template itself).
- Plugin instantiations whose options are not an object literal.

## Usage

```sh
npx codemod run @webpack/html-plugins-to-native-html
```

## Example

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use diff it's much more simpler to read and get what change


Before:

```js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
entry: "./src/index.js",
plugins: [
new HtmlWebpackPlugin({
filename: "app.html",
title: "My App",
meta: { viewport: "width=device-width, initial-scale=1" },
}),
],
};
```

After:

```js
module.exports = {
output: {
html: { title: "My App", meta: { viewport: "width=device-width, initial-scale=1" } },
htmlFilename: "app.html",
},
experiments: {
html: true,
},
entry: "./src/index.js",
};
```

With a `template`, the template becomes the entry point and gets a `<script>` tag for the previous entry added to it:

```js
module.exports = {
experiments: {
html: true,
},
// The template is now the entry and loads the previous entry via <script defer src="./index.js"></script>
entry: "./src/index.html",
output: {
htmlFilename: "index.html",
},
};
```

The codemod also removes `html-webpack-plugin` and `html-loader` from your `package.json` (`dependencies` and `devDependencies`). If other tooling in the repo still uses them (Storybook, test setups, …), reinstall the ones you need.
23 changes: 23 additions & 0 deletions codemods/html-plugins-to-native-html/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
schema_version: "1.0"
name: "@webpack/html-plugins-to-native-html"
version: "0.0.0"
description: Migrate html-webpack-plugin and html-loader rules to webpack's native HTML support (experiments.html)
author: bjohansebas (Sebastian Beltran)
license: MIT
workflow: workflow.yaml
repository: "https://github.com/webpack/codemods/tree/HEAD/codemods/html-plugins-to-native-html"
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration
- webpack

registry:
access: public
visibility: public
27 changes: 27 additions & 0 deletions codemods/html-plugins-to-native-html/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "@webpack/html-plugins-to-native-html",
"private": true,
"version": "0.0.0",
"description": "Migrate html-webpack-plugin and html-loader rules to webpack's native HTML support (experiments.html).",
"type": "module",
"scripts": {
"test": "npm run test:workflow && npm run test:dependencies",
"test:workflow": "npx codemod jssg test -l typescript ./src/workflow.ts",
"test:dependencies": "npx codemod jssg test -l json ./src/remove-dependencies.ts ./tests/remove-dependencies"
},
"repository": {
"type": "git",
"url": "git+https://github.com/webpack/codemods.git",
"directory": "codemods/html-plugins-to-native-html",
"bugs": "https://github.com/webpack/codemods/issues"
},
Comment on lines +12 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's not shallow by the codemod Registry and not going to be publish on npm registry you can remove it. it's reduce maintenance

nodejs/userland-migrations#513

"author": "Sebastian Beltran <bjohansebas@gmail.com>",
"license": "MIT",
"homepage": "https://github.com/webpack/codemods/blob/main/codemods/html-plugins-to-native-html/README.md",
"dependencies": {
"@webpack/codemod-utils": "*"
},
"devDependencies": {
"@codemod.com/jssg-types": "^1.6.2"
}
}
36 changes: 36 additions & 0 deletions codemods/html-plugins-to-native-html/src/remove-dependencies.ts

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type Js from "@codemod.com/jssg-types/langs/javascript";
import type Json from "@codemod.com/jssg-types/langs/json";
import type { SgNode, SgRoot } from "@codemod.com/jssg-types/main";
import { ConfigEditor, findPair, keyName, namedChildren, pairsOf } from "@webpack/codemod-utils";

// Packages replaced by native HTML; review your lockfile if other tooling
// (Storybook, tests, …) still relies on them.
const REMOVED_PACKAGES = new Set([
"html-webpack-plugin",
"html-loader",
"csp-html-webpack-plugin",
"webpack-subresource-integrity",
"favicons-webpack-plugin",
]);
const DEPENDENCY_KEYS = ["dependencies", "devDependencies"];

async function transform(root: SgRoot<Json>): Promise<string | null> {
// JSON shares the object/pair/string node kinds the editor operates on.
const rootNode = root.root() as unknown as SgNode<Js>;
const editor = new ConfigEditor(rootNode);
const manifest = namedChildren(rootNode)[0];
if (!manifest || manifest.kind() !== "object") return null;
for (const key of DEPENDENCY_KEYS) {
const value = findPair(manifest, key)?.field("value");
if (!value || value.kind() !== "object") continue;
for (const pair of pairsOf(value)) {
const name = keyName(pair);
if (name && REMOVED_PACKAGES.has(name)) editor.markForRemoval(pair);
}
}
editor.finalizeRemovals();
if (!editor.hasEdits) return null;
return editor.commit();
}

export default transform;
Loading
Loading