From 9eb1cfa702eadc6fd9d4e09d604c730c3402e339 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 23 Jul 2026 16:59:12 -0700 Subject: [PATCH 1/5] fix(homepage): align code boxes --- pages/index.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pages/index.md b/pages/index.md index 191eb0fb..05c174aa 100644 --- a/pages/index.md +++ b/pages/index.md @@ -31,7 +31,7 @@ deno add npm:webpack npm:webpack-cli ```javascript displayName="webpack.config.js" -const path = require('path'); +const path = require('node:path'); module.exports = { entry: './src/index.js', @@ -41,10 +41,11 @@ module.exports = { }, mode: 'production', }; + ``` ```javascript displayName="webpack.config.mjs" -import path from 'path'; +import path from 'node:path'; export default { entry: './src/index.js', @@ -54,22 +55,21 @@ export default { }, mode: 'production', }; + ``` ```typescript displayName="webpack.config.ts" -import path from 'path'; +import path from 'node:path'; import { Configuration } from 'webpack'; -const config: Configuration = { - entry: './src/index.ts', +export default { + entry: './src/index.js', output: { filename: 'bundle.js', - path: path.resolve(__dirname, 'dist'), + path: path.resolve(import.meta.dirname, 'dist'), }, mode: 'production', -}; - -export default config; +} satisfies Configuration; ``` From bd41b4a81dea7f98d92658ab9b8051dc0d0fcc8e Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 23 Jul 2026 17:00:25 -0700 Subject: [PATCH 2/5] fixup! --- pages/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pages/index.md b/pages/index.md index 05c174aa..db6bd876 100644 --- a/pages/index.md +++ b/pages/index.md @@ -34,7 +34,7 @@ deno add npm:webpack npm:webpack-cli const path = require('node:path'); module.exports = { - entry: './src/index.js', + entry: './src/index.cjs', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), @@ -48,7 +48,7 @@ module.exports = { import path from 'node:path'; export default { - entry: './src/index.js', + entry: './src/index.mjs', output: { filename: 'bundle.js', path: path.resolve(import.meta.dirname, 'dist'), @@ -63,7 +63,7 @@ import path from 'node:path'; import { Configuration } from 'webpack'; export default { - entry: './src/index.js', + entry: './src/index.ts', output: { filename: 'bundle.js', path: path.resolve(import.meta.dirname, 'dist'), From d38641ee2df613994434e528300c1ec59bca7186 Mon Sep 17 00:00:00 2001 From: Aviv Keller Date: Thu, 23 Jul 2026 17:07:55 -0700 Subject: [PATCH 3/5] Update index.md --- pages/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/index.md b/pages/index.md index db6bd876..4f0ee65d 100644 --- a/pages/index.md +++ b/pages/index.md @@ -33,6 +33,7 @@ deno add npm:webpack npm:webpack-cli ```javascript displayName="webpack.config.js" const path = require('node:path'); + module.exports = { entry: './src/index.cjs', output: { @@ -41,12 +42,12 @@ module.exports = { }, mode: 'production', }; - ``` ```javascript displayName="webpack.config.mjs" import path from 'node:path'; + export default { entry: './src/index.mjs', output: { @@ -55,7 +56,6 @@ export default { }, mode: 'production', }; - ``` ```typescript displayName="webpack.config.ts" From fdd1fc914488aac82db7c62c6326146ed70c7db7 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Thu, 23 Jul 2026 22:57:23 -0700 Subject: [PATCH 4/5] fixup! --- pages/index.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/pages/index.md b/pages/index.md index 4f0ee65d..734b673e 100644 --- a/pages/index.md +++ b/pages/index.md @@ -33,7 +33,6 @@ deno add npm:webpack npm:webpack-cli ```javascript displayName="webpack.config.js" const path = require('node:path'); - module.exports = { entry: './src/index.cjs', output: { @@ -47,7 +46,6 @@ module.exports = { ```javascript displayName="webpack.config.mjs" import path from 'node:path'; - export default { entry: './src/index.mjs', output: { From dd801c9e1b8f827a46c4feea97e8c7830e7a45a4 Mon Sep 17 00:00:00 2001 From: avivkeller Date: Fri, 24 Jul 2026 08:53:33 -0700 Subject: [PATCH 5/5] feat(release): auto-generate blog posts --- .github/workflows/release.yml | 4 +- .prettierignore | 3 +- package-lock.json | 64 ++++++++++++++++++ package.json | 1 + scripts/markdown/governance.mjs | 10 +-- scripts/markdown/readmes.mjs | 11 +-- scripts/release/create-post.mjs | 82 +++++++++++++++++++++++ scripts/release/template.md.hbs | 38 +++++++++++ scripts/{ => release}/update-versions.mjs | 0 scripts/utils/fetch.mjs | 24 +++++-- 10 files changed, 214 insertions(+), 23 deletions(-) create mode 100644 scripts/release/create-post.mjs create mode 100644 scripts/release/template.md.hbs rename scripts/{ => release}/update-versions.mjs (100%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da0e7d86..efc425e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,7 +36,9 @@ jobs: shell: bash run: | set -euo pipefail - node scripts/update-versions.mjs "$TAG" + node scripts/release/update-versions.mjs "$TAG" + node scripts/release/create-post.mjs "$TAG" + node --run format - name: Open pull request uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1 diff --git a/.prettierignore b/.prettierignore index c0258d5b..a8191ea4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,4 +8,5 @@ out /pages/docs/plugins/* !/pages/docs/plugins/index.md versions.json -/generated \ No newline at end of file +/generated +*.hbs \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4dd6c75f..1d9b1a68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "eslint": "^10.4.0", "feed": "^6.0.0", "globals": "^17.6.0", + "handlebars": "^4.7.9", "husky": "^9.1.7", "lint-staged": "^17.0.5", "prettier": "^3.8.3", @@ -5327,6 +5328,38 @@ "node": ">=6.0" } }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/handlebars/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -8271,6 +8304,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minimizer-webpack-plugin": { "version": "5.6.1", "resolved": "https://registry.npmjs.org/minimizer-webpack-plugin/-/minimizer-webpack-plugin-5.6.1.tgz", @@ -10949,6 +10992,20 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -11533,6 +11590,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-10.0.0.tgz", diff --git a/package.json b/package.json index 6b0e4313..dffc4270 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "eslint": "^10.4.0", "feed": "^6.0.0", "globals": "^17.6.0", + "handlebars": "^4.7.9", "husky": "^9.1.7", "lint-staged": "^17.0.5", "prettier": "^3.8.3", diff --git a/scripts/markdown/governance.mjs b/scripts/markdown/governance.mjs index b47a6293..01f1bbee 100644 --- a/scripts/markdown/governance.mjs +++ b/scripts/markdown/governance.mjs @@ -1,14 +1,8 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { fetchWithRetry } from '../utils/fetch.mjs'; +import { fetchWithAuth } from '../utils/fetch.mjs'; import { rewriteRelativeLinks } from './sanitize.mjs'; -const { GH_TOKEN } = process.env; - -const BASE_HEADERS = { - ...(GH_TOKEN && { Authorization: `Bearer ${GH_TOKEN}` }), -}; - // Maps source filenames in webpack/governance repo to their output slug and sidebar label. // Insertion order determines sidebar order, this could be changed as per need. const FILE_MAP = { @@ -50,7 +44,7 @@ await mkdir(outputDir, { recursive: true }); const results = await Promise.all( Object.entries(FILE_MAP).map(async ([source, { output, label }]) => { const url = `https://raw.githubusercontent.com/webpack/governance/HEAD/${source}`; - const res = await fetchWithRetry(url, { headers: BASE_HEADERS }); + const res = await fetchWithAuth(url); if (!res.ok) { console.error(`Failed: ${source} -> ${res.status} ${res.statusText}`); diff --git a/scripts/markdown/readmes.mjs b/scripts/markdown/readmes.mjs index 8a29aebf..8508a80f 100644 --- a/scripts/markdown/readmes.mjs +++ b/scripts/markdown/readmes.mjs @@ -1,15 +1,8 @@ import { mkdir, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; -import { fetchWithRetry } from '../utils/fetch.mjs'; +import { fetchWithAuth, fetchWithRetry } from '../utils/fetch.mjs'; import cleanupMarkdown from './sanitize.mjs'; -const { GH_TOKEN } = process.env; - -const BASE_HEADERS = { - ...(GH_TOKEN && { Authorization: `Bearer ${GH_TOKEN}` }), - 'X-GitHub-Api-Version': '2022-11-28', -}; - const parseNextLink = linkHeader => linkHeader?.match(/<([^>]+)>;\s*rel="next"/)?.[1] ?? null; @@ -20,7 +13,7 @@ const discoverRepos = async () => { 'https://api.github.com/orgs/webpack/repos?per_page=100&type=public'; while (url) { - const res = await fetchWithRetry(url, { headers: BASE_HEADERS }); + const res = await fetchWithAuth(url); for (const repo of await res.json()) { if (repo.archived) continue; diff --git a/scripts/release/create-post.mjs b/scripts/release/create-post.mjs new file mode 100644 index 00000000..82a1df77 --- /dev/null +++ b/scripts/release/create-post.mjs @@ -0,0 +1,82 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { parseArgs } from 'node:util'; + +import Handlebars from 'handlebars'; +import matter from 'gray-matter'; + +import { fetchWithAuth } from '../../utils/fetch.mjs'; + +const { values } = parseArgs({ + options: { + // Author + author: { type: 'string', default: 'avivkeller' }, + }, +}); + +const API_BASE = 'https://api.github.com/repos/webpack/webpack'; +const TEMPLATE_PATH = new URL('./template.md.hbs', import.meta.url); + +const fetchJSON = url => fetchWithAuth(url).then(r => r.json()); + +const getLatestVersion = async () => { + const { tag_name } = await fetchJSON(`${API_BASE}/releases/latest`); + return tag_name; +}; + +const getRemovedChangesets = async version => { + const { files = [] } = await fetchJSON(`${API_BASE}/commits/${version}`); + + return files.filter( + file => + file.status === 'removed' && + file.filename.startsWith('.changeset/') && + file.patch + ); +}; + +const stripPatchMarkers = patch => patch.replace(/^@@.*\n|^[- ]/gm, ''); +const capitalize = value => value.charAt(0).toUpperCase() + value.slice(1); + +const parseChangeset = file => { + const { data, content } = matter(stripPatchMarkers(file.patch)); + const [title, ...paragraphs] = content.trim().split(/\n{2,}/); + + return { + notable: data.notable === true, + semver: capitalize(data.webpack), + title, + description: paragraphs.join('\n\n'), + }; +}; + +async function renderReleaseNotes(version, changes, date) { + Handlebars.registerHelper('filter', (items, property, expected) => + items.filter(item => item[property] === expected) + ); + + Handlebars.registerHelper('groupBy', (items, property) => + Object.groupBy(items, item => item[property]) + ); + + const source = await readFile(TEMPLATE_PATH, 'utf8'); + const render = Handlebars.compile(source, { noEscape: true }); + + return render({ + changes, + version, + date, + ...values, + }); +} + +const date = new Date().toISOString().slice(0, 10); +const version = await getLatestVersion(); +const changesets = await getRemovedChangesets(version); +const changes = changesets.map(parseChangeset); +const post = await renderReleaseNotes(version, changes, date); +writeFile( + new URL( + import.meta.resolve(`../../../pages/blog/${date}-webpack-${version}.md`) + ), + post +); diff --git a/scripts/release/template.md.hbs b/scripts/release/template.md.hbs new file mode 100644 index 00000000..cf77fea8 --- /dev/null +++ b/scripts/release/template.md.hbs @@ -0,0 +1,38 @@ +--- +date: '{{date}}' +category: release +title: 'webpack {{version}}' +layout: blog-post +author: '{{author}}' +--- + +# webpack {{version}} + + + +{{#with (filter changes "notable" true) as |notableChanges|}} +{{#if notableChanges.length}} +## Notable Changes + +{{#each notableChanges}} +### _({{semver}})_ {{title}} + +{{description}} + +{{/each}} +{{/if}} +{{/with}} +{{#with (filter changes "notable" false) as |otherChanges|}} +{{#if otherChanges.length}} +## Other Changes + +{{#each (groupBy otherChanges "semver")}} +### {{@key}} + +{{#each this}} +- {{title}} +{{/each}} + +{{/each}} +{{/if}} +{{/with}} \ No newline at end of file diff --git a/scripts/update-versions.mjs b/scripts/release/update-versions.mjs similarity index 100% rename from scripts/update-versions.mjs rename to scripts/release/update-versions.mjs diff --git a/scripts/utils/fetch.mjs b/scripts/utils/fetch.mjs index f18bd13c..cac9e373 100644 --- a/scripts/utils/fetch.mjs +++ b/scripts/utils/fetch.mjs @@ -1,10 +1,8 @@ -// fetch() wrapper that retries flakey responses like the GitHub and Open Collective -// occasionally 503 midbuild,so one blip doesn't fail the whole deploy. +import { setTimeout as sleep } from 'node:timers/promises'; +const { GH_TOKEN } = process.env; const RETRYABLE = new Set([429, 502, 503, 504]); -const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); - // Use the server's Retry After when it sends one, otherwise back off. const delayFor = (attempt, baseDelay, response) => { const retryAfter = Number(response?.headers.get('retry-after')); @@ -40,3 +38,21 @@ export const fetchWithRetry = async ( await sleep(delayFor(attempt, baseDelay, response)); } }; + +const githubHeaders = { + ...(GH_TOKEN && { Authorization: `Bearer ${GH_TOKEN}` }), + 'X-GitHub-Api-Version': '2022-11-28', +}; + +export const fetchWithAuth = (url, fetchOptions = {}, retryOptions) => + fetchWithRetry( + url, + { + ...fetchOptions, + headers: { + ...fetchOptions?.headers, + githubHeaders, + }, + }, + retryOptions + );