-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add html-plugins-to-native-html codemod #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
bccba82
ba91d13
244b0ef
03e42c7
39cac8f
a3d83bf
f4c3dbf
27514e0
6218f1c
e907771
2d5ce89
a337ac2
eb0932d
9e9885d
30285c6
ea8b556
245a8e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. |
| 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 | ||
|
|
||
| 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. | ||
| 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 |
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| "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" | ||
| } | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. https://github.com/nodejs/userland-migrations/blob/main/utils/src/remove-dependencies.ts also copy test to be sure |
| 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; |
There was a problem hiding this comment.
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