diff --git a/.github/workflows/daily-rebuild.yml b/.github/workflows/daily-rebuild.yml new file mode 100644 index 00000000..4785c55a --- /dev/null +++ b/.github/workflows/daily-rebuild.yml @@ -0,0 +1,24 @@ +name: Daily Vercel Redeploy + +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + redeploy: + runs-on: ubuntu-latest + + steps: + - name: Trigger Vercel redeploy + env: + VERCEL_REDEPLOY_HOOK: ${{ secrets.VERCEL_REDEPLOY_HOOK }} + run: | + curl \ + --fail-with-body \ + --silent \ + --show-error \ + --location \ + "$VERCEL_REDEPLOY_HOOK" diff --git a/README.md b/README.md index 77a9f633..235d695d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# webpack-doc-kit +# webpack.js.org Automated TypeScript API documentation generator for [webpack](https://github.com/webpack/webpack). Extracts type definitions from webpack's `types.d.ts` and produces Markdown and HTML documentation, deployed to GitHub Pages. diff --git a/components/Layout.jsx b/components/Layout.jsx index ffaf5466..3a766399 100644 --- a/components/Layout.jsx +++ b/components/Layout.jsx @@ -1,4 +1,6 @@ import DefaultLayout from '@node-core/doc-kit/src/generators/web/ui/components/Layout/index.jsx'; +import { Analytics } from '@vercel/analytics/react'; +import { SpeedInsights } from '@vercel/speed-insights/react'; import HomeLayout from '../layouts/Home/index.jsx'; import SponsorsLayout from '../layouts/Sponsors/index.jsx'; import BlogLayout from '../layouts/Blog/index.jsx'; @@ -17,5 +19,11 @@ const LAYOUTS = { export default function Layout(props) { const Component = LAYOUTS[props.metadata.layout] ?? DefaultLayout; - return ; + return ( + <> + + + + + ); } diff --git a/layouts/PageNotFound/index.jsx b/layouts/PageNotFound/index.jsx index 697195fe..bfb1cdaa 100644 --- a/layouts/PageNotFound/index.jsx +++ b/layouts/PageNotFound/index.jsx @@ -21,7 +21,7 @@ export default ({ metadata }) => { Go to Home Report an issue diff --git a/package-lock.json b/package-lock.json index ceac1875..051cd271 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,15 @@ { - "name": "webpack-doc-kit", + "name": "webpack.js.org", "lockfileVersion": 3, "requires": true, "packages": { "": { + "name": "webpack.js.org", "dependencies": { "@node-core/doc-kit": "1.4.3", "@tailwindcss/node": "^4.3.0", + "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "classnames": "^2.5.1", "gray-matter": "^4.0.3", "npm-run-all": "^4.1.5", @@ -3117,6 +3120,86 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@vercel/analytics": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@vercel/analytics/-/analytics-2.0.1.tgz", + "integrity": "sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==", + "license": "MIT", + "peerDependencies": { + "@remix-run/react": "^2", + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "nuxt": ">= 3", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@remix-run/react": { + "optional": true + }, + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "nuxt": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, + "node_modules/@vercel/speed-insights": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-2.0.0.tgz", + "integrity": "sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==", + "license": "Apache-2.0", + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "nuxt": ">= 3", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "nuxt": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -8772,7 +8855,6 @@ "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.7.0.tgz", "integrity": "sha512-Z4WR8fmLMRpdYqJ9i7vrlXSsSrxVJydwrkEXHapexfARbWfGb7vGcnvNQnIzN0cXciMVOlz/XLoiMCi9gUsy9Q==", "license": "MIT", - "peer": true, "peerDependencies": { "preact": ">=10 || >= 11.0.0-0" } diff --git a/package.json b/package.json index fbaed220..10e72104 100644 --- a/package.json +++ b/package.json @@ -1,4 +1,5 @@ { + "name": "webpack.js.org", "scripts": { "build:prepare": "node scripts/prepare/index.mjs", "build:data": "npm-run-all build:data:*", @@ -24,6 +25,8 @@ "dependencies": { "@node-core/doc-kit": "1.4.3", "@tailwindcss/node": "^4.3.0", + "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "classnames": "^2.5.1", "gray-matter": "^4.0.3", "npm-run-all": "^4.1.5", diff --git a/pages/guides/contributing/debugging.md b/pages/guides/contributing/debugging.md new file mode 100644 index 00000000..d779ecf9 --- /dev/null +++ b/pages/guides/contributing/debugging.md @@ -0,0 +1,47 @@ +--- +title: Debugging +authors: skipjack,tbroadley,madhavarshney,bhavya9107,akaustav +--- + +# Debugging + +When contributing to the core repo, writing a loader/plugin, or even working on a complex project, debugging tools can be central to your workflow. Whether the problem is slow performance on a large project or an unhelpful traceback, the following utilities can make figuring it out less painful. + +- The [`Stats` API](/docs/api/v5.x/stats/Stats) available through webpack's Node.js API and the [CLI](/docs/api/v5.x/cli). +- Chrome **DevTools** and the latest Node.js version. + +## Stats + +Whether you inspect it manually or pass it to another tool, webpack's [`Stats` object](/docs/api/v5.x/stats/Stats) can be extremely useful when debugging build issues. It can provide the following information: + +- The contents of every module. +- The modules contained within every chunk. +- Per module compilation and resolving stats. +- Build errors and warnings. +- The relationships between modules. +- And much more... + +On top of that, the official [analyze tool](https://github.com/webpack/analyse) and [various others](/guides/optimization/code-splitting/#bundle-analysis) will accept this data and visualize it in various ways. + +## DevTools + +While [`console`](https://nodejs.org/api/console.html) statements may work well in straightforward scenarios, sometimes a more robust solution is needed. As most front-end developers already know, Chrome DevTools are a life saver when debugging web applications, _but they don’t have to stop there_. As of Node v6.3.0+, developers can use the built-in `--inspect` flag to debug a node program in DevTools. + +Let's start by invoking webpack with the `node --inspect`. + +Note that we cannot run npm `scripts`, e.g. `npm run build`, so we'll have to specify the full `node_modules` path: + +```bash +node --inspect ./node_modules/webpack/bin/webpack.js +``` + +Which should output something like: + +```bash +Debugger listening on ws://127.0.0.1:9229/c624201a-250f-416e-a018-300bbec7be2c +For help see https://nodejs.org/en/docs/inspector +``` + +Now jump to `chrome://inspect` in the browser and you should see any active scripts you've inspected under the _Remote Target_ header. Click the "inspect" link under each script to open a dedicated debugger or the _Open dedicated DevTools for Node_ link for a session that will connect automatically. You can also check out the [NiM extension](https://chrome.google.com/webstore/detail/nodejs-v8-inspector-manag/gnhhdgbaldcilmgcpfddgdbkhjohddkj), a handy Chrome plugin that will automatically open a DevTools tab every time you `--inspect` a script. + +We recommend using the `--inspect-brk` flag which will break on the first statement of the script allowing you to go through the source to set breakpoints and start/stop the build as you please. Also, don't forget that you can still pass arguments to the script. For example, if you have multiple configuration files you could pass `--config webpack.prod.js` to specify the configuration you'd like to debug. diff --git a/pages/guides/contributing/plugin-patterns.md b/pages/guides/contributing/plugin-patterns.md new file mode 100644 index 00000000..1b4add50 --- /dev/null +++ b/pages/guides/contributing/plugin-patterns.md @@ -0,0 +1,149 @@ +--- +title: Plugin Patterns +authors: nveenjain,EugeneHlushko,benglynn,raj-sapalya +--- + +# Plugin Patterns + +Plugins grant unlimited opportunity to perform customizations within the webpack build system. This allows you to create custom asset types, perform unique build modifications, or even enhance the webpack runtime while using middleware. The following are some features of webpack that become useful while writing plugins. + +## Exploring assets, chunks, modules, and dependencies + +After a compilation is sealed, all structures within the compilation may be traversed. + +```js +class MyPlugin { + apply(compiler) { + compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { + // Explore each chunk (build output): + for (const chunk of compilation.chunks) { + // Explore each module within the chunk (built inputs): + for (const module of chunk.getModules()) { + // Explore each source file path that was included into the module: + if (module.buildInfo && module.buildInfo.fileDependencies) { + for (const fileDependency of module.buildInfo.fileDependencies) { + // we've learned a lot about the source structure now... + console.log(fileDependency); + } + } + } + + // Explore each asset filename generated by the chunk: + for (const filename of chunk.files) { + // Get the asset source for each file generated by the chunk: + const source = compilation.assets[filename].source(); + } + } + + callback(); + }); + } +} + +export default MyPlugin; +``` + +- `compilation.modules`: A set of modules (built inputs) in the compilation. Each module manages the build of a raw file from your source library. + +> [!WARNING] +> **Deprecation warning**: Array functions will still work. + +- `module.fileDependencies`: An array of source file paths included into a module. This includes the source JavaScript file itself (ex: `index.js`), and all dependency asset files (stylesheets, images, etc) that it has required. Reviewing dependencies is useful for seeing what source files belong to a module. +- `compilation.chunks`: A set of chunks (build outputs) in the compilation. Each chunk manages the composition of a final rendered assets. + +> [!WARNING] +> **Deprecation warning**: Array functions will still work. + +- `chunk.getModules()`: An array of modules that are included into a chunk. By extension, you may look through each module's dependencies to see what raw source files fed into a chunk. +- `chunk.files`: A Set of output filenames generated by the chunk. You may access these asset sources from the `compilation.assets` table. + +### Monitoring the watch graph + +While running webpack middleware, each compilation includes a `fileDependencies` `Set` (what files are being watched) and a `fileTimestamps` `Map` that maps watched file paths to a timestamp. These are extremely useful for detecting what files have changed within the compilation: + +```js +class MyPlugin { + constructor() { + this.startTime = Date.now(); + this.prevTimestamps = new Map(); + } + + apply(compiler) { + compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { + const changedFiles = [...compilation.fileTimestamps.keys()].filter( + watchfile => + (this.prevTimestamps.get(watchfile) || this.startTime) < + (compilation.fileTimestamps.get(watchfile) || Infinity) + ); + + this.prevTimestamps = compilation.fileTimestamps; + callback(); + }); + } +} + +export default MyPlugin; +``` + +You may also feed new file paths into the watch graph to receive compilation triggers when those files change. Add valid file paths into the `compilation.fileDependencies` `Set` to add them to the watched files. + +> [!TIP] +> The `fileDependencies` `Set` is rebuilt in each compilation, so your plugin must add its own watched dependencies into each compilation to keep them under watch. + +> [!WARNING] +> Since webpack 5, `compilation.fileDependencies`, `compilation.contextDependencies` and `compilation.missingDependencies` are now a `Set` instead of a `Sortable Set` and thus no longer sorted. + +## Changed chunks + +Similar to the watch graph, you can monitor changed chunks within a compilation by tracking their content hashes. + +> [!WARNING] +> `compilation.chunks` is a `Set`, not an array. Calling `.filter()` or `.map()` directly on it throws a `TypeError`. +> Use `for...of` or spread it into an array first. + +> [!WARNING] +> `chunk.hash` is not defined on chunk objects. Use `chunk.contentHash.javascript` instead. +> Accessing `chunk.hash` returns `undefined`, causing every chunk to appear changed on every rebuild. + +```js +class MyPlugin { + constructor() { + this.chunkVersions = {}; + } + + apply(compiler) { + compiler.hooks.emit.tapAsync('MyPlugin', (compilation, callback) => { + const changedChunks = []; + + for (const chunk of compilation.chunks) { + const key = chunk.id ?? chunk.name; + const currentHash = chunk.contentHash.javascript; + + if (currentHash === undefined) continue; + + const oldHash = this.chunkVersions[key]; + this.chunkVersions[key] = currentHash; + + if (currentHash !== oldHash) { + changedChunks.push(chunk); + } + } + + if (changedChunks.length > 0) { + console.log( + 'Changed chunks:', + changedChunks.map(chunk => chunk.id) + ); + } + + callback(); + }); + } +} + +export default MyPlugin; +``` + +> [!TIP] +> Use `chunk.id ?? chunk.name` as the tracking key. Some chunks have an `id` but no `name`, so relying on `chunk.name` alone can miss changes. +> CSS or other non-JS chunks may not have a `javascript` key in `contentHash` — the `continue` guard skips those safely. diff --git a/pages/guides/contributing/writers-guide.md b/pages/guides/contributing/writers-guide.md new file mode 100644 index 00000000..91593d89 --- /dev/null +++ b/pages/guides/contributing/writers-guide.md @@ -0,0 +1,128 @@ +--- +title: Writer's Guide +authors: pranshuchittora,EugeneHlushko,shivxmsharma +--- + +# Writer's Guide + +This guide explains how to contribute hand-written content to the webpack website. It does not cover API references, loader and plugin references, or governance pages because those are generated from their source repositories. + +## Choose the source of truth + +Before editing a page, identify who owns its content: + +| Content | Source of truth | How this site receives it | +| --------------------------------------- | ---------------------------------- | ------------------------------------------ | +| Guides and conceptual documentation | `pages/guides/` in this repository | Written and reviewed here | +| webpack API and configuration reference | webpack source, types, and schemas | Generated by `npm run build:md:api` | +| Loader and plugin reference pages | Package README files | Generated by `npm run build:md:readmes` | +| Governance documentation | webpack governance files | Generated by `npm run build:md:governance` | + +> [!IMPORTANT] +> Do not edit generated pages under `pages/docs/api/`, `pages/docs/loaders/`, `pages/docs/plugins/`, or `pages/about/governance/`. Make the change in the owning repository and regenerate the site instead. + +## Add or edit a guide + +Guide pages are Markdown files under `pages/guides/`. Place new content in the section that best matches the reader's task. Prefer expanding an existing page over creating a second page with overlapping material. + +Every page starts with YAML frontmatter and an H1 that matches its title: + +```md +--- +title: Build Performance +authors: github-user,another-user +--- + +# Build Performance +``` + +- `title` is the page title displayed by the site. +- `authors` is a comma-separated list of GitHub usernames. Add your username when you make a meaningful contribution. + +When adding or moving a page, update `pages/guides/site.json` so it appears in the Guides sidebar. Its `link` must match the page's generated URL: an `index.md` file represents its directory, while any other filename represents a page without a trailing slash. + +## Write for the reader's task + +Start by saying what the reader will accomplish and why it matters. Put prerequisites before the steps that require them, then organize the page around the work the reader performs. + +- Use direct instructions and concrete examples. +- Introduce one idea at a time. +- Explain unfamiliar terms when they first appear. +- Avoid “simple,” “just,” and other language that assumes the reader's experience. +- Use “webpack” in lowercase except at the beginning of a sentence. +- Write loader package names in kebab case, such as `css-loader`. +- Write plugin class names in Pascal case, such as `DefinePlugin`. +- Refer to releases as “webpack 5,” not “webpack v5.” + +Use sentence case for headings. Keep the page title as the only H1 and nest later headings without skipping levels. + +## Code examples + +Examples should be complete enough to understand and focused on the behavior being taught. + +- Use fenced code blocks with the correct language. +- Use single quotes in JavaScript examples. +- Use `node:` prefixes for Node.js built-ins. +- Prefer ECMAScript modules in new examples. +- Include filenames with `displayName` when the surrounding text does not make the file clear. +- Use a `diff` block when a reader is changing an existing example. +- Test commands and runnable examples before opening a pull request. + +```js displayName="webpack.config.js" +export default { + mode: 'development', +}; +``` + +## Links + +Use root-relative links for pages on this site and full `https://` URLs for external resources: + +```md +[Asset Modules](/guides/core-workflows/asset-modules) +[Node.js](https://nodejs.org/) +``` + +Check links against the generated output, not the old webpack site. Current webpack API links include their version, for example `/docs/api/v5.x/options#mode`. + +Do not add a trailing slash to a normal page route. Directory index pages, such as `/guides/contributing/`, are the exception. + +## Callouts + +Use callouts sparingly for information that should stand apart from the main flow: + +```md +> [!TIP] +> Optional advice that helps in a common situation. + +> [!IMPORTANT] +> Information the reader needs to succeed. + +> [!WARNING] +> A risk or behavior that can cause an incorrect result. +``` + +Do not use callouts as a substitute for explaining required steps in the main text. + +## Preview and verify + +Install dependencies and generate the site: + +```bash +npm install +npm run build +``` + +For changes limited to hand-written pages, you can regenerate the HTML after the generated Markdown already exists: + +```bash +npm run build:html +``` + +Before opening a pull request: + +1. Read the rendered page from beginning to end. +2. Check every internal and external link you changed. +3. Confirm code samples and commands are accurate. +4. Run `npm run format:check` and `npm run lint`. +5. Keep the pull request focused on one documentation task. diff --git a/pages/guides/contributing/writing-a-loader.md b/pages/guides/contributing/writing-a-loader.md new file mode 100644 index 00000000..643b0794 --- /dev/null +++ b/pages/guides/contributing/writing-a-loader.md @@ -0,0 +1,478 @@ +--- +title: Writing a Loader +authors: asulaiman,michael-ciniawsky,byzyk,anikethsaha,jamesgeorge007,chenxsan,dev-itsheng,evenstensberg,hagemaruwu,raj-sapalya +--- + +# Writing a Loader + +A loader is a Node.js module that exports a function. webpack calls this function when a resource needs to be transformed. The function receives the [loader context API](/docs/api/v5.x/loaders/types#interface-loaderrunnerloadercontext) through `this`. + +## Setup + +Before we dig into the different types of loaders, their usage, and examples, let's take a look at the three ways you can develop and test a loader locally. + +To test a single loader, you can use `path` to `resolve` a local file within a rule object: + +**webpack.config.js** + +```js +import path from 'node:path'; + +export default { + // ... + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: path.resolve('path/to/loader.js'), + options: {/* ... */}, + }, + ], + }, + ], + }, +}; +``` + +To test multiple, you can utilize the `resolveLoader.modules` configuration to update where webpack will search for loaders. For example, if you had a local `/loaders` directory in your project: + +**webpack.config.js** + +```js +import path from 'node:path'; + +const __dirname = import.meta.dirname; + +export default { + // ... + resolveLoader: { + modules: ['node_modules', path.resolve(__dirname, 'loaders')], + }, +}; +``` + +By the way, if you've already created a separate repository and package for your loader, you could [`npm link`](https://docs.npmjs.com/cli/link) it to the project in which you'd like to test it out. + +> [!TIP] +> You can use [`webpack-defaults` package](https://github.com/webpack-contrib/webpack-defaults) to generate boilerplate code necessary to start writing your loader. + +## Simple Usage + +When a single loader is applied to the resource, the loader is called with only one parameter – a string containing the content of the resource file. + +Synchronous loaders can `return` a single value representing the transformed module. In more complex cases, the loader can return any number of values by using the `this.callback(err, values...)` function. Errors are either passed to the `this.callback` function or thrown in a sync loader. + +The loader is expected to give back one or two values. The first value is a resulting JavaScript code as string or buffer. The second optional value is a SourceMap as JavaScript object. + +## Complex Usage + +When multiple loaders are chained, it is important to remember that they are executed in reverse order – either right to left or bottom to top depending on array format. + +- The last loader, called first, will be passed the contents of the raw resource. +- The first loader, called last, is expected to return JavaScript and an optional source map. +- The loaders in between will be executed with the result(s) of the previous loader in the chain. + +In the following example, the `foo-loader` would be passed the raw resource and the `bar-loader` would receive the output of the `foo-loader` and return the final transformed module and a source map if necessary. + +**webpack.config.js** + +```js +export default { + // ... + module: { + rules: [ + { + test: /\.js/, + use: ['bar-loader', 'foo-loader'], + }, + ], + }, +}; +``` + +## Pitching loaders + +Loaders normally run **right to left**. A loader can also export a +`pitch` function, which runs **left to right** before the normal phase +begins. This allows a loader to pass data to its own normal phase or +short-circuit the remaining loader chain. + +```js +// my-loader.js +export default function (source) { + // Normal phase — runs right to left + const prefix = this.data.value ?? ''; + return `${prefix}\n${source}`; +} + +export function pitch(remainingRequest, precedingRequest, data) { + // Pitch phase — runs left to right before normal loaders + data.value = '/* processed by my-loader */'; +} +``` + +> [!TIP] +> `data` is shared only between the `pitch` and normal functions of +> the same loader. It is not shared across different loaders in +> the chain. + +### Short-circuiting the loader chain + +If a `pitch` function returns a value, webpack skips the remaining +loaders to the right and reverses immediately. This can be useful when +you want to generate module code early and bypass the normal phase. + +```js +export function pitch(remainingRequest) { + return ` +import style from ${JSON.stringify(`!!${remainingRequest}`)}; +const el = document.createElement("style"); +el.textContent = style; +document.head.appendChild(el); + `; +} +``` + +> [!WARNING] +> When a `pitch` function returns a value, the default export of that +> loader does not run. Only use this pattern when you intend to replace +> the normal processing pipeline. + +## Guidelines + +The following guidelines should be followed when writing a loader. They are ordered in terms of importance and some only apply in certain scenarios, read the detailed sections that follow for more information. + +- Keep them **simple**. +- Utilize **chaining**. +- Emit **modular** output. +- Make sure they're **stateless**. +- Employ **loader utilities**. +- Mark **loader dependencies**. +- Resolve **module dependencies**. +- Extract **common code**. +- Avoid **absolute paths**. +- Use **peer dependencies**. + +### Simple + +Loaders should do only a single task. This not only makes the job of maintaining each loader easier, but also allows them to be chained for usage in more scenarios. + +### Chaining + +Take advantage of the fact that loaders can be chained together. Instead of writing a single loader that tackles five tasks, write five simpler loaders that divide this effort. Isolating them not only keeps each individual loader simple, but may allow for them to be used for something you hadn't thought of originally. + +Take the case of rendering a template file with data specified via loader options or query parameters. It could be written as a single loader that compiles the template from source, executes it and returns a module that exports a string containing the HTML code. However, in accordance with guidelines, an `apply-loader` exists that can be chained with other open source loaders: + +- `pug-loader`: Convert template to a module that exports a function. +- `apply-loader`: Executes the function with loader options and returns raw HTML. +- `html-loader`: Accepts HTML and outputs a valid JavaScript module. + +> [!TIP] +> The fact that loaders can be chained also means they don't necessarily have to output JavaScript. As long as the next loader in the chain can handle its output, the loader can return any type of module. + +### Modular + +Keep the output modular. Loader generated modules should respect the same design principles as normal modules. + +### Stateless + +Make sure the loader does not retain state between module transformations. Each run should always be independent of other compiled modules as well as previous compilations of the same module. + +### Loader Utilities + +Take advantage of the [`loader-utils`](https://github.com/webpack/loader-utils) package which provides a variety of useful tools. Along with `loader-utils`, the [`schema-utils`](https://github.com/webpack/schema-utils) package should be used for consistent JSON Schema based validation of loader options. Here's a brief example that utilizes both: + +**loader.js** + +```js +import { urlToRequest } from 'loader-utils'; +import { validate } from 'schema-utils'; + +const schema = { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, +}; + +export default function (source) { + const options = this.getOptions(); + + validate(schema, options, { + name: 'Example Loader', + baseDataPath: 'options', + }); + + console.log('The request path', urlToRequest(this.resourcePath)); + + // Apply some transformations to the source... + + return `export default ${JSON.stringify(source)}`; +} +``` + +### Data Sharing + +In webpack, loaders can be chained together and share data with subsequent loaders in the chain. To achieve this, you can pass data along with the content (source code) using the `this.callback` method in raw loaders. In the default exported function of a raw loader, you can pass data using the fourth argument of `this.callback`. + +```js +export default function (source) { + const options = getOptions(this); + // Pass data using the fourth argument of this.callback + this.callback(null, `export default ${JSON.stringify(source)}`, null, { + some: data, + }); +} +``` + +In the example above, `some` property in the fourth argument of `this.callback` is used to pass data to the next chained loader. + +### Loader Dependencies + +If a loader uses external resources (i.e. by reading from filesystem), they **must** indicate it. This information is used to invalidate cacheable loaders and recompile in watch mode. Here's a brief example of how to accomplish this using the `addDependency` method: + +**loader.js** + +```js +import path from 'node:path'; + +export default function (source) { + const callback = this.async(); + const headerPath = path.resolve('header.js'); + + this.addDependency(headerPath); + + fs.readFile(headerPath, 'utf8', (err, header) => { + if (err) return callback(err); + callback(null, `${header}\n${source}`); + }); +} +``` + +### Module Dependencies + +Depending on the type of module, there may be a different schema used to specify dependencies. In CSS for example, the `@import` and `url(...)` statements are used. These dependencies should be resolved by the module system. + +This can be done in one of two ways: + +- By transforming them to `require` statements. +- Using the `this.resolve` function to resolve the path. + +The `css-loader` is a good example of the first approach. It transforms dependencies to `require`s, by replacing `@import` statements with a `require` to the other stylesheet and `url(...)` with a `require` to the referenced file. + +In the case of the `less-loader`, it cannot transform each `@import` to a `require` because all `.less` files must be compiled in one pass for variables and mixin tracking. Therefore, the `less-loader` extends the less compiler with custom path resolving logic. It then takes advantage of the second approach, `this.resolve`, to resolve the dependency through webpack. + +> [!TIP] +> If the language only accepts relative urls (e.g. `url(file)` always refers to `./file`), you can use the `~` convention to specify references to installed modules (e.g. those in `node_modules`). In the case of `url`, that would look something like `url('~some-library/image.jpg')`. + +### Common Code + +- Avoid generating common code in every module the loader processes. Instead, create a runtime file in the loader and `import` (or `require`) it as a shared module: + +> [!WARNING] +> While CommonJS syntax (`require`) is completely supported, we highly recommend using ECMAScript Modules (`import`) for new loaders. + +**src/loader-runtime.js** + +```js +import { someOtherModule } from './some-other-module.js'; + +export default function runtime(params) { + const x = params.y * 2; + + return someOtherModule(params, x); +} +``` + +**src/loader.js** + +```js +import runtime from './loader-runtime.js'; + +export default function loader(source) { + // Custom loader logic + + return `${runtime({ + source, + y: Math.random(), + })}`; +} +``` + +### Absolute Paths + +Don't insert absolute paths into the module code as they break hashing when the root for the project is moved. You can use below code to convert absolute paths to relative ones. + +```js +// `loaderContext` is same as `this` inside loader function +JSON.stringify( + loaderContext.utils.contextify( + loaderContext.context || loaderContext.rootContext, + request + ) +); +``` + +### Peer Dependencies + +If the loader you're working on is a simple wrapper around another package, then you should include the package as a `peerDependency`. This approach allows the application's developer to specify the exact version in the `package.json` if desired. + +For instance, the `sass-loader` [specifies `node-sass`](https://github.com/webpack/sass-loader/blob/main/package.json) as peer dependency like so: + +```json +{ + "peerDependencies": { + "node-sass": "^4.0.0" + } +} +``` + +## Testing + +So you've written a loader, followed the guidelines above, and have it set up to run locally. What's next? Let's go through a unit testing example to ensure our loader is working the way we expect. We'll be using the [Jest](https://jestjs.io/) framework to do this. We'll also install `babel-jest` and some presets that will allow us to use the `import` / `export` and `async` / `await`. Let's start by installing and saving these as a `devDependencies`: + +```bash +npm install --save-dev jest babel-jest @babel/core @babel/preset-env +``` + +**babel.config.js** + +```js +export default { + presets: [ + [ + '@babel/preset-env', + { + targets: { + node: 'current', + }, + }, + ], + ], +}; +``` + +Our loader will process `.txt` files and replace any instance of `[name]` with the `name` option given to the loader. Then it will output a valid JavaScript module containing the text as its default export: + +**src/loader.js** + +```js +export default function loader(source) { + const options = this.getOptions(); + + source = source.replaceAll(/\[name\]/, options.name); + + return `export default ${JSON.stringify(source)}`; +} +``` + +We'll use this loader to process the following file: + +**test/example.txt** + +```bash +Hey [name]! +``` + +Pay close attention to this next step: we'll use webpack's [`Compiler` API](/docs/api/v5.x/compilation/Compiler) and [`memfs`](https://github.com/streamich/memfs) to execute webpack. This avoids writing output to disk and gives us access to the [`Stats` object](/docs/api/v5.x/stats/Stats), which we can use to inspect the transformed module: + +```bash +npm install --save-dev webpack memfs +``` + +**test/compiler.js** + +```js +import path from 'node:path'; +import { Volume, createFsFromVolume } from 'memfs'; +import webpack from 'webpack'; + +const __dirname = import.meta.dirname; + +export default (fixture, options = {}) => { + const compiler = webpack({ + context: __dirname, + entry: `./${fixture}`, + output: { + path: path.resolve(__dirname), + filename: 'bundle.js', + }, + module: { + rules: [ + { + test: /\.txt$/, + use: { + loader: path.resolve(__dirname, '../src/loader.js'), + options, + }, + }, + ], + }, + }); + + compiler.outputFileSystem = createFsFromVolume(new Volume()); + compiler.outputFileSystem.join = path.join.bind(path); + + return new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) reject(err); + if (stats.hasErrors()) reject(stats.toJson().errors); + + resolve(stats); + }); + }); +}; +``` + +> [!TIP] +> In this case, we've inlined our webpack configuration but you can also accept a configuration as a parameter to the exported function. This would allow you to test multiple setups using the same compiler module. + +And now, finally, we can write our test and add an npm script to run it: + +**test/loader.test.js** + +```js +/** + * @jest-environment node + */ +import compiler from './compiler.js'; + +test('Inserts name and outputs JavaScript', async () => { + const stats = await compiler('example.txt', { name: 'Alice' }); + const output = stats.toJson({ source: true }).modules[0].source; + + expect(output).toBe('export default "Hey Alice!\\n"'); +}); +``` + +**package.json** + +```json +{ + "scripts": { + "test": "jest" + }, + "jest": { + "testEnvironment": "node" + } +} +``` + +With everything in place, we can run it and see if our new loader passes the test: + +```bash + PASS test/loader.test.js + ✓ Inserts name and outputs JavaScript (229ms) + +Test Suites: 1 passed, 1 total +Tests: 1 passed, 1 total +Snapshots: 0 total +Time: 1.853s, estimated 2s +Ran all test suites. +``` + +It worked! At this point you should be ready to start developing, testing, and deploying your own loaders. We hope that you'll share your creations with the rest of the community! diff --git a/pages/guides/contributing/writing-a-plugin.md b/pages/guides/contributing/writing-a-plugin.md new file mode 100644 index 00000000..9e7c9761 --- /dev/null +++ b/pages/guides/contributing/writing-a-plugin.md @@ -0,0 +1,501 @@ +--- +title: Writing a Plugin +authors: slavafomin,tbroadley,nveenjain,iamakulov,byzyk,franjohn21,EugeneHlushko,snitin315,rahul3v,jamesgeorge007 +--- + +# Writing a Plugin + +Plugins expose the full potential of the webpack engine to third-party developers. Using staged build callbacks, developers can introduce their own behaviors into the webpack build process. Building plugins is a bit more advanced than building loaders, because you'll need to understand some of the webpack low-level internals to hook into them. Be prepared to read some source code! + +## Creating a Plugin + +A plugin for webpack consists of: + +- A named JavaScript function or a JavaScript class. +- Defines `apply` method in its prototype. +- Specifies a hook on the [`Compiler`](/docs/api/v5.x/compilation/Compiler) or [`Compilation`](/docs/api/v5.x/compilation/Compilation) to tap into. +- Manipulates webpack internal instance specific data. +- Invokes webpack provided callback after functionality is complete. + +```js +// A JavaScript class. +class MyExampleWebpackPlugin { + // Define `apply` as its prototype method which is supplied with compiler as its argument + apply(compiler) { + // Hook into the compilation + compiler.hooks.thisCompilation.tap( + 'MyExampleWebpackPlugin', + compilation => { + // Specify the asset processing hook + compilation.hooks.processAssets.tap( + { + name: 'MyExampleWebpackPlugin', + stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONS, + }, + assets => { + console.log('This is an example plugin!'); + console.log( + 'Here’s the `compilation` object which represents a single build of assets:', + compilation + ); + } + ); + } + ); + } +} +``` + +## Basic plugin architecture + +Plugins are instantiated objects with an `apply` method on their prototype. This `apply` method is called once by the webpack compiler while installing the plugin. The `apply` method is given a reference to the underlying webpack compiler, which grants access to compiler callbacks. A plugin is structured as follows: + +```js +class HelloWorldPlugin { + apply(compiler) { + // This hook runs when the build process is fully completed + compiler.hooks.done.tap( + 'Hello World Plugin', + ( + stats /* stats is passed as an argument when done hook is tapped. */ + ) => { + console.log('Hello World!'); + } + ); + } +} + +export default HelloWorldPlugin; +``` + +Then to use the plugin, include an instance in your webpack configuration `plugins` array: + +```js +// webpack.config.js +import HelloWorldPlugin from 'hello-world'; + +export default { + // ... configuration settings here ... + plugins: [new HelloWorldPlugin({ options: true })], +}; +``` + +A familiar way to validate plugin options in webpack versions **before `5.106`** is to use [`schema-utils`](https://github.com/webpack/schema-utils) in the constructor: + +```js +import { validate } from 'schema-utils'; + +// schema for options object +const schema = { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, +}; + +export default class HelloWorldPlugin { + constructor(options = {}) { + validate(schema, options, { + name: 'Hello World Plugin', + baseDataPath: 'options', + }); + } + + apply(compiler) {} +} +``` + +Starting in webpack `5.106`, you can move that validation into `apply()` by tapping `compiler.hooks.validate` and calling `compiler.validate(...)` instead: + +```js +export default class HelloWorldPlugin { + constructor(options = {}) { + this.options = options; + } + + apply(compiler) { + compiler.hooks.validate.tap('HelloWorldPlugin', () => { + compiler.validate( + () => require('./schema/hello-world-plugin.json'), + this.options + ); + }); + } +} +``` + +> [!WARNING] +> This validation flow is skipped when webpack is configured with `validate: false`. With `experiments.futureDefaults`, validation is enabled by default in development mode and disabled by default in production mode. + +## Compiler and Compilation + +The two most important resources when developing plugins are the [`Compiler`](/docs/api/v5.x/compilation/Compiler) and [`Compilation`](/docs/api/v5.x/compilation/Compilation) objects. Understanding their roles is an important first step in extending the webpack engine. + +```js +class HelloCompilationPlugin { + apply(compiler) { + // Tap into compilation hook which gives compilation as argument to the callback function + compiler.hooks.compilation.tap('HelloCompilationPlugin', compilation => { + // Now we can tap into various hooks available through compilation + compilation.hooks.optimize.tap('HelloCompilationPlugin', () => { + console.log('Assets are being optimized.'); + }); + }); + } +} + +export default HelloCompilationPlugin; +``` + +For the public classes and types exposed by webpack, see the [webpack API reference](/docs/api/v5.x/). + +## Async event hooks + +Some plugin hooks are asynchronous. To tap into them, we can use `tap` method which will behave in synchronous manner or use one of `tapAsync` method or `tapPromise` method which are asynchronous methods. + +### tapAsync + +When we use `tapAsync` method to tap into plugins, we need to call the callback function which is supplied as the last argument to our function. + +```js +class HelloAsyncPlugin { + apply(compiler) { + compiler.hooks.emit.tapAsync( + 'HelloAsyncPlugin', + (compilation, callback) => { + // Do something async... + setTimeout(() => { + console.log('Done with async work...'); + callback(); + }, 1000); + } + ); + } +} + +export default HelloAsyncPlugin; +``` + +#### tapPromise + +When we use `tapPromise` method to tap into plugins, we need to return a promise which resolves when our asynchronous task is completed. + +```js +class HelloAsyncPlugin { + apply(compiler) { + compiler.hooks.emit.tapPromise( + 'HelloAsyncPlugin', + compilation => + // return a Promise that resolves when we are done... + new Promise((resolve, reject) => { + setTimeout(() => { + console.log('Done with async work...'); + resolve(); + }, 1000); + }) + ); + } +} + +export default HelloAsyncPlugin; +``` + +## Example + +Once we can latch onto the webpack compiler and each individual compilations, the possibilities become endless for what we can do with the engine itself. We can reformat existing files, create derivative files, or fabricate entirely new assets. + +Let's write an example plugin that generates a new build file called `assets.md`, the contents of which will list all of the asset files in our build. This plugin might look something like this: + +```js +class FileListPlugin { + static defaultOptions = { + outputFile: 'assets.md', + }; + + // Any options should be passed in the constructor of your plugin, + // (this is a public API of your plugin). + constructor(options = {}) { + // Applying user-specified options over the default options + // and making merged options further available to the plugin methods. + // You should probably validate all the options here as well. + this.options = { ...FileListPlugin.defaultOptions, ...options }; + } + + apply(compiler) { + const pluginName = FileListPlugin.name; + + // webpack module instance can be accessed from the compiler object, + // this ensures that correct version of the module is used + // (do not require/import the webpack or any symbols from it directly). + const { webpack } = compiler; + + // Compilation object gives us reference to some useful constants. + const { Compilation } = webpack; + + // RawSource is one of the "sources" classes that should be used + // to represent asset sources in compilation. + const { RawSource } = webpack.sources; + + // Tapping to the "thisCompilation" hook in order to further tap + // to the compilation process on an earlier stage. + compiler.hooks.thisCompilation.tap(pluginName, compilation => { + // Tapping to the assets processing pipeline on a specific stage. + compilation.hooks.processAssets.tap( + { + name: pluginName, + + // Using one of the later asset processing stages to ensure + // that all assets were already added to the compilation by other plugins. + stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE, + }, + assets => { + // "assets" is an object that contains all assets + // in the compilation, the keys of the object are pathnames of the assets + // and the values are file sources. + + // Iterating over all the assets and + // generating content for our Markdown file. + const content = `# In this build:\n\n${Object.keys(assets) + .map(filename => `- ${filename}`) + .join('\n')}`; + + // Adding new asset to the compilation, so it would be automatically + // generated by the webpack in the output directory. + compilation.emitAsset( + this.options.outputFile, + new RawSource(content) + ); + } + ); + }); + } +} + +export default FileListPlugin; +``` + +**webpack.config.js** + +```js +import FileListPlugin from './file-list-plugin.js'; + +// Use the plugin in your webpack configuration: +export default { + // … + + plugins: [ + // Adding the plugin with the default options + new FileListPlugin(), + + // OR: + + // You can choose to pass any supported options to it: + new FileListPlugin({ + outputFile: 'my-assets.md', + }), + ], +}; +``` + +This will generate a markdown file with chosen name that looks like this: + +```markdown +# In this build: + +- main.css +- main.js +- index.html +``` + +> [!TIP] +> We are using synchronous `tap()` method to tap into the `processAssets` hook because we don't need to perform any asynchronous operations in the example above. However, the `processAssets` hook is an asynchronous one, so you can also use `tapPromise()` or `tapAsync()` if you actually need to. + +> [!TIP] +> The `processAssets` hook on [`Compilation`](/docs/api/v5.x/compilation/Compilation) also supports the `additionalAssets` property. This lets a plugin intercept assets added both before and after its configured stage. In this example, the `SUMMARIZE` stage is sufficient to capture assets generated during earlier stages. + +## Watching for file changes + +When webpack runs in watch mode (`webpack --watch` or `webpack serve`), +it creates a new compilation for each rebuild triggered by file changes. + +The `compiler.modifiedFiles` Set lets your plugin know **which specific +files triggered the rebuild**, so you can skip expensive work for unrelated files. + +This is useful for plugins that need to react only to specific file changes +(e.g., regenerating assets, reprocessing templates, or invalidating caches). + +```js +class WatchNotifierPlugin { + apply(compiler) { + compiler.hooks.watchRun.tap('WatchNotifierPlugin', compiler => { + if (compiler.modifiedFiles) { + const changedFiles = [...compiler.modifiedFiles] + .map(file => ` • ${file}`) + .join('\n'); + + console.log(`\nFiles changed:\n${changedFiles}`); + } + }); + } +} + +export default WatchNotifierPlugin; +``` + +> **Note**: +> +> - `compiler.modifiedFiles` is a `Set`, not an array +> - It is `undefined` on the first (cold) build +> - It is only populated during watch rebuilds + +### Adding custom file dependencies + +If your plugin reads external files (config files, templates, etc.) +that webpack does not track by default, you must tell webpack to watch them. + +You can tell webpack to watch different types of dependencies: + +- `compilation.fileDependencies` is used to track individual files that your plugin depends on, so webpack can trigger a rebuild when those files change + +- `compilation.contextDependencies` is used to watch directories, so any change inside them triggers a rebuild + +- `compilation.missingDependencies` is used to track files that are currently missing, so webpack can trigger a rebuild when they are created + +```js +import path from 'node:path'; + +class TemplateWatchPlugin { + apply(compiler) { + compiler.hooks.compilation.tap('TemplateWatchPlugin', compilation => { + const templatePath = path.resolve(__dirname, 'my-template.html'); + + // Ensure webpack watches this file + compilation.fileDependencies.add(templatePath); + + // Watch a directory (context dependency) + const templatesDir = path.resolve(__dirname, 'templates'); + compilation.contextDependencies.add(templatesDir); + + // Example: mark a missing dependency + const missingFile = path.resolve(__dirname, 'missing-file.txt'); + compilation.missingDependencies.add(missingFile); + }); + } +} + +export default TemplateWatchPlugin; +``` + +Without calling `fileDependencies.add()`, webpack will not trigger +a rebuild when the file changes — even if your plugin depends on it. + +## Different Plugin Shapes + +A plugin can be classified into types based on the event hooks it taps into. Every event hook is pre-defined as synchronous or asynchronous or waterfall or parallel hook and hook is called internally using call/callAsync method. The list of hooks that are supported or can be tapped into is generally specified in `this.hooks` property. + +For example: + +```js +this.hooks = { + shouldEmit: new SyncBailHook(['compilation']), +}; +``` + +It represents that the only hook supported is `shouldEmit` which is a hook of `SyncBailHook` type and the only parameter which will be passed to any plugin that taps into `shouldEmit` hook is `compilation`. + +Various types of hooks supported are : + +### Synchronous Hooks + +- **SyncHook** + - Defined as `new SyncHook([params])` + - Tapped into using `tap` method. + - Called using `call(...params)` method. + +- **Bail Hooks** + - Defined using `SyncBailHook[params]` + - Tapped into using `tap` method. + - Called using `call(...params)` method. + + In these types of hooks, each of the plugin callbacks will be invoked one after the other with the specific `args`. If any value is returned except undefined by any plugin, then that value is returned by hook and no further plugin callback is invoked. Many useful events like `optimizeChunks`, `optimizeChunkModules` are SyncBailHooks. + +- **Waterfall Hooks** + - Defined using `SyncWaterfallHook[params]` + - Tapped into using `tap` method. + - Called using `call(...params)` method + + Here each of the plugins is called one after the other with the arguments from the return value of the previous plugin. The plugin must take the order of its execution into account. + It must accept arguments from the previous plugin that was executed. The value for the first plugin is `init`. Hence at least 1 param must be supplied for waterfall hooks. This pattern is used in the Tapable instances which are related to the webpack templates like `ModuleTemplate`, `ChunkTemplate` etc. + +### Asynchronous Hooks + +- **Async Series Hook** + - Defined using `AsyncSeriesHook[params]` + - Tapped into using `tap`/`tapAsync`/`tapPromise` method. + - Called using `callAsync(...params)` method + + The plugin handler functions are called with all arguments and a callback function with the signature `(err?: Error) -> void`. The handler functions are called in order of registration. `callback` is called after all the handlers are called. + This is also a commonly used pattern for events like `emit`, `run`. + +- **Async waterfall** The plugins will be applied asynchronously in the waterfall manner. + - Defined using `AsyncWaterfallHook[params]` + - Tapped into using `tap`/`tapAsync`/`tapPromise` method. + - Called using `callAsync(...params)` method + + The plugin handler functions are called with the current value and a callback function with the signature `(err: Error, nextValue: any) -> void.` When called `nextValue` is the current value for the next handler. The current value for the first handler is `init`. After all handlers are applied, callback is called with the last value. If any handler passes a value for `err`, the callback is called with this error and no more handlers are called. + This plugin pattern is expected for events like `before-resolve` and `after-resolve`. + +- **Async Series Bail** + - Defined using `AsyncSeriesBailHook[params]` + - Tapped into using `tap`/`tapAsync`/`tapPromise` method. + - Called using `callAsync(...params)` method + +- **Async Parallel** + - Defined using `AsyncParallelHook[params]` + - Tapped into using `tap`/`tapAsync`/`tapPromise` method. + - Called using `callAsync(...params)` method + +### Configuration defaults + +Webpack applies configuration defaults after plugins defaults are applied. This allows plugins to feature their own defaults and provides a way to create configuration preset plugins. + +### Example: AssetLoggerPlugin + +The following example shows a minimal Webpack plugin that logs all generated assets using Webpack’s logger interface. + +```js +class AssetLoggerPlugin { + apply(compiler) { + compiler.hooks.thisCompilation.tap('AssetLoggerPlugin', compilation => { + const logger = compilation.getLogger('AssetLoggerPlugin'); + compilation.hooks.processAssets.tap( + { + name: 'AssetLoggerPlugin', + stage: compilation.constructor.PROCESS_ASSETS_STAGE_SUMMARIZE, + }, + assets => { + logger.info('Generated assets:'); + for (const assetName of Object.keys(assets)) { + logger.info(assetName); + } + } + ); + }); + } +} + +export default AssetLoggerPlugin; +``` + +This plugin taps into the `emit` hook of the Webpack compiler and prints the names of all generated assets to the console. It demonstrates how plugins can interact with the compilation process using Webpack hooks. + +For example, when running a Webpack build, the output may look like: + +```text +Generated assets: +main.js +vendor.js +styles.css +``` diff --git a/pages/guides/index.md b/pages/guides/index.md index 4feaaa4e..e131a4b3 100644 --- a/pages/guides/index.md +++ b/pages/guides/index.md @@ -18,4 +18,4 @@ The guides increase in complexity as you progress. Each one serves as a starting - [Migration](/guides/migration) — Step-by-step guides for upgrading smoothly to the latest major releases > [!TIP] -> The output shown in these guides may differ slightly from what you see with newer versions of webpack. As long as your bundles look similar and run correctly, there's no cause for concern. If you find an example that seems broken by a newer release, please [open an issue](https://github.com/webpack/webpack-doc-kit/issues/new/choose) and we'll do our best to resolve the discrepancy. +> The output shown in these guides may differ slightly from what you see with newer versions of webpack. As long as your bundles look similar and run correctly, there's no cause for concern. If you find an example that seems broken by a newer release, please [open an issue](https://github.com/webpack/webpack.js.org/issues/new/choose) and we'll do our best to resolve the discrepancy. diff --git a/pages/guides/site.json b/pages/guides/site.json index c217ed35..33ad275e 100644 --- a/pages/guides/site.json +++ b/pages/guides/site.json @@ -204,6 +204,31 @@ { "link": "/guides/migration/to-v4", "label": "To v4 from v3" }, { "link": "/guides/migration/to-v5", "label": "To v5 from v4" } ] + }, + { + "groupName": "Contributing", + "items": [ + { + "link": "/guides/contributing/writers-guide", + "label": "Writer's Guide" + }, + { + "link": "/guides/contributing/writing-a-loader", + "label": "Writing a Loader" + }, + { + "link": "/guides/contributing/writing-a-plugin", + "label": "Writing a Plugin" + }, + { + "link": "/guides/contributing/plugin-patterns", + "label": "Plugin Patterns" + }, + { + "link": "/guides/contributing/debugging", + "label": "Debugging" + } + ] } ] } diff --git a/public/.well-known/atproto-did b/public/.well-known/atproto-did new file mode 100644 index 00000000..4dba04b3 --- /dev/null +++ b/public/.well-known/atproto-did @@ -0,0 +1 @@ +did:plc:xeltce4yywjuga4mc422pxvh diff --git a/public/.well-known/security.txt b/public/.well-known/security.txt new file mode 100644 index 00000000..1b1192f8 --- /dev/null +++ b/public/.well-known/security.txt @@ -0,0 +1,4 @@ +Contact: https://github.com/webpack/webpack/security/advisories/new +Expires: 2027-07-16T00:00:00.000Z +Preferred-Languages: en +Canonical: https://webpack.js.org/.well-known/security.txt diff --git a/assets/banners.json b/public/assets/banners.json similarity index 100% rename from assets/banners.json rename to public/assets/banners.json diff --git a/assets/blog/cover-1.svg b/public/assets/blog/cover-1.svg similarity index 100% rename from assets/blog/cover-1.svg rename to public/assets/blog/cover-1.svg diff --git a/assets/blog/cover-2.svg b/public/assets/blog/cover-2.svg similarity index 100% rename from assets/blog/cover-2.svg rename to public/assets/blog/cover-2.svg diff --git a/assets/blog/cover-3.svg b/public/assets/blog/cover-3.svg similarity index 100% rename from assets/blog/cover-3.svg rename to public/assets/blog/cover-3.svg diff --git a/assets/blog/cover-4.svg b/public/assets/blog/cover-4.svg similarity index 100% rename from assets/blog/cover-4.svg rename to public/assets/blog/cover-4.svg diff --git a/assets/favicon.ico b/public/assets/favicon.ico similarity index 100% rename from assets/favicon.ico rename to public/assets/favicon.ico diff --git a/assets/og_preview.png b/public/assets/og_preview.png similarity index 100% rename from assets/og_preview.png rename to public/assets/og_preview.png diff --git a/public/googlef7893965f8fb4e95.html b/public/googlef7893965f8fb4e95.html new file mode 100644 index 00000000..b5eaddfb --- /dev/null +++ b/public/googlef7893965f8fb4e95.html @@ -0,0 +1 @@ +google-site-verification: googlef7893965f8fb4e95.html diff --git a/scripts/html/doc-kit.config.mjs b/scripts/html/doc-kit.config.mjs index 9a5cc457..7606bb74 100644 --- a/scripts/html/doc-kit.config.mjs +++ b/scripts/html/doc-kit.config.mjs @@ -1,4 +1,5 @@ import { join, dirname } from 'node:path'; +import { cp } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { major } from 'semver'; @@ -48,7 +49,7 @@ export default { remoteConfigUrl: '/assets/banners.json', title: VERSION ? `Webpack ${MAJOR_VERSION} Documentation` : 'Webpack', editURL: - 'https://github.com/webpack/webpack-doc-kit/blob/main/pages/{path}.md', + 'https://github.com/webpack/webpack.js.org/blob/main/pages/{path}.md', head: { meta: [ { @@ -104,5 +105,8 @@ export default { }, }, }, - pathsToCopy: ['assets'], }; + +if (!VERSION) { + cp('public', 'out', { recursive: true, force: true }); +} diff --git a/vercel.json b/vercel.json index 1eddcb5c..3a882144 100644 --- a/vercel.json +++ b/vercel.json @@ -11,6 +11,461 @@ "destination": "/about/branding", "permanent": true }, + { + "source": "/contribute", + "destination": "/guides/contributing", + "permanent": false + }, + { + "source": "/contribute/:path*", + "destination": "/guides/contributing/:path*", + "permanent": false + }, + { + "source": "/concepts", + "destination": "/guides/getting-started/concepts", + "permanent": false + }, + { + "source": "/concepts/:path*", + "destination": "/guides/getting-started/concepts/:path*", + "permanent": false + }, + { + "source": "/migrate", + "destination": "/guides/migration", + "permanent": false + }, + { + "source": "/migrate/3", + "destination": "/guides/migration/to-v3", + "permanent": false + }, + { + "source": "/migrate/4", + "destination": "/guides/migration/to-v4", + "permanent": false + }, + { + "source": "/migrate/5", + "destination": "/guides/migration/to-v5", + "permanent": false + }, + { + "source": "/guides/installation", + "destination": "/guides/getting-started/installing-webpack", + "permanent": false + }, + { + "source": "/guides/asset-management", + "destination": "/guides/core-workflows/asset-management", + "permanent": false + }, + { + "source": "/guides/asset-modules", + "destination": "/guides/core-workflows/asset-modules", + "permanent": false + }, + { + "source": "/guides/development", + "destination": "/guides/core-workflows/development", + "permanent": false + }, + { + "source": "/guides/development-vagrant", + "destination": "/guides/core-workflows/development/vagrant", + "permanent": false + }, + { + "source": "/guides/hot-module-replacement", + "destination": "/guides/core-workflows/development/hot-module-replacement", + "permanent": false + }, + { + "source": "/guides/environment-variables", + "destination": "/guides/core-workflows/environment-variables", + "permanent": false + }, + { + "source": "/guides/output-management", + "destination": "/guides/core-workflows/output-management", + "permanent": false + }, + { + "source": "/guides/production", + "destination": "/guides/core-workflows/production", + "permanent": false + }, + { + "source": "/guides/build-performance", + "destination": "/guides/optimization/build-performance", + "permanent": false + }, + { + "source": "/guides/caching", + "destination": "/guides/optimization/caching", + "permanent": false + }, + { + "source": "/guides/code-splitting", + "destination": "/guides/optimization/code-splitting", + "permanent": false + }, + { + "source": "/guides/lazy-loading", + "destination": "/guides/optimization/code-splitting/lazy-loading", + "permanent": false + }, + { + "source": "/guides/tree-shaking", + "destination": "/guides/optimization/tree-shaking", + "permanent": false + }, + { + "source": "/guides/modern-web-platform", + "destination": "/guides/modern-web", + "permanent": false + }, + { + "source": "/guides/native-css", + "destination": "/guides/modern-web/native-css", + "permanent": false + }, + { + "source": "/guides/progressive-web-application", + "destination": "/guides/modern-web/progressive-web-application", + "permanent": false + }, + { + "source": "/guides/web-workers", + "destination": "/guides/modern-web/web-workers", + "permanent": false + }, + { + "source": "/guides/dependency-management", + "destination": "/guides/modules-and-dependencies/dependency-management", + "permanent": false + }, + { + "source": "/guides/ecma-script-modules", + "destination": "/guides/modules-and-dependencies/ecma-script-modules", + "permanent": false + }, + { + "source": "/guides/entry-advanced", + "destination": "/guides/modules-and-dependencies/entry-advanced", + "permanent": false + }, + { + "source": "/guides/package-exports", + "destination": "/guides/modules-and-dependencies/package-exports", + "permanent": false + }, + { + "source": "/guides/shimming", + "destination": "/guides/modules-and-dependencies/shimming", + "permanent": false + }, + { + "source": "/guides/author-libraries", + "destination": "/guides/tooling-and-integration/authoring-libraries", + "permanent": false + }, + { + "source": "/guides/csp", + "destination": "/guides/tooling-and-integration/content-security-policy", + "permanent": false + }, + { + "source": "/guides/integrations", + "destination": "/guides/tooling-and-integration/integrations", + "permanent": false + }, + { + "source": "/guides/typescript", + "destination": "/guides/tooling-and-integration/typescript", + "permanent": false + }, + { + "source": "/guides/public-path", + "destination": "/docs/api/v5.x/options#outputpublicpath", + "permanent": false + }, + { + "source": "/loaders", + "destination": "/docs/loaders", + "permanent": false + }, + { + "source": "/loaders/:path*", + "destination": "/docs/loaders/:path*", + "permanent": false + }, + { + "source": "/configuration", + "destination": "/docs/api/v5.x/options", + "permanent": false + }, + { + "source": "/configuration/:path*", + "destination": "/docs/api/v5.x/options", + "permanent": false + }, + { + "source": "/api", + "destination": "/docs/api/v5.x", + "permanent": false + }, + { + "source": "/api/cli", + "destination": "/docs/api/v5.x/cli", + "permanent": false + }, + { + "source": "/api/loaders", + "destination": "/docs/api/v5.x/loaders/types", + "permanent": false + }, + { + "source": "/api/node", + "destination": "/docs/api/v5.x/compilation/Compiler", + "permanent": false + }, + { + "source": "/api/stats", + "destination": "/docs/api/v5.x/stats/Stats", + "permanent": false + }, + { + "source": "/api/compiler-hooks", + "destination": "/docs/api/v5.x/compilation/Compiler", + "permanent": false + }, + { + "source": "/api/compilation-hooks", + "destination": "/docs/api/v5.x/compilation/Compilation", + "permanent": false + }, + { + "source": "/api/compilation-object", + "destination": "/docs/api/v5.x/compilation/Compilation", + "permanent": false + }, + { + "source": "/api/parser", + "destination": "/docs/api/v5.x/javascript/JavascriptParser", + "permanent": false + }, + { + "source": "/api/resolvers", + "destination": "/docs/api/v5.x/resolvers/Resolver", + "permanent": false + }, + { + "source": "/api/:path*", + "destination": "/docs/api/v5.x", + "permanent": false + }, + { + "source": "/plugins/automatic-prefetch-plugin", + "destination": "/docs/api/v5.x/plugins/AutomaticPrefetchPlugin", + "permanent": false + }, + { + "source": "/plugins/banner-plugin", + "destination": "/docs/api/v5.x/plugins/BannerPlugin", + "permanent": false + }, + { + "source": "/plugins/context-exclusion-plugin", + "destination": "/docs/api/v5.x/plugins/ContextExclusionPlugin", + "permanent": false + }, + { + "source": "/plugins/context-replacement-plugin", + "destination": "/docs/api/v5.x/plugins/ContextReplacementPlugin", + "permanent": false + }, + { + "source": "/plugins/define-plugin", + "destination": "/docs/api/v5.x/plugins/DefinePlugin", + "permanent": false + }, + { + "source": "/plugins/dll-plugin", + "destination": "/docs/api/v5.x/plugins/DllPlugin", + "permanent": false + }, + { + "source": "/plugins/environment-plugin", + "destination": "/docs/api/v5.x/plugins/EnvironmentPlugin", + "permanent": false + }, + { + "source": "/plugins/eval-source-map-dev-tool-plugin", + "destination": "/docs/api/v5.x/plugins/EvalSourceMapDevToolPlugin", + "permanent": false + }, + { + "source": "/plugins/hashed-module-ids-plugin", + "destination": "/docs/api/v5.x/ids/HashedModuleIdsPlugin", + "permanent": false + }, + { + "source": "/plugins/hot-module-replacement-plugin", + "destination": "/docs/api/v5.x/plugins/HotModuleReplacementPlugin", + "permanent": false + }, + { + "source": "/plugins/ignore-plugin", + "destination": "/docs/api/v5.x/plugins/IgnorePlugin", + "permanent": false + }, + { + "source": "/plugins/limit-chunk-count-plugin", + "destination": "/docs/api/v5.x/optimize/LimitChunkCountPlugin", + "permanent": false + }, + { + "source": "/plugins/manifest-plugin", + "destination": "/docs/api/v5.x/plugins/ManifestPlugin", + "permanent": false + }, + { + "source": "/plugins/merge-duplicate-chunks-plugin", + "destination": "/docs/api/v5.x/optimize/MergeDuplicateChunksPlugin", + "permanent": false + }, + { + "source": "/plugins/min-chunk-size-plugin", + "destination": "/docs/api/v5.x/optimize/MinChunkSizePlugin", + "permanent": false + }, + { + "source": "/plugins/module-concatenation-plugin", + "destination": "/docs/api/v5.x/optimize/ModuleConcatenationPlugin", + "permanent": false + }, + { + "source": "/plugins/module-federation-plugin", + "destination": "/docs/api/v5.x/container/ModuleFederationPlugin", + "permanent": false + }, + { + "source": "/plugins/normal-module-replacement-plugin", + "destination": "/docs/api/v5.x/plugins/NormalModuleReplacementPlugin", + "permanent": false + }, + { + "source": "/plugins/no-emit-on-errors-plugin", + "destination": "/docs/api/v5.x/plugins/NoEmitOnErrorsPlugin", + "permanent": false + }, + { + "source": "/plugins/prefetch-plugin", + "destination": "/docs/api/v5.x/plugins/PrefetchPlugin", + "permanent": false + }, + { + "source": "/plugins/profiling-plugin", + "destination": "/docs/api/v5.x/debug/ProfilingPlugin", + "permanent": false + }, + { + "source": "/plugins/progress-plugin", + "destination": "/docs/api/v5.x/plugins/ProgressPlugin", + "permanent": false + }, + { + "source": "/plugins/provide-plugin", + "destination": "/docs/api/v5.x/plugins/ProvidePlugin", + "permanent": false + }, + { + "source": "/plugins/source-map-dev-tool-plugin", + "destination": "/docs/api/v5.x/plugins/SourceMapDevToolPlugin", + "permanent": false + }, + { + "source": "/plugins/split-chunks-plugin", + "destination": "/docs/api/v5.x/optimize/SplitChunksPlugin", + "permanent": false + }, + { + "source": "/plugins/virtual-url-plugin", + "destination": "/docs/api/v5.x/experiments/schemes/VirtualUrlPlugin", + "permanent": false + }, + { + "source": "/plugins/watch-ignore-plugin", + "destination": "/docs/api/v5.x/plugins/WatchIgnorePlugin", + "permanent": false + }, + { + "source": "/plugins", + "destination": "/docs/api/v5.x", + "permanent": false + }, + { + "source": "/plugins/:path*", + "destination": "/docs/api/v5.x", + "permanent": false + }, + { + "source": "/blog/2020-10-10-webpack-5-release", + "destination": "/blog/posts/2020-10-10-webpack-5-release", + "permanent": false + }, + { + "source": "/blog/2020-12-08-roadmap-2021", + "destination": "/blog/posts/2020-12-08-roadmap-2021", + "permanent": false + }, + { + "source": "/blog/2026-02-03-webpack-5-105", + "destination": "/blog/posts/2026-02-03-webpack-5-105", + "permanent": false + }, + { + "source": "/blog/2026-02-04-roadmap-2026", + "destination": "/blog/posts/2026-02-04-roadmap-2026", + "permanent": false + }, + { + "source": "/blog/2026-04-08-webpack-5-106", + "destination": "/blog/posts/2026-04-08-webpack-5-106", + "permanent": false + }, + { + "source": "/blog/2026-05-19-webpack-5-107", + "destination": "/blog/posts/2026-05-19-webpack-5-107", + "permanent": false + }, + { + "source": "/blog/2026-06-25-webpack-5-108", + "destination": "/blog", + "permanent": false + }, + { + "source": "/awesome-webpack", + "destination": "/docs", + "permanent": false + }, + { + "source": "/comparison", + "destination": "/guides/getting-started/concepts/why-webpack", + "permanent": false + }, + { + "source": "/glossary", + "destination": "/guides/getting-started/concepts", + "permanent": false + }, + { + "source": "/license", + "destination": "https://github.com/webpack/webpack/blob/main/LICENSE", + "permanent": false + }, { "source": "/docs/api/:path((?!v\\d+\\.x).*)", "destination": "/docs/api/v5.x/:path",