diff --git a/README.md b/README.md index 4109469a420..b5668edf6f2 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/heft-node-everything-test](./build-tests/heft-node-everything-test/) | Building this project tests every task and config file for Heft when targeting the Node.js runtime | | [/build-tests/heft-parameter-plugin](./build-tests/heft-parameter-plugin/) | This project contains a Heft plugin that adds a custom parameter to built-in actions | | [/build-tests/heft-parameter-plugin-test](./build-tests/heft-parameter-plugin-test/) | This project exercises a built-in Heft action with a custom parameter | +| [/build-tests/heft-rspack-everything-test](./build-tests/heft-rspack-everything-test/) | Building this project tests every task and config file for Heft when targeting the web browser runtime using Rspack | | [/build-tests/heft-sass-test](./build-tests/heft-sass-test/) | This project illustrates a minimal tutorial Heft project targeting the web browser runtime | | [/build-tests/heft-swc-test](./build-tests/heft-swc-test/) | Building this project tests building with SWC | | [/build-tests/heft-typescript-composite-test](./build-tests/heft-typescript-composite-test/) | Building this project tests behavior of Heft when the tsconfig.json file uses project references. | @@ -212,6 +213,7 @@ These GitHub repositories provide supplementary resources for Rush Stack: | [/build-tests/set-webpack-public-path-plugin-test](./build-tests/set-webpack-public-path-plugin-test/) | Building this project tests the set-webpack-public-path-plugin | | [/build-tests/webpack-local-version-test](./build-tests/webpack-local-version-test/) | Building this project tests the rig loading for the local version of webpack | | [/eslint/local-eslint-config](./eslint/local-eslint-config/) | An ESLint configuration consumed projects inside the rushstack repo. | +| [/heft-plugins/heft-rspack-plugin](./heft-plugins/heft-rspack-plugin/) | Heft plugin for Rspack | | [/libraries/rush-themed-ui](./libraries/rush-themed-ui/) | Rush Component Library: a set of themed components for rush projects | | [/libraries/rushell](./libraries/rushell/) | Execute shell commands using a consistent syntax on every platform | | [/repo-scripts/doc-plugin-rush-stack](./repo-scripts/doc-plugin-rush-stack/) | API Documenter plugin used with the rushstack.io website | diff --git a/build-tests-samples/heft-storybook-react-tutorial/webpack.config.js b/build-tests-samples/heft-storybook-react-tutorial/webpack.config.js index 74788cd1048..e509592fe18 100644 --- a/build-tests-samples/heft-storybook-react-tutorial/webpack.config.js +++ b/build-tests-samples/heft-storybook-react-tutorial/webpack.config.js @@ -13,7 +13,7 @@ function createWebpackConfig({ production }) { // Documentation: https://webpack.js.org/configuration/mode/ mode: production ? 'production' : 'development', resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.json'] }, module: { rules: [ diff --git a/build-tests-samples/heft-webpack-basic-tutorial/webpack.config.js b/build-tests-samples/heft-webpack-basic-tutorial/webpack.config.js index c6a4c30ea7d..683afc0cc96 100644 --- a/build-tests-samples/heft-webpack-basic-tutorial/webpack.config.js +++ b/build-tests-samples/heft-webpack-basic-tutorial/webpack.config.js @@ -12,7 +12,7 @@ function createWebpackConfig({ production }) { // Documentation: https://webpack.js.org/configuration/mode/ mode: production ? 'production' : 'development', resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.json'] }, module: { rules: [ diff --git a/build-tests/heft-rspack-everything-test/config/heft.json b/build-tests/heft-rspack-everything-test/config/heft.json new file mode 100644 index 00000000000..7d826b05556 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/config/heft.json @@ -0,0 +1,66 @@ +/** + * Defines configuration used by core Heft. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft.schema.json", + + // TODO: Add comments + "phasesByName": { + "build": { + "cleanFiles": [{ "includeGlobs": ["dist", "lib", "lib-commonjs"] }], + + "tasksByName": { + "typescript": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-typescript-plugin" + } + }, + "lint": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-lint-plugin" + } + }, + "rspack": { + "taskDependencies": ["typescript"], + "taskPlugin": { + "pluginPackage": "@rushstack/heft-rspack-plugin" + } + } + } + }, + + "test": { + "phaseDependencies": ["build"], + "tasksByName": { + "jest": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-jest-plugin" + } + } + } + }, + + "trust-dev-cert": { + "tasksByName": { + "trust-dev-cert": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-dev-cert-plugin", + "pluginName": "trust-dev-certificate-plugin" + } + } + } + }, + + "untrust-dev-cert": { + "tasksByName": { + "untrust-dev-cert": { + "taskPlugin": { + "pluginPackage": "@rushstack/heft-dev-cert-plugin", + "pluginName": "untrust-dev-certificate-plugin" + } + } + } + } + } +} diff --git a/build-tests/heft-rspack-everything-test/config/jest.config.json b/build-tests/heft-rspack-everything-test/config/jest.config.json new file mode 100644 index 00000000000..f22bb14d6d1 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/config/jest.config.json @@ -0,0 +1,12 @@ +{ + "extends": "@rushstack/heft-jest-plugin/includes/jest-web.config.json", + + // Enable code coverage for Jest + "collectCoverage": true, + "coverageDirectory": "/coverage", + "coverageReporters": ["cobertura", "html"], + + // Use v8 coverage provider to avoid Babel + "coverageProvider": "v8", + "resolver": "@rushstack/heft-jest-plugin/lib/exports/jest-node-modules-symlink-resolver" +} diff --git a/build-tests/heft-rspack-everything-test/config/rush-project.json b/build-tests/heft-rspack-everything-test/config/rush-project.json new file mode 100644 index 00000000000..030d8d0ff0e --- /dev/null +++ b/build-tests/heft-rspack-everything-test/config/rush-project.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rush/v5/rush-project.schema.json", + + "operationSettings": [ + { + "operationName": "_phase:build", + "outputFolderNames": ["lib", "dist"] + }, + { + "operationName": "_phase:test", + "outputFolderNames": ["coverage"] + } + ] +} diff --git a/build-tests/heft-rspack-everything-test/config/typescript.json b/build-tests/heft-rspack-everything-test/config/typescript.json new file mode 100644 index 00000000000..86a32ee3552 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/config/typescript.json @@ -0,0 +1,55 @@ +/** + * Configures the TypeScript plugin for Heft. This plugin also manages linting. + */ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/typescript.schema.json", + + /** + * If provided, emit these module kinds in addition to the modules specified in the tsconfig. + * Note that this option only applies to the main tsconfig.json configuration. + */ + "additionalModuleKindsToEmit": [ + // { + // /** + // * (Required) Must be one of "commonjs", "amd", "umd", "system", "es2015", "esnext" + // */ + // "moduleKind": "amd", + // + // /** + // * (Required) The name of the folder where the output will be written. + // */ + // "outFolderName": "lib-amd" + // } + { + "moduleKind": "commonjs", + "outFolderName": "lib-commonjs" + } + ], + + /** + * Describes the way files should be statically coped from src to TS output folders + */ + "staticAssetsToCopy": { + /** + * File extensions that should be copied from the src folder to the destination folder(s). + */ + "fileExtensions": [".css", ".png"] + + /** + * Glob patterns that should be explicitly included. + */ + // "includeGlobs": [ + // "some/path/*.js" + // ], + + /** + * Glob patterns that should be explicitly excluded. This takes precedence over globs listed + * in "includeGlobs" and files that match the file extensions provided in "fileExtensions". + */ + // "excludeGlobs": [ + // "some/path/*.css" + // ] + }, + + "onlyResolveSymlinksInNodeModules": true +} diff --git a/build-tests/heft-rspack-everything-test/eslint.config.js b/build-tests/heft-rspack-everything-test/eslint.config.js new file mode 100644 index 00000000000..5a9df48909b --- /dev/null +++ b/build-tests/heft-rspack-everything-test/eslint.config.js @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const webAppProfile = require('local-eslint-config/flat/profile/web-app'); + +module.exports = [ + ...webAppProfile, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/build-tests/heft-rspack-everything-test/package.json b/build-tests/heft-rspack-everything-test/package.json new file mode 100644 index 00000000000..0b83d231efa --- /dev/null +++ b/build-tests/heft-rspack-everything-test/package.json @@ -0,0 +1,29 @@ +{ + "name": "heft-rspack-everything-test", + "description": "Building this project tests every task and config file for Heft when targeting the web browser runtime using Rspack", + "version": "1.0.0", + "private": true, + "scripts": { + "build": "heft build --clean", + "start": "heft build-watch", + "serve": "heft build-watch --serve", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" + }, + "devDependencies": { + "@rushstack/heft-dev-cert-plugin": "workspace:*", + "@rushstack/heft-jest-plugin": "workspace:*", + "@rushstack/heft-lint-plugin": "workspace:*", + "@rushstack/heft-typescript-plugin": "workspace:*", + "@rushstack/heft-rspack-plugin": "workspace:*", + "@rushstack/heft": "workspace:*", + "@rushstack/node-core-library": "workspace:*", + "@rushstack/rush-sdk": "workspace:*", + "@types/heft-jest": "1.0.1", + "@types/node": "20.17.19", + "eslint": "~9.37.0", + "local-eslint-config": "workspace:*", + "typescript": "~5.8.2", + "@rspack/core": "~1.6.0-beta.0" + } +} \ No newline at end of file diff --git a/build-tests/heft-rspack-everything-test/rspack.config.mjs b/build-tests/heft-rspack-everything-test/rspack.config.mjs new file mode 100644 index 00000000000..fa68d03598f --- /dev/null +++ b/build-tests/heft-rspack-everything-test/rspack.config.mjs @@ -0,0 +1,51 @@ +// @ts-check +/** @typedef {import('@rushstack/heft-rspack-plugin').IRspackConfiguration} IRspackConfiguration */ +'use strict'; + +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { HtmlRspackPlugin, SwcJsMinimizerRspackPlugin } from '@rspack/core'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** @type {IRspackConfiguration} */ +const config = { + mode: 'production', + module: { + rules: [ + { + test: /\.png$/i, + type: 'asset/resource' + }, + { + test: /\.js$/, + enforce: 'pre' + // TODO: enable after rspack drops a new version with this commit https://github.com/web-infra-dev/rspack/commit/d31f2fa07179d72eee99b21db517946d08073767 + // extractSourceMap: true + } + ] + }, + target: ['web', 'es2020'], + resolve: { + extensions: ['.js', '.json'] + }, + entry: { + 'heft-test-A': resolve(__dirname, 'lib', 'indexA.js'), + 'heft-test-B': resolve(__dirname, 'lib', 'indexB.js') + }, + output: { + path: resolve(__dirname, 'dist'), + filename: '[name]_[contenthash].js', + chunkFilename: '[id].[name]_[contenthash].js', + assetModuleFilename: '[name]_[contenthash][ext][query]' + }, + devtool: 'source-map', + optimization: { + minimize: true, + minimizer: [new SwcJsMinimizerRspackPlugin({})] + }, + plugins: [new HtmlRspackPlugin()] +}; + +export default config; diff --git a/build-tests/heft-rspack-everything-test/rspack.dev.config.mjs b/build-tests/heft-rspack-everything-test/rspack.dev.config.mjs new file mode 100644 index 00000000000..3e0315a615d --- /dev/null +++ b/build-tests/heft-rspack-everything-test/rspack.dev.config.mjs @@ -0,0 +1,45 @@ +// @ts-check +/** @typedef {import('@rushstack/heft-rspack-plugin').IRspackConfiguration} IRspackConfiguration */ +'use strict'; + +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { HtmlRspackPlugin } from '@rspack/core'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +/** @type {IRspackConfiguration} */ +const config = { + mode: 'none', + module: { + rules: [ + { + test: /\.png$/i, + type: 'asset/resource' + } + ] + }, + target: ['web', 'es2020'], + resolve: { + extensions: ['.js', '.json'] + }, + entry: { + 'heft-test-A': resolve(__dirname, 'lib', 'indexA.js'), + 'heft-test-B': resolve(__dirname, 'lib', 'indexB.js') + }, + output: { + path: resolve(__dirname, 'dist'), + filename: '[name]_[contenthash].js', + chunkFilename: '[id].[name]_[contenthash].js', + assetModuleFilename: '[name]_[contenthash][ext][query]' + }, + devtool: 'source-map', + optimization: { + minimize: false, + minimizer: [] + }, + plugins: [new HtmlRspackPlugin()] +}; + +export default config; diff --git a/build-tests/heft-rspack-everything-test/src/chunks/ChunkClass.ts b/build-tests/heft-rspack-everything-test/src/chunks/ChunkClass.ts new file mode 100644 index 00000000000..ddbf7d148c7 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/chunks/ChunkClass.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +export class ChunkClass { + public doStuff(): void { + // eslint-disable-next-line no-console + console.log('CHUNK'); + } + + public getImageUrl(): string { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require('./image.png'); + } +} diff --git a/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts b/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts new file mode 100644 index 00000000000..f38a285dfd9 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/chunks/image.d.png.ts @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +declare const path: string; + +export default path; diff --git a/build-tests/heft-rspack-everything-test/src/chunks/image.png b/build-tests/heft-rspack-everything-test/src/chunks/image.png new file mode 100644 index 00000000000..a028cfeb69f Binary files /dev/null and b/build-tests/heft-rspack-everything-test/src/chunks/image.png differ diff --git a/build-tests/heft-rspack-everything-test/src/copiedAsset.css b/build-tests/heft-rspack-everything-test/src/copiedAsset.css new file mode 100644 index 00000000000..e9747e441d3 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/copiedAsset.css @@ -0,0 +1 @@ +/* THIS FILE SHOULD GET COPIED TO THE "lib" FOLDER BECAUSE IT IS REFERENCED IN copy-static-assets.json */ diff --git a/build-tests/heft-rspack-everything-test/src/indexA.ts b/build-tests/heft-rspack-everything-test/src/indexA.ts new file mode 100644 index 00000000000..6b4db719843 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/indexA.ts @@ -0,0 +1,10 @@ +/* eslint-disable */ +/* tslint:disable */ +import(/* webpackChunkName: 'chunk' */ './chunks/ChunkClass') + .then(({ ChunkClass }) => { + const chunk: any = new ChunkClass(); + chunk.doStuff(); + }) + .catch((e) => { + console.log('Error: ' + e.message); + }); diff --git a/build-tests/heft-rspack-everything-test/src/indexB.ts b/build-tests/heft-rspack-everything-test/src/indexB.ts new file mode 100644 index 00000000000..2bcd3820a4b --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/indexB.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +// eslint-disable-next-line no-console +console.log('dostuff'); diff --git a/build-tests/heft-rspack-everything-test/src/test/ExampleTest.test.ts b/build-tests/heft-rspack-everything-test/src/test/ExampleTest.test.ts new file mode 100644 index 00000000000..565432eacf5 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/test/ExampleTest.test.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { ChunkClass } from '../chunks/ChunkClass'; + +describe('Example Test', () => { + it('Correctly tests stuff', () => { + expect(true).toBeTruthy(); + }); + + it('Correctly handles images', () => { + const chunkClass: ChunkClass = new ChunkClass(); + expect(() => chunkClass.getImageUrl()).not.toThrow(); + expect(typeof chunkClass.getImageUrl()).toBe('string'); + }); +}); diff --git a/build-tests/heft-rspack-everything-test/src/test/Image.test.ts b/build-tests/heft-rspack-everything-test/src/test/Image.test.ts new file mode 100644 index 00000000000..c336d269d60 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/test/Image.test.ts @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import image from '../chunks/image.png'; + +describe('Image Test', () => { + it('correctly handles urls for images', () => { + expect(image).toBe('lib-commonjs/chunks/image.png'); + }); +}); diff --git a/build-tests/heft-rspack-everything-test/src/test/SourceMapTest.test.ts b/build-tests/heft-rspack-everything-test/src/test/SourceMapTest.test.ts new file mode 100644 index 00000000000..2c500cdd547 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/test/SourceMapTest.test.ts @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import { FileSystem, PackageJsonLookup } from '@rushstack/node-core-library'; + +interface IMap { + sources?: string[]; + file?: string; + sourcesContent?: string[]; + names?: string[]; +} + +interface IMapValue { + mapFileName: string; + mapObject: IMap; +} + +interface IMapTestEntry { + name: string; + mapRegex: RegExp; + map: IMapValue | undefined; +} + +const mapTests: IMapTestEntry[] = [ + { + name: 'Test-A', + mapRegex: /^heft-test-A_[\w\d]*\.js.map$/, + map: undefined + }, + { + name: 'Test-B', + mapRegex: /^heft-test-B_[\w\d]*\.js.map$/, + map: undefined + }, + { + name: 'Chunk', + mapRegex: /^[\w\d\.]*chunk_[\w\d]*\.js.map$/, + map: undefined + } +]; + +const lookup: PackageJsonLookup = new PackageJsonLookup(); +lookup.tryGetPackageFolderFor(__dirname); +const thisProjectFolder: string | undefined = lookup.tryGetPackageFolderFor(__dirname); +if (!thisProjectFolder) { + throw new Error('Cannot find project folder'); +} +const distEntries: string[] = FileSystem.readFolderItemNames(thisProjectFolder + '/dist'); +for (const distEntry of distEntries) { + for (const test of mapTests) { + if (test.mapRegex.test(distEntry)) { + const mapText: string = FileSystem.readFile(`${thisProjectFolder}/dist/${distEntry}`); + const mapObject: IMap = JSON.parse(mapText); + test.map = { + mapFileName: distEntry, + mapObject + }; + } + } +} + +describe('Source Maps', () => { + for (const test of mapTests) { + mapValueCheck(test); + } +}); + +function mapValueCheck(entry: IMapTestEntry): void { + it(`${entry.name} has map value`, () => { + expect(entry.map).toBeTruthy(); + }); + + if (!entry.map) { + return; + } + + const map: IMapValue = entry.map; + + it(`${entry.name} has filename matching file attribute`, () => { + if (map.mapObject.file) { + expect(map.mapFileName).toMatch(`${map.mapObject.file}.map`); + } + }); + + const properties: (keyof IMap)[] = ['sources', 'file', 'sourcesContent', 'names']; + for (const property of properties) { + it(`${map.mapFileName} has ${property} property`, () => { + expect(map.mapObject[property]).toBeTruthy(); + }); + } + + it(`${entry.name} has sources and sourcesContent arrays of the same length`, () => { + if (map.mapObject.sourcesContent && map.mapObject.sources) { + let numSrcs: number = 0; + for (const source of map.mapObject.sources) { + if (source) { + numSrcs++; + } + } + + let numContents: number = 0; + for (const content of map.mapObject.sourcesContent) { + if (content) { + numContents++; + } + } + expect(numSrcs).toEqual(numContents); + } + }); + + it(`${entry.name} has a source that matches the sourceFileRegex`, () => { + if (map.mapObject.sources) { + expect(map.mapObject.sources).toMatchSnapshot(); + } + }); +} diff --git a/build-tests/heft-rspack-everything-test/src/test/__snapshots__/SourceMapTest.test.ts.snap b/build-tests/heft-rspack-everything-test/src/test/__snapshots__/SourceMapTest.test.ts.snap new file mode 100644 index 00000000000..23c890d36c4 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/src/test/__snapshots__/SourceMapTest.test.ts.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Source Maps Chunk has a source that matches the sourceFileRegex 1`] = ` +Array [ + "webpack://heft-rspack-everything-test/./lib/chunks/ChunkClass.js", +] +`; + +exports[`Source Maps Test-A has a source that matches the sourceFileRegex 1`] = ` +Array [ + "webpack://heft-rspack-everything-test/webpack/runtime/jsonp_chunk_loading", + "webpack://heft-rspack-everything-test/webpack/runtime/define_property_getters", + "webpack://heft-rspack-everything-test/webpack/runtime/ensure_chunk", + "webpack://heft-rspack-everything-test/webpack/runtime/get javascript chunk filename", + "webpack://heft-rspack-everything-test/webpack/runtime/global", + "webpack://heft-rspack-everything-test/webpack/runtime/has_own_property", + "webpack://heft-rspack-everything-test/webpack/runtime/load_script", + "webpack://heft-rspack-everything-test/webpack/runtime/rspack_version", + "webpack://heft-rspack-everything-test/webpack/runtime/auto_public_path", + "webpack://heft-rspack-everything-test/webpack/runtime/rspack_unique_id", + "webpack://heft-rspack-everything-test/./lib/indexA.js", +] +`; + +exports[`Source Maps Test-B has a source that matches the sourceFileRegex 1`] = ` +Array [ + "webpack://heft-rspack-everything-test/webpack/runtime/rspack_version", + "webpack://heft-rspack-everything-test/webpack/runtime/rspack_unique_id", + "webpack://heft-rspack-everything-test/./lib/indexB.js", +] +`; diff --git a/build-tests/heft-rspack-everything-test/tsconfig.json b/build-tests/heft-rspack-everything-test/tsconfig.json new file mode 100644 index 00000000000..1838fa44b78 --- /dev/null +++ b/build-tests/heft-rspack-everything-test/tsconfig.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json.schemastore.org/tsconfig", + + "compilerOptions": { + "outDir": "lib", + "rootDir": "src", + + "allowArbitraryExtensions": true, + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "jsx": "react", + "declaration": true, + "sourceMap": true, + "declarationMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "types": ["heft-jest", "node"], + + "module": "esnext", + "moduleResolution": "node", + "target": "es5", + "lib": ["es5", "scripthost", "es2015.collection", "es2015.promise", "es2015.iterable", "dom"] + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "lib"] +} diff --git a/build-tests/heft-webpack4-everything-test/webpack.config.js b/build-tests/heft-webpack4-everything-test/webpack.config.js index a6aa6151bdb..c4be9e959c4 100644 --- a/build-tests/heft-webpack4-everything-test/webpack.config.js +++ b/build-tests/heft-webpack4-everything-test/webpack.config.js @@ -24,7 +24,7 @@ module.exports = { ] }, resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.json'] }, entry: { 'heft-test-A': path.join(__dirname, 'lib', 'indexA.js'), diff --git a/build-tests/heft-webpack5-everything-test/webpack.config.js b/build-tests/heft-webpack5-everything-test/webpack.config.js index 68ab9d96538..25edbf94925 100644 --- a/build-tests/heft-webpack5-everything-test/webpack.config.js +++ b/build-tests/heft-webpack5-everything-test/webpack.config.js @@ -22,7 +22,7 @@ module.exports = { }, target: ['web', 'es2020'], resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.json'] }, entry: { 'heft-test-A': path.join(__dirname, 'lib', 'indexA.js'), diff --git a/build-tests/heft-webpack5-everything-test/webpack.dev.config.js b/build-tests/heft-webpack5-everything-test/webpack.dev.config.js index 25459e25909..504bdc6e13c 100644 --- a/build-tests/heft-webpack5-everything-test/webpack.dev.config.js +++ b/build-tests/heft-webpack5-everything-test/webpack.dev.config.js @@ -15,7 +15,7 @@ module.exports = { }, target: ['web', 'es2020'], resolve: { - extensions: ['.js', '.jsx', '.json'] + extensions: ['.js', '.json'] }, entry: { 'heft-test-A': path.join(__dirname, 'lib', 'indexA.js'), diff --git a/build-tests/localization-plugin-test-03/webpack.config.js b/build-tests/localization-plugin-test-03/webpack.config.js index 80b0f933a4f..56d3cf532e8 100644 --- a/build-tests/localization-plugin-test-03/webpack.config.js +++ b/build-tests/localization-plugin-test-03/webpack.config.js @@ -51,7 +51,7 @@ function generateConfiguration(mode, outputFolderName, webpack) { ] }, resolve: { - extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'] + extensions: ['.js', '.json', '.ts', '.tsx'] }, entry: { 'localization-test-A': `${__dirname}/src/indexA.ts`, diff --git a/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-04-34.json b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-04-34.json new file mode 100644 index 00000000000..e62ded6bf1b --- /dev/null +++ b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-04-34.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-webpack5-plugin", + "comment": "", + "type": "none" + } + ], + "packageName": "@rushstack/heft-webpack5-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-05-25.json b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-05-25.json new file mode 100644 index 00000000000..3e9d03da9d7 --- /dev/null +++ b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-07-05-25.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-webpack5-plugin", + "comment": "dev-server: add ipv6 loopback address support in terminal output", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-webpack5-plugin" +} \ No newline at end of file diff --git a/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-15-22-34.json b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-15-22-34.json new file mode 100644 index 00000000000..e78af2cd5ac --- /dev/null +++ b/common/changes/@rushstack/heft-webpack5-plugin/rspack-plugin_2025-10-15-22-34.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@rushstack/heft-webpack5-plugin", + "comment": "add debug log when Webpack is imported from the rig package", + "type": "patch" + } + ], + "packageName": "@rushstack/heft-webpack5-plugin" +} \ No newline at end of file diff --git a/common/config/rush/nonbrowser-approved-packages.json b/common/config/rush/nonbrowser-approved-packages.json index 164d3881b80..f4a71081795 100644 --- a/common/config/rush/nonbrowser-approved-packages.json +++ b/common/config/rush/nonbrowser-approved-packages.json @@ -122,6 +122,14 @@ "name": "@redis/client", "allowedCategories": [ "libraries" ] }, + { + "name": "@rspack/core", + "allowedCategories": [ "libraries", "tests" ] + }, + { + "name": "@rspack/dev-server", + "allowedCategories": [ "libraries" ] + }, { "name": "@rushstack/debug-certificate-manager", "allowedCategories": [ "libraries", "vscode-extensions" ] @@ -194,6 +202,10 @@ "name": "@rushstack/heft-node-rig", "allowedCategories": [ "libraries", "tests", "vscode-extensions" ] }, + { + "name": "@rushstack/heft-rspack-plugin", + "allowedCategories": [ "tests" ] + }, { "name": "@rushstack/heft-sass-load-themed-styles-plugin", "allowedCategories": [ "tests" ] diff --git a/common/config/subspaces/default/pnpm-lock.yaml b/common/config/subspaces/default/pnpm-lock.yaml index cc55254a6c1..3008c3b9ce2 100644 --- a/common/config/subspaces/default/pnpm-lock.yaml +++ b/common/config/subspaces/default/pnpm-lock.yaml @@ -1817,6 +1817,51 @@ importers: specifier: ~5.8.2 version: 5.8.2 + ../../../build-tests/heft-rspack-everything-test: + devDependencies: + '@rspack/core': + specifier: ~1.6.0-beta.0 + version: 1.6.0-beta.0 + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/heft-dev-cert-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-dev-cert-plugin + '@rushstack/heft-jest-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-jest-plugin + '@rushstack/heft-lint-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-lint-plugin + '@rushstack/heft-rspack-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-rspack-plugin + '@rushstack/heft-typescript-plugin': + specifier: workspace:* + version: link:../../heft-plugins/heft-typescript-plugin + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../../libraries/node-core-library + '@rushstack/rush-sdk': + specifier: workspace:* + version: link:../../libraries/rush-sdk + '@types/heft-jest': + specifier: 1.0.1 + version: 1.0.1 + '@types/node': + specifier: 20.17.19 + version: 20.17.19 + eslint: + specifier: ~9.37.0 + version: 9.37.0(supports-color@8.1.1) + local-eslint-config: + specifier: workspace:* + version: link:../../eslint/local-eslint-config + typescript: + specifier: ~5.8.2 + version: 5.8.2 + ../../../build-tests/heft-sass-test: dependencies: buttono: @@ -3019,6 +3064,40 @@ importers: specifier: workspace:* version: link:../../rigs/local-node-rig + ../../../heft-plugins/heft-rspack-plugin: + dependencies: + '@rspack/dev-server': + specifier: ^1.1.4 + version: 1.1.4(@rspack/core@1.6.0-beta.0)(webpack@5.98.0) + '@rushstack/debug-certificate-manager': + specifier: workspace:* + version: link:../../libraries/debug-certificate-manager + '@rushstack/node-core-library': + specifier: workspace:* + version: link:../../libraries/node-core-library + tapable: + specifier: 2.3.0 + version: 2.3.0 + webpack: + specifier: ~5.98.0 + version: 5.98.0 + devDependencies: + '@rspack/core': + specifier: ~1.6.0-beta.0 + version: 1.6.0-beta.0 + '@rushstack/heft': + specifier: workspace:* + version: link:../../apps/heft + '@rushstack/terminal': + specifier: workspace:* + version: link:../../libraries/terminal + eslint: + specifier: ~9.37.0 + version: 9.37.0(supports-color@8.1.1) + local-node-rig: + specifier: workspace:* + version: link:../../rigs/local-node-rig + ../../../heft-plugins/heft-sass-load-themed-styles-plugin: dependencies: '@microsoft/load-themed-styles': @@ -7639,6 +7718,28 @@ packages: engines: {node: '>=10.0.0'} dev: true + /@emnapi/core@1.5.0: + resolution: {integrity: sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==} + requiresBuild: true + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + /@emnapi/runtime@1.5.0: + resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + + /@emnapi/wasi-threads@1.1.0: + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + /@emotion/cache@10.0.29: resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==} dependencies: @@ -10416,6 +10517,37 @@ packages: - supports-color dev: false + /@module-federation/error-codes@0.20.0: + resolution: {integrity: sha512-pwKqIFXHG72AaXjtptZb+l5VOO3O7JQMVZ4txFhBH4H/BMu7o1LRBONllTisVmojLHOC/RQpBrxXSGrC64LC4w==} + + /@module-federation/runtime-core@0.20.0: + resolution: {integrity: sha512-M/0F/Ed6o1eCC5gKW3V3QtbxeNZ1w0Y7r6NKNacnwKKC12Nn7Ty9Rg1Kjw2B13EUqP8Qs2Y2IwmBEApy7cFLMw==} + dependencies: + '@module-federation/error-codes': 0.20.0 + '@module-federation/sdk': 0.20.0 + + /@module-federation/runtime-tools@0.20.0: + resolution: {integrity: sha512-5NimrYQyYr8hBl48YVU+w6bzl9uWDKNq3IEqYDgYljTYlupbVqsH2MJTf2A+c95nuCycjHS0vp5B3rnJ3Kdotg==} + dependencies: + '@module-federation/runtime': 0.20.0 + '@module-federation/webpack-bundler-runtime': 0.20.0 + + /@module-federation/runtime@0.20.0: + resolution: {integrity: sha512-9vHE27aLCWbvzUfYWCTCsNbx4IQ5MtK3f340s4swQofTKj0Qv5dJ6gRIwmHk3DqvH5/1FZoQi3FYMCmrThiGrg==} + dependencies: + '@module-federation/error-codes': 0.20.0 + '@module-federation/runtime-core': 0.20.0 + '@module-federation/sdk': 0.20.0 + + /@module-federation/sdk@0.20.0: + resolution: {integrity: sha512-bBFGA07PpfioJLY0DITVe+szGwLtFad+8R4rb5bPFKCZPZsKqLKwMB9tSsdHeieFPSc+1v20s6wq+R1DiWe56Q==} + + /@module-federation/webpack-bundler-runtime@0.20.0: + resolution: {integrity: sha512-TB0v5FRjfpL5fR8O5L4L3FTKJsb4EsflK8aNkdrJ46Tm/MR+PvL4SEx/AXpnsY+g/zkGRkiz10vwF0/RgMh6fQ==} + dependencies: + '@module-federation/runtime': 0.20.0 + '@module-federation/sdk': 0.20.0 + /@mrmlnc/readdir-enhanced@2.2.1: resolution: {integrity: sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==} engines: {node: '>=4'} @@ -10424,6 +10556,15 @@ packages: glob-to-regexp: 0.3.0 dev: true + /@napi-rs/wasm-runtime@1.0.7: + resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} + requiresBuild: true + dependencies: + '@emnapi/core': 1.5.0 + '@emnapi/runtime': 1.5.0 + '@tybys/wasm-util': 0.10.1 + optional: true + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -11147,6 +11288,130 @@ packages: engines: {node: '>=14.0.0'} dev: true + /@rspack/binding-darwin-arm64@1.6.0-beta.0: + resolution: {integrity: sha512-iRza6XmLT0obDwlaFvZ/LUJ3FqbcZnwA48C1bwCUlDnM+9uVFInCrcHkOjKuXnNcWBBJvNmLDWA5XYWzjtbXsw==} + cpu: [arm64] + os: [darwin] + requiresBuild: true + optional: true + + /@rspack/binding-darwin-x64@1.6.0-beta.0: + resolution: {integrity: sha512-4rHw6IHpWO04C22lEQK34vk6H+CtBkVQK9pEoA4gr+zMX8H+Bfl8liVQgeaAgwRqD7ipDo3jXGLaakVGktDexw==} + cpu: [x64] + os: [darwin] + requiresBuild: true + optional: true + + /@rspack/binding-linux-arm64-gnu@1.6.0-beta.0: + resolution: {integrity: sha512-ww45NhAOreOYRFpjf1TYRvNQYLOMr/LodFZA1yI5MLRCSYMgvWuv6B1WIY7o3pLaGWeY/H+zf89JTpR2rNn25g==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rspack/binding-linux-arm64-musl@1.6.0-beta.0: + resolution: {integrity: sha512-nxkqLF8vz62NR65wxxMpdRDXce6q36R3BHvibyXlElSbSPSPL0n7gsOW2yMeCB5q812DRkzm2c9RNkpQ8nfXwQ==} + cpu: [arm64] + os: [linux] + requiresBuild: true + optional: true + + /@rspack/binding-linux-x64-gnu@1.6.0-beta.0: + resolution: {integrity: sha512-xfOzI6+TeLZFJ8I1siOguIlyf5m0ABWPAStPY2Wm4EIEhJz+dr5PTWa5V6DlXZkUfV/6E5ZAQGZmgcgOR+DPHw==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rspack/binding-linux-x64-musl@1.6.0-beta.0: + resolution: {integrity: sha512-OauuEH2P4ZXQ6vCnNZZocmxUJCTdmyUyZYHBzd+cK62uGzXcGGBp+R74grfmwThL+b4rFI2IupxMCLcDOop0zg==} + cpu: [x64] + os: [linux] + requiresBuild: true + optional: true + + /@rspack/binding-wasm32-wasi@1.6.0-beta.0: + resolution: {integrity: sha512-0sSaIM39lSNthIvZsDA7WDnwdP4/4rRH4QyN4/DLUOCdci5xUSUSz/MsRXA+smI41qu4sh9yU/jRT8pq9bSWAQ==} + cpu: [wasm32] + requiresBuild: true + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + + /@rspack/binding-win32-arm64-msvc@1.6.0-beta.0: + resolution: {integrity: sha512-QqtnwHhpst37I3eQtx1FqpWCQCkigg2M73QUDvjhCE+opb1X46XK+kZOyt5qO0fKRdOgtxTcTVLLzRUWuT4t4A==} + cpu: [arm64] + os: [win32] + requiresBuild: true + optional: true + + /@rspack/binding-win32-ia32-msvc@1.6.0-beta.0: + resolution: {integrity: sha512-Bi8fcihGY8QEgo+u8S3JeD41OLIs1/b4lYYAKOUVmpwJeN0lTlT7oMVikD2bLLpPs+gUkinTeeoUjwx0YWlGlA==} + cpu: [ia32] + os: [win32] + requiresBuild: true + optional: true + + /@rspack/binding-win32-x64-msvc@1.6.0-beta.0: + resolution: {integrity: sha512-h7EltjtoAI2ohKxurD09uoXoe94nHiqcozca4Z7sB+f1RaBnc9U3RmddCVkENGZ2YFDx77fC6bayu2WkdY2YIw==} + cpu: [x64] + os: [win32] + requiresBuild: true + optional: true + + /@rspack/binding@1.6.0-beta.0: + resolution: {integrity: sha512-8CoJCSkWhhgAcmymvkvXb6fHV1zfRnx68mW/tFD/pN2BzwLrAqzvq9PUhJw0MLq4Dg+RIYG6xjzDDp3fkt574Q==} + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.6.0-beta.0 + '@rspack/binding-darwin-x64': 1.6.0-beta.0 + '@rspack/binding-linux-arm64-gnu': 1.6.0-beta.0 + '@rspack/binding-linux-arm64-musl': 1.6.0-beta.0 + '@rspack/binding-linux-x64-gnu': 1.6.0-beta.0 + '@rspack/binding-linux-x64-musl': 1.6.0-beta.0 + '@rspack/binding-wasm32-wasi': 1.6.0-beta.0 + '@rspack/binding-win32-arm64-msvc': 1.6.0-beta.0 + '@rspack/binding-win32-ia32-msvc': 1.6.0-beta.0 + '@rspack/binding-win32-x64-msvc': 1.6.0-beta.0 + + /@rspack/core@1.6.0-beta.0: + resolution: {integrity: sha512-wrGg90zjX5/cf2hKh2La0q3++twOBnk6z2WkWWcd1Jz5F9OcbHWyn+9yGHJI921A0AziC0pv7P1MmucmG3e7fA==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + dependencies: + '@module-federation/runtime-tools': 0.20.0 + '@rspack/binding': 1.6.0-beta.0 + '@rspack/lite-tapable': 1.0.1 + + /@rspack/dev-server@1.1.4(@rspack/core@1.6.0-beta.0)(webpack@5.98.0): + resolution: {integrity: sha512-kGHYX2jYf3ZiHwVl0aUEPBOBEIG1aWleCDCAi+Jg32KUu3qr/zDUpCEd0wPuHfLEgk0X0xAEYCS6JMO7nBStNQ==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': '*' + dependencies: + '@rspack/core': 1.6.0-beta.0 + chokidar: 3.6.0 + http-proxy-middleware: 2.0.9 + p-retry: 6.2.0 + webpack-dev-server: 5.2.2(webpack@5.98.0) + ws: 8.18.0 + transitivePeerDependencies: + - '@types/webpack' + - bufferutil + - debug + - supports-color + - utf-8-validate + - webpack + - webpack-cli + dev: false + + /@rspack/lite-tapable@1.0.1: + resolution: {integrity: sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==} + engines: {node: '>=16.0.0'} + /@rtsao/scc@1.1.0: resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} dev: false @@ -13770,6 +14035,13 @@ packages: engines: {node: '>=10.13.0'} dev: false + /@tybys/wasm-util@0.10.1: + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + requiresBuild: true + dependencies: + tslib: 2.8.1 + optional: true + /@types/argparse@1.0.38: resolution: {integrity: sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==} @@ -18867,7 +19139,7 @@ packages: engines: {node: '>=10.13.0'} dependencies: graceful-fs: 4.2.11 - tapable: 2.2.1 + tapable: 2.3.0 /enquirer@2.4.1: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} @@ -20498,6 +20770,43 @@ packages: utils-merge: 1.0.1 vary: 1.1.2 + /express@4.21.2: + resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==} + engines: {node: '>= 0.10.0'} + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.3 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.1 + cookie-signature: 1.0.6 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.1 + fresh: 0.5.2 + http-errors: 2.0.0 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.13.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.0 + serve-static: 1.16.2 + setprototypeof: 1.2.0 + statuses: 2.0.1 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + dev: false + /express@5.1.0: resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} engines: {node: '>= 18'} @@ -22048,6 +22357,20 @@ packages: - debug dev: false + /http-proxy-middleware@2.0.9: + resolution: {integrity: sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==} + engines: {node: '>=12.0.0'} + dependencies: + '@types/express': 4.17.21 + '@types/http-proxy': 1.17.14 + http-proxy: 1.18.1 + is-glob: 4.0.3 + is-plain-obj: 3.0.0 + micromatch: 4.0.5 + transitivePeerDependencies: + - debug + dev: false + /http-proxy@1.18.1: resolution: {integrity: sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==} engines: {node: '>=8.0.0'} @@ -25787,6 +26110,10 @@ packages: /path-to-regexp@0.1.10: resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + /path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + dev: false + /path-to-regexp@8.2.0: resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} engines: {node: '>=16'} @@ -29137,6 +29464,10 @@ packages: resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} engines: {node: '>=6'} + /tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + /tar-fs@2.1.1: resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} requiresBuild: true @@ -30649,6 +30980,59 @@ packages: - utf-8-validate dev: false + /webpack-dev-server@5.2.2(webpack@5.98.0): + resolution: {integrity: sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==} + engines: {node: '>= 18.12.0'} + hasBin: true + peerDependencies: + '@types/webpack': ^4 + webpack: ^5.0.0 || ^4 || ^5 + webpack-cli: '*' + peerDependenciesMeta: + '@types/webpack': + optional: true + webpack: + optional: true + webpack-cli: + optional: true + dependencies: + '@types/bonjour': 3.5.13 + '@types/connect-history-api-fallback': 1.5.4 + '@types/express': 4.17.21 + '@types/express-serve-static-core': 4.17.43 + '@types/serve-index': 1.9.4 + '@types/serve-static': 1.15.5 + '@types/sockjs': 0.3.36 + '@types/ws': 8.5.12 + ansi-html-community: 0.0.8 + anymatch: 3.1.3 + bonjour-service: 1.2.1 + chokidar: 3.6.0 + colorette: 2.0.20 + compression: 1.7.4 + connect-history-api-fallback: 2.0.0 + express: 4.21.2 + graceful-fs: 4.2.11 + http-proxy-middleware: 2.0.9 + ipaddr.js: 2.1.0 + launch-editor: 2.9.1 + open: 10.1.0 + p-retry: 6.2.0 + schema-utils: 4.3.0 + selfsigned: 2.4.1 + serve-index: 1.9.1 + sockjs: 0.3.24 + spdy: 4.0.2 + webpack: 5.98.0 + webpack-dev-middleware: 7.4.2(webpack@5.98.0) + ws: 8.18.0 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + /webpack-filter-warnings-plugin@1.2.1(webpack@4.47.0): resolution: {integrity: sha512-Ez6ytc9IseDMLPo0qCuNNYzgtUl8NovOqjIq4uAU8LTD4uoa1w1KpZyyzFtLTEMZpkkOkLfL9eN+KGYdk1Qtwg==} engines: {node: '>= 4.3 < 5.0.0 || >= 5.10'} diff --git a/common/config/subspaces/default/repo-state.json b/common/config/subspaces/default/repo-state.json index 0e7e44523cd..7d287129e85 100644 --- a/common/config/subspaces/default/repo-state.json +++ b/common/config/subspaces/default/repo-state.json @@ -1,5 +1,5 @@ // DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. { - "pnpmShrinkwrapHash": "b483220c289054803691f7c96d6b5278c7a05fce", + "pnpmShrinkwrapHash": "0786622d1159b3ecef516f25da3beac2c0701039", "preferredVersionsHash": "a9b67c38568259823f9cfb8270b31bf6d8470b27" } diff --git a/common/reviews/api/heft-rspack-plugin.api.md b/common/reviews/api/heft-rspack-plugin.api.md new file mode 100644 index 00000000000..2d2020909d9 --- /dev/null +++ b/common/reviews/api/heft-rspack-plugin.api.md @@ -0,0 +1,67 @@ +## API Report File for "@rushstack/heft-rspack-plugin" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import type { AsyncParallelHook } from 'tapable'; +import type { AsyncSeriesBailHook } from 'tapable'; +import type { AsyncSeriesHook } from 'tapable'; +import type { AsyncSeriesWaterfallHook } from 'tapable'; +import type { HeftConfiguration } from '@rushstack/heft'; +import type { IHeftTaskSession } from '@rushstack/heft'; +import { rspackCore } from '@rspack/core'; +import type * as TRspack from '@rspack/core'; +import type * as TRspackDevServer from '@rspack/dev-server'; + +// @beta (undocumented) +export type IRspackConfiguration = TRspack.Configuration | TRspack.Configuration[]; + +// @beta +export interface IRspackConfigurationFnEnvironment { + heftConfiguration: HeftConfiguration; + prod: boolean; + production: boolean; + rspack: RspackCoreImport; + taskSession: IHeftTaskSession; +} + +// @beta (undocumented) +export interface IRspackConfigurationWithDevServer extends TRspack.Configuration { + // (undocumented) + devServer?: TRspackDevServer.Configuration; +} + +// @beta (undocumented) +export interface IRspackPluginAccessor { + readonly hooks: IRspackPluginAccessorHooks; + readonly parameters: IRspackPluginAccessorParameters; +} + +// @beta (undocumented) +export interface IRspackPluginAccessorHooks { + readonly onAfterConfigure: AsyncParallelHook<[IRspackConfiguration], never>; + readonly onConfigure: AsyncSeriesHook<[IRspackConfiguration], never>; + readonly onEmitStats: AsyncParallelHook<[TRspack.Stats | TRspack.MultiStats], never>; + readonly onGetWatchOptions: AsyncSeriesWaterfallHook<[ + Parameters[0], + Readonly + ], never>; + readonly onLoadConfiguration: AsyncSeriesBailHook<[], IRspackConfiguration | undefined | false>; +} + +// @beta (undocumented) +export interface IRspackPluginAccessorParameters { + readonly isServeMode: boolean; +} + +// @beta (undocumented) +export const PluginName: 'rspack-plugin'; + +// @beta (undocumented) +export type RspackCoreImport = rspackCore; + +// @beta +export const STAGE_LOAD_LOCAL_CONFIG: 1000; + +``` diff --git a/heft-plugins/heft-rspack-plugin/.npmignore b/heft-plugins/heft-rspack-plugin/.npmignore new file mode 100644 index 00000000000..ffb155d74e6 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/.npmignore @@ -0,0 +1,34 @@ +# THIS IS A STANDARD TEMPLATE FOR .npmignore FILES IN THIS REPO. + +# Ignore all files by default, to avoid accidentally publishing unintended files. +* + +# Use negative patterns to bring back the specific things we want to publish. +!/bin/** +!/lib/** +!/lib-*/** +!/dist/** + +!CHANGELOG.md +!CHANGELOG.json +!heft-plugin.json +!rush-plugin-manifest.json +!ThirdPartyNotice.txt + +# Ignore certain patterns that should not get published. +/dist/*.stats.* +/lib/**/test/ +/lib-*/**/test/ +*.test.js + +# NOTE: These don't need to be specified, because NPM includes them automatically. +# +# package.json +# README.md +# LICENSE + +# --------------------------------------------------------------------------- +# DO NOT MODIFY ABOVE THIS LINE! Add any project-specific overrides below. +# --------------------------------------------------------------------------- + +!/includes/** diff --git a/heft-plugins/heft-rspack-plugin/LICENSE b/heft-plugins/heft-rspack-plugin/LICENSE new file mode 100644 index 00000000000..53dca6cd336 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/LICENSE @@ -0,0 +1,24 @@ +@rushstack/heft-rspack-plugin + +Copyright (c) Microsoft Corporation. All rights reserved. + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/heft-plugins/heft-rspack-plugin/README.md b/heft-plugins/heft-rspack-plugin/README.md new file mode 100644 index 00000000000..91a8874cfa1 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/README.md @@ -0,0 +1,12 @@ +# @rushstack/heft-rspack-plugin + +This is a Heft plugin for using Rspack. + +## Links + +- [CHANGELOG.md]( + https://github.com/microsoft/rushstack/blob/main/heft-plugins/heft-rspack-plugin/CHANGELOG.md) - Find + out what's new in the latest version +- [@rushstack/heft](https://www.npmjs.com/package/@rushstack/heft) - Heft is a config-driven toolchain that invokes popular tools such as TypeScript, ESLint, Jest, Webpack, and API Extractor. + +Heft is part of the [Rush Stack](https://rushstack.io/) family of projects. diff --git a/heft-plugins/heft-rspack-plugin/config/api-extractor.json b/heft-plugins/heft-rspack-plugin/config/api-extractor.json new file mode 100644 index 00000000000..74590d3c4f8 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/config/api-extractor.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + + "mainEntryPointFilePath": "/lib/index.d.ts", + "apiReport": { + "enabled": true, + "reportFolder": "../../../common/reviews/api" + }, + "docModel": { + "enabled": false + }, + "dtsRollup": { + "enabled": true, + "betaTrimmedFilePath": "/dist/.d.ts" + } +} diff --git a/heft-plugins/heft-rspack-plugin/config/jest.config.json b/heft-plugins/heft-rspack-plugin/config/jest.config.json new file mode 100644 index 00000000000..d1749681d90 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/config/jest.config.json @@ -0,0 +1,3 @@ +{ + "extends": "local-node-rig/profiles/default/config/jest.config.json" +} diff --git a/heft-plugins/heft-rspack-plugin/config/rig.json b/heft-plugins/heft-rspack-plugin/config/rig.json new file mode 100644 index 00000000000..165ffb001f5 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/config/rig.json @@ -0,0 +1,7 @@ +{ + // The "rig.json" file directs tools to look for their config files in an external package. + // Documentation for this system: https://www.npmjs.com/package/@rushstack/rig-package + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + + "rigPackageName": "local-node-rig" +} diff --git a/heft-plugins/heft-rspack-plugin/eslint.config.js b/heft-plugins/heft-rspack-plugin/eslint.config.js new file mode 100644 index 00000000000..c15e6077310 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/eslint.config.js @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +const nodeTrustedToolProfile = require('local-node-rig/profiles/default/includes/eslint/flat/profile/node-trusted-tool'); +const friendlyLocalsMixin = require('local-node-rig/profiles/default/includes/eslint/flat/mixins/friendly-locals'); + +module.exports = [ + ...nodeTrustedToolProfile, + ...friendlyLocalsMixin, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + tsconfigRootDir: __dirname + } + } + } +]; diff --git a/heft-plugins/heft-rspack-plugin/heft-plugin.json b/heft-plugins/heft-rspack-plugin/heft-plugin.json new file mode 100644 index 00000000000..a8ce14984a6 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/heft-plugin.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/heft/v0/heft-plugin.schema.json", + + "taskPlugins": [ + { + "pluginName": "rspack-plugin", + "entryPoint": "./lib/RspackPlugin", + "optionsSchema": "./lib/schemas/heft-rspack-plugin.schema.json", + + "parameterScope": "rspack", + "parameters": [ + { + "longName": "--serve", + "parameterKind": "flag", + "description": "Start a local web server for testing purposes using @rspack/dev-server. This parameter is only available when running in watch mode." + } + ] + } + ] +} diff --git a/heft-plugins/heft-rspack-plugin/package.json b/heft-plugins/heft-rspack-plugin/package.json new file mode 100644 index 00000000000..50fd8aabc12 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/package.json @@ -0,0 +1,38 @@ +{ + "name": "@rushstack/heft-rspack-plugin", + "version": "0.0.0", + "description": "Heft plugin for Rspack", + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rushstack.git", + "directory": "heft-plugins/heft-rspack-plugin" + }, + "homepage": "https://rushstack.io/pages/heft/overview/", + "main": "lib/index.js", + "types": "dist/heft-rspack-plugin.d.ts", + "license": "MIT", + "scripts": { + "build": "heft build --clean", + "start": "heft test --clean --watch", + "_phase:build": "heft run --only build -- --clean", + "_phase:test": "heft run --only test -- --clean" + }, + "peerDependencies": { + "@rushstack/heft": "^0.75.0", + "@rspack/core": "^1.6.0-beta.0" + }, + "dependencies": { + "@rushstack/debug-certificate-manager": "workspace:*", + "@rushstack/node-core-library": "workspace:*", + "tapable": "2.3.0", + "@rspack/dev-server": "^1.1.4", + "webpack": "~5.98.0" + }, + "devDependencies": { + "@rushstack/heft": "workspace:*", + "@rushstack/terminal": "workspace:*", + "eslint": "~9.37.0", + "local-node-rig": "workspace:*", + "@rspack/core": "~1.6.0-beta.0" + } +} \ No newline at end of file diff --git a/heft-plugins/heft-rspack-plugin/src/RspackConfigurationLoader.ts b/heft-plugins/heft-rspack-plugin/src/RspackConfigurationLoader.ts new file mode 100644 index 00000000000..3864f08dc6f --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/src/RspackConfigurationLoader.ts @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import * as path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +import type * as TRspack from '@rspack/core'; + +import type { HeftConfiguration, IHeftTaskSession } from '@rushstack/heft'; +import { FileSystem } from '@rushstack/node-core-library'; + +import type { IRspackPluginOptions } from './RspackPlugin'; +import { + type IRspackConfiguration, + type IRspackConfigurationFnEnvironment, + type IRspackPluginAccessorHooks, + PLUGIN_NAME, + STAGE_LOAD_LOCAL_CONFIG +} from './shared'; + +type IRspackConfigJsExport = + | TRspack.Configuration + | TRspack.Configuration[] + | Promise + | Promise + | ((env: IRspackConfigurationFnEnvironment) => TRspack.Configuration | TRspack.Configuration[]) + | ((env: IRspackConfigurationFnEnvironment) => Promise); +type IRspackConfigJs = IRspackConfigJsExport | { default: IRspackConfigJsExport }; + +/** + * @internal + */ +export interface ILoadRspackConfigurationOptions { + taskSession: IHeftTaskSession; + heftConfiguration: HeftConfiguration; + serveMode: boolean; + loadRspackAsyncFn: () => Promise; + hooks: Pick; + + _tryLoadConfigFileAsync?: typeof tryLoadRspackConfigurationFileAsync; +} + +const DEFAULT_RSPACK_CONFIG_PATH: './rspack.config.mjs' = './rspack.config.mjs'; +const DEFAULT_RSPACK_DEV_CONFIG_PATH: './rspack.dev.config.js' = './rspack.dev.config.js'; + +/** + * @internal + */ +export async function tryLoadRspackConfigurationAsync( + options: ILoadRspackConfigurationOptions, + pluginOptions: IRspackPluginOptions +): Promise { + const { taskSession, hooks, _tryLoadConfigFileAsync = tryLoadRspackConfigurationFileAsync } = options; + const { logger } = taskSession; + const { terminal } = logger; + + // Apply default behavior. Due to the state of `this._rspackConfiguration`, this code + // will execute exactly once. + hooks.onLoadConfiguration.tapPromise( + { + name: PLUGIN_NAME, + stage: STAGE_LOAD_LOCAL_CONFIG + }, + async () => { + terminal.writeVerboseLine(`Attempting to load Rspack configuration from local file`); + const rspackConfiguration: IRspackConfiguration | undefined = await _tryLoadConfigFileAsync( + options, + pluginOptions + ); + + if (rspackConfiguration) { + terminal.writeVerboseLine(`Loaded Rspack configuration from local file.`); + } + + return rspackConfiguration; + } + ); + + // Obtain the Rspack configuration by calling into the hook. + // The local configuration is loaded at STAGE_LOAD_LOCAL_CONFIG + terminal.writeVerboseLine('Attempting to load Rspack configuration'); + let rspackConfiguration: IRspackConfiguration | false | undefined = + await hooks.onLoadConfiguration.promise(); + + if (rspackConfiguration === false) { + terminal.writeLine('Rspack disabled by external plugin'); + rspackConfiguration = undefined; + } else if ( + rspackConfiguration === undefined || + (Array.isArray(rspackConfiguration) && rspackConfiguration.length === 0) + ) { + terminal.writeLine('No Rspack configuration found'); + rspackConfiguration = undefined; + } else { + if (hooks.onConfigure.isUsed()) { + // Allow for plugins to customize the configuration + await hooks.onConfigure.promise(rspackConfiguration); + } + if (hooks.onAfterConfigure.isUsed()) { + // Provide the finalized configuration + await hooks.onAfterConfigure.promise(rspackConfiguration); + } + } + return rspackConfiguration as IRspackConfiguration | undefined; +} + +/** + * @internal + */ +export async function tryLoadRspackConfigurationFileAsync( + options: ILoadRspackConfigurationOptions, + pluginOptions: IRspackPluginOptions +): Promise { + const { taskSession, heftConfiguration, loadRspackAsyncFn, serveMode } = options; + const { + logger, + parameters: { production } + } = taskSession; + const { terminal } = logger; + const { configurationPath, devConfigurationPath } = pluginOptions; + let rspackConfigJs: IRspackConfigJs | undefined; + + try { + const buildFolderPath: string = heftConfiguration.buildFolderPath; + if (serveMode) { + const devConfigPath: string = path.resolve( + buildFolderPath, + devConfigurationPath || DEFAULT_RSPACK_DEV_CONFIG_PATH + ); + terminal.writeVerboseLine(`Attempting to load rspack configuration from "${devConfigPath}".`); + rspackConfigJs = await _tryLoadRspackConfigurationFileInnerAsync(devConfigPath); + } + + if (!rspackConfigJs) { + const configPath: string = path.resolve( + buildFolderPath, + configurationPath || DEFAULT_RSPACK_CONFIG_PATH + ); + terminal.writeVerboseLine(`Attempting to load rspack configuration from "${configPath}".`); + rspackConfigJs = await _tryLoadRspackConfigurationFileInnerAsync(configPath); + } + } catch (error) { + logger.emitError(error as Error); + } + + if (rspackConfigJs) { + const rspackConfig: IRspackConfigJsExport = + (rspackConfigJs as { default: IRspackConfigJsExport }).default || + (rspackConfigJs as IRspackConfigJsExport); + + if (typeof rspackConfig === 'function') { + // Defer loading of rspack until we know for sure that we will need it + return rspackConfig({ + prod: production, + production, + taskSession, + heftConfiguration, + rspack: await loadRspackAsyncFn() + }); + } else { + return rspackConfig; + } + } else { + return undefined; + } +} + +/** + * @internal + */ +export async function _tryLoadRspackConfigurationFileInnerAsync( + configurationPath: string +): Promise { + const configExists: boolean = await FileSystem.existsAsync(configurationPath); + if (configExists) { + try { + const configurationUri: string = pathToFileURL(configurationPath).href; + return await import(configurationUri); + } catch (e) { + const error: NodeJS.ErrnoException = e as NodeJS.ErrnoException; + if (error.code === 'ERR_MODULE_NOT_FOUND') { + // No configuration found, return undefined. + return undefined; + } + throw new Error(`Error loading Rspack configuration at "${configurationPath}": ${e}`); + } + } else { + return undefined; + } +} diff --git a/heft-plugins/heft-rspack-plugin/src/RspackPlugin.ts b/heft-plugins/heft-rspack-plugin/src/RspackPlugin.ts new file mode 100644 index 00000000000..4349b82a2be --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/src/RspackPlugin.ts @@ -0,0 +1,504 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type { AddressInfo } from 'node:net'; + +import type * as TRspack from '@rspack/core'; +import type * as TRspackDevServer from '@rspack/dev-server'; +import { AsyncParallelHook, AsyncSeriesBailHook, AsyncSeriesHook, AsyncSeriesWaterfallHook } from 'tapable'; + +import { CertificateManager, type ICertificate } from '@rushstack/debug-certificate-manager'; +import { FileError, InternalError, LegacyAdapters } from '@rushstack/node-core-library'; +import type { + HeftConfiguration, + IHeftTaskSession, + IHeftTaskPlugin, + IHeftTaskRunHookOptions, + IScopedLogger, + IHeftTaskRunIncrementalHookOptions +} from '@rushstack/heft'; + +import { + type IRspackConfiguration, + type IRspackPluginAccessor, + PLUGIN_NAME, + type IRspackPluginAccessorHooks, + type RspackCoreImport +} from './shared'; +import { tryLoadRspackConfigurationAsync } from './RspackConfigurationLoader'; + +export interface IRspackPluginOptions { + devConfigurationPath?: string | undefined; + configurationPath?: string | undefined; +} +const SERVE_PARAMETER_LONG_NAME: '--serve' = '--serve'; +const RSPACK_PACKAGE_NAME: '@rspack/core' = '@rspack/core'; +const RSPACK_DEV_SERVER_PACKAGE_NAME: '@rspack/dev-server' = '@rspack/dev-server'; +const RSPACK_DEV_SERVER_ENV_VAR_NAME: 'RSPACK_DEV_SERVER' = 'RSPACK_DEV_SERVER'; +const WEBPACK_DEV_MIDDLEWARE_PACKAGE_NAME: 'webpack-dev-middleware' = 'webpack-dev-middleware'; + +/** + * @internal + */ +export default class RspackPlugin implements IHeftTaskPlugin { + private _accessor: IRspackPluginAccessor | undefined; + private _isServeMode: boolean = false; + private _rspack: RspackCoreImport | undefined; + private _rspackCompiler: TRspack.Compiler | TRspack.MultiCompiler | undefined; + private _rspackConfiguration: IRspackConfiguration | undefined | false = false; + private _rspackCompilationDonePromise: Promise | undefined; + private _rspackCompilationDonePromiseResolveFn: (() => void) | undefined; + + private _warnings: Error[] = []; + private _errors: Error[] = []; + + public get accessor(): IRspackPluginAccessor { + if (!this._accessor) { + this._accessor = { + hooks: _createAccessorHooks(), + parameters: { + isServeMode: this._isServeMode + } + }; + } + return this._accessor; + } + + public apply( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + options: IRspackPluginOptions = {} + ): void { + this._isServeMode = taskSession.parameters.getFlagParameter(SERVE_PARAMETER_LONG_NAME).value; + if (this._isServeMode && !taskSession.parameters.watch) { + throw new Error( + `The ${JSON.stringify( + SERVE_PARAMETER_LONG_NAME + )} parameter is only available when running in watch mode.` + + ` Try replacing "${taskSession.parsedCommandLine?.unaliasedCommandName}" with` + + ` "${taskSession.parsedCommandLine?.unaliasedCommandName}-watch" in your Heft command line.` + ); + } + + taskSession.hooks.run.tapPromise(PLUGIN_NAME, async (runOptions: IHeftTaskRunHookOptions) => { + await this._runRspackAsync(taskSession, heftConfiguration, options); + }); + + taskSession.hooks.runIncremental.tapPromise( + PLUGIN_NAME, + async (runOptions: IHeftTaskRunIncrementalHookOptions) => { + await this._runRspackWatchAsync(taskSession, heftConfiguration, options, runOptions.requestRun); + } + ); + } + + private async _getRspackConfigurationAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + options: IRspackPluginOptions, + requestRun?: () => void + ): Promise { + if (this._rspackConfiguration === false) { + const rspackConfiguration: IRspackConfiguration | undefined = await tryLoadRspackConfigurationAsync( + { + taskSession, + heftConfiguration, + hooks: this.accessor.hooks, + serveMode: this._isServeMode, + loadRspackAsyncFn: this._loadRspackAsync.bind(this, taskSession, heftConfiguration) + }, + options + ); + + this._rspackConfiguration = rspackConfiguration; + } + + return this._rspackConfiguration; + } + + private async _loadRspackAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration + ): Promise { + if (!this._rspack) { + try { + const rspackPackagePath: string = await heftConfiguration.rigPackageResolver.resolvePackageAsync( + RSPACK_PACKAGE_NAME, + taskSession.logger.terminal + ); + this._rspack = await import(rspackPackagePath); + taskSession.logger.terminal.writeDebugLine(`Using Rspack from rig package at "${rspackPackagePath}"`); + } catch (e) { + // Fallback to bundled version if not found in rig. + this._rspack = await import(RSPACK_PACKAGE_NAME); + taskSession.logger.terminal.writeDebugLine(`Using Rspack from built-in "${RSPACK_PACKAGE_NAME}"`); + } + } + return this._rspack!; + } + + private async _getRspackCompilerAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + rspackConfiguration: IRspackConfiguration + ): Promise { + if (!this._rspackCompiler) { + const rspack: RspackCoreImport = await this._loadRspackAsync(taskSession, heftConfiguration); + taskSession.logger.terminal.writeLine(`Using Rspack version ${rspack.version}`); + this._rspackCompiler = Array.isArray(rspackConfiguration) + ? rspack.default(rspackConfiguration) /* (rspack.Compilation[]) => MultiCompiler */ + : rspack.default(rspackConfiguration); /* (rspack.Compilation) => Compiler */ + } + return this._rspackCompiler; + } + + private async _runRspackAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + options: IRspackPluginOptions + ): Promise { + this._validateEnvironmentVariable(taskSession); + if (taskSession.parameters.watch || this._isServeMode) { + // Should never happen, but just in case + throw new InternalError('Cannot run Rspack in compilation mode when watch mode is enabled'); + } + + // Load the config and compiler, and return if there is no config found + const rspackConfiguration: IRspackConfiguration | undefined = await this._getRspackConfigurationAsync( + taskSession, + heftConfiguration, + options + ); + if (!rspackConfiguration) { + return; + } + const compiler: TRspack.Compiler | TRspack.MultiCompiler = await this._getRspackCompilerAsync( + taskSession, + heftConfiguration, + rspackConfiguration + ); + taskSession.logger.terminal.writeLine('Running Rspack compilation'); + + // Run the rspack compiler + let stats: TRspack.Stats | TRspack.MultiStats | undefined; + try { + stats = await LegacyAdapters.convertCallbackToPromise( + (compiler as TRspack.Compiler).run.bind(compiler) + ); + await LegacyAdapters.convertCallbackToPromise(compiler.close.bind(compiler)); + } catch (e) { + taskSession.logger.emitError(e as Error); + } + + // Emit the errors from the stats object, if present + if (stats) { + this._recordErrors(stats, heftConfiguration.buildFolderPath); + this._emitErrors(taskSession.logger); + if (this.accessor.hooks.onEmitStats.isUsed()) { + await this.accessor.hooks.onEmitStats.promise(stats); + } + } + } + + private async _runRspackWatchAsync( + taskSession: IHeftTaskSession, + heftConfiguration: HeftConfiguration, + options: IRspackPluginOptions, + requestRun: () => void + ): Promise { + // Save a handle to the original promise, since the this-scoped promise will be replaced whenever + // the compilation completes. + let rspackCompilationDonePromise: Promise | undefined = this._rspackCompilationDonePromise; + + if (!this._rspackCompiler) { + this._validateEnvironmentVariable(taskSession); + if (!taskSession.parameters.watch) { + // Should never happen, but just in case + throw new InternalError('Cannot run Rspack in watch mode when watch mode is not enabled'); + } + + // Load the config and compiler, and return if there is no config found + const rspackConfiguration: IRspackConfiguration | undefined = await this._getRspackConfigurationAsync( + taskSession, + heftConfiguration, + options, + requestRun + ); + if (!rspackConfiguration) { + return; + } + + // Get the compiler which will be used for both serve and watch mode + const compiler: TRspack.Compiler | TRspack.MultiCompiler = await this._getRspackCompilerAsync( + taskSession, + heftConfiguration, + rspackConfiguration + ); + + // Set up the hook to detect when the watcher completes the watcher compilation. We will also log out + // errors from the compilation if present from the output stats object. + this._rspackCompilationDonePromise = new Promise((resolve: () => void) => { + this._rspackCompilationDonePromiseResolveFn = resolve; + }); + rspackCompilationDonePromise = this._rspackCompilationDonePromise; + compiler.hooks.done.tap(PLUGIN_NAME, (stats?: TRspack.Stats | TRspack.MultiStats) => { + this._rspackCompilationDonePromiseResolveFn!(); + this._rspackCompilationDonePromise = new Promise((resolve: () => void) => { + this._rspackCompilationDonePromiseResolveFn = resolve; + }); + + if (stats) { + this._recordErrors(stats, heftConfiguration.buildFolderPath); + } + }); + + // Determine how we will run the compiler. When serving, we will run the compiler + // via the @rspack/dev-server. Otherwise, we will run the compiler directly. + if (this._isServeMode) { + const defaultDevServerOptions: TRspackDevServer.Configuration = { + host: 'localhost', + devMiddleware: { + publicPath: '/', + stats: { + cached: false, + cachedAssets: false, + colors: heftConfiguration.terminalProvider.supportsColor + } + }, + client: { + logging: 'info', + webSocketURL: { + port: 8080 + } + }, + watchFiles: [], + static: [], + port: 8080, + onListening: (server: TRspackDevServer.RspackDevServer) => { + const addressInfo: AddressInfo | string | undefined = server.server?.address() as AddressInfo; + if (addressInfo) { + let url: string; + if (typeof addressInfo === 'string') { + url = addressInfo; + } else { + const address: string = + addressInfo.family === 'IPv6' + ? `[${addressInfo.address}]:${addressInfo.port}` + : `${addressInfo.address}:${addressInfo.port}`; + url = `https://${address}/`; + } + taskSession.logger.terminal.writeLine(`Started Rspack Dev Server at ${url}`); + } + } + }; + + // Obtain the devServerOptions from the rspack configuration, and combine with the default options + let devServerOptions: TRspackDevServer.Configuration; + if (Array.isArray(rspackConfiguration)) { + const filteredDevServerOptions: TRspackDevServer.Configuration[] = rspackConfiguration + .map((configuration) => configuration.devServer) + .filter((devServer): devServer is TRspackDevServer.Configuration => !!devServer); + if (filteredDevServerOptions.length > 1) { + taskSession.logger.emitWarning( + new Error(`Detected multiple rspack devServer configurations, using the first one.`) + ); + } + devServerOptions = { ...defaultDevServerOptions, ...filteredDevServerOptions[0] }; + } else { + devServerOptions = { ...defaultDevServerOptions, ...rspackConfiguration.devServer }; + } + + // Add the certificate and key to the devServerOptions if these fields don't already have values + if (!devServerOptions.server) { + const certificateManager: CertificateManager = new CertificateManager(); + const certificate: ICertificate = await certificateManager.ensureCertificateAsync( + true, + taskSession.logger.terminal + ); + + // Update the web socket URL to use the hostname provided by the certificate + const clientConfiguration: TRspackDevServer.Configuration['client'] = devServerOptions.client; + const hostname: string | undefined = certificate.subjectAltNames?.[0]; + if (hostname && typeof clientConfiguration === 'object') { + const { webSocketURL } = clientConfiguration; + if (typeof webSocketURL === 'object') { + clientConfiguration.webSocketURL = { + ...webSocketURL, + hostname + }; + } + } + + devServerOptions = { + ...devServerOptions, + server: { + type: 'https', + options: { + minVersion: 'TLSv1.3', + key: certificate.pemKey, + cert: certificate.pemCertificate, + ca: certificate.pemCaCertificate + } + } + }; + } + + // Since the webpack-dev-server does not return infrastructure errors via a callback like + // compiler.watch(...), we will need to intercept them and log them ourselves. + // note: @rspack/dev-server extends webpack-dev-server and also has this behavior + compiler.hooks.infrastructureLog.tap( + PLUGIN_NAME, + (name: string, type: string, args: unknown[] | undefined) => { + if (name === WEBPACK_DEV_MIDDLEWARE_PACKAGE_NAME && type === 'error') { + const error: Error | undefined = args?.[0] as Error | undefined; + if (error) { + taskSession.logger.emitError(error); + } + } + } + ); + + // The webpack-dev-server package has a design flaw, where merely loading its package will set the + // WEBPACK_DEV_SERVER environment variable -- even if no APIs are accessed. This environment variable + // causes incorrect behavior if Heft is not running in serve mode. Thus, we need to be careful to call + // require() only if Heft is in serve mode. + // note: @rspack/dev-server extends webpack-dev-server and also has this behavior + taskSession.logger.terminal.writeLine('Starting rspack-dev-server'); + const RspackDevServer: typeof TRspackDevServer.RspackDevServer = ( + await import(RSPACK_DEV_SERVER_PACKAGE_NAME) + ).RspackDevServer; + const rspackDevServer: TRspackDevServer.RspackDevServer = new RspackDevServer( + devServerOptions, + compiler + ); + await rspackDevServer.start(); + } else { + // Create the watcher. Compilation will start immediately after invoking watch(). + taskSession.logger.terminal.writeLine('Starting Rspack watcher'); + + const { onGetWatchOptions } = this.accessor.hooks; + + const watchOptions: + | Parameters[0] + | Parameters[0] = onGetWatchOptions.isUsed() + ? await onGetWatchOptions.promise({}, rspackConfiguration) + : {}; + + (compiler as TRspack.Compiler).watch(watchOptions, (error?: Error | null) => { + if (error) { + taskSession.logger.emitError(error); + } + }); + } + } + + // Resume the compilation, wait for the compilation to complete, then suspend the watchers until the + // next iteration. Even if there are no changes, the promise should resolve since resuming from a + // suspended state invalidates the state of the watcher. + await rspackCompilationDonePromise; + + this._emitErrors(taskSession.logger); + } + + private _validateEnvironmentVariable(taskSession: IHeftTaskSession): void { + if (!this._isServeMode && process.env[RSPACK_DEV_SERVER_ENV_VAR_NAME]) { + taskSession.logger.emitWarning( + new Error( + `The "${RSPACK_DEV_SERVER_ENV_VAR_NAME}" environment variable is set, ` + + 'which will cause problems when rspack is not running in serve mode. ' + + `(Did a dependency inadvertently load the "${RSPACK_DEV_SERVER_PACKAGE_NAME}" package?)` + ) + ); + } + } + + private _emitErrors(logger: IScopedLogger): void { + for (const warning of this._warnings) { + logger.emitWarning(warning); + } + for (const error of this._errors) { + logger.emitError(error); + } + } + + private _recordErrors(stats: TRspack.Stats | TRspack.MultiStats, buildFolderPath: string): void { + const errors: Error[] = this._errors; + const warnings: Error[] = this._warnings; + + errors.length = 0; + warnings.length = 0; + + if (stats.hasErrors() || stats.hasWarnings()) { + const serializedStats: TRspack.StatsCompilation[] = [stats.toJson('errors-warnings')]; + + for (const compilationStats of serializedStats) { + if (compilationStats.warnings) { + for (const warning of compilationStats.warnings) { + warnings.push(this._normalizeError(buildFolderPath, warning)); + } + } + + if (compilationStats.errors) { + for (const error of compilationStats.errors) { + errors.push(this._normalizeError(buildFolderPath, error)); + } + } + + if (compilationStats.children) { + for (const child of compilationStats.children) { + serializedStats.push(child); + } + } + } + } + } + + private _normalizeError(buildFolderPath: string, error: TRspack.StatsError): Error { + if (error instanceof Error) { + return error; + } else if (error.moduleIdentifier) { + let lineNumber: number | undefined; + let columnNumber: number | undefined; + if (error.loc) { + // Format of ":-" + // https://webpack.js.org/api/stats/#errors-and-warnings + const [lineNumberRaw, columnRangeRaw] = error.loc.split(':'); + const [startColumnRaw] = columnRangeRaw.split('-'); + if (lineNumberRaw) { + lineNumber = parseInt(lineNumberRaw, 10); + if (Number.isNaN(lineNumber)) { + lineNumber = undefined; + } + } + if (startColumnRaw) { + columnNumber = parseInt(startColumnRaw, 10); + if (Number.isNaN(columnNumber)) { + columnNumber = undefined; + } + } + } + + return new FileError(error.message, { + absolutePath: error.moduleIdentifier, + projectFolder: buildFolderPath, + line: lineNumber, + column: columnNumber + }); + } else { + return new Error(error.message); + } + } +} + +/** + * @internal + */ +export function _createAccessorHooks(): IRspackPluginAccessorHooks { + return { + onLoadConfiguration: new AsyncSeriesBailHook(), + onConfigure: new AsyncSeriesHook(['rspackConfiguration']), + onAfterConfigure: new AsyncParallelHook(['rspackConfiguration']), + onEmitStats: new AsyncParallelHook(['rspackStats']), + onGetWatchOptions: new AsyncSeriesWaterfallHook(['watchOptions', 'rspackConfiguration']) + }; +} diff --git a/heft-plugins/heft-rspack-plugin/src/index.ts b/heft-plugins/heft-rspack-plugin/src/index.ts new file mode 100644 index 00000000000..0246643b10d --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/src/index.ts @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +/** + * HeftRspackPlugin is a Heft plugin that integrates the Rspack bundler into the Heft build process. + * + * @packageDocumentation + */ + +export { PLUGIN_NAME as PluginName, STAGE_LOAD_LOCAL_CONFIG } from './shared'; + +export type { + IRspackConfigurationWithDevServer, + IRspackConfiguration, + IRspackConfigurationFnEnvironment, + IRspackPluginAccessor, + IRspackPluginAccessorHooks, + IRspackPluginAccessorParameters, + RspackCoreImport +} from './shared'; diff --git a/heft-plugins/heft-rspack-plugin/src/schemas/heft-rspack-plugin.schema.json b/heft-plugins/heft-rspack-plugin/src/schemas/heft-rspack-plugin.schema.json new file mode 100644 index 00000000000..3a488443547 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/src/schemas/heft-rspack-plugin.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Rspack Plugin Configuration", + "description": "Defines options for Rspack plugin execution.", + "type": "object", + + "additionalProperties": false, + + "properties": { + "$schema": { + "description": "Part of the JSON Schema standard, this optional keyword declares the URL of the schema that the file conforms to. Editors may download the schema and use it to perform syntax highlighting.", + "type": "string" + }, + + "devConfigurationPath": { + "description": "Specifies a relative path to the Rspack dev configuration, which is used in \"serve\" mode. The default value is \"./rspack.dev.config.js\".", + "type": "string" + }, + + "configurationPath": { + "description": "Specifies a relative path to the Rspack configuration. The default value is \"./rspack.config.js\".", + "type": "string" + } + } +} diff --git a/heft-plugins/heft-rspack-plugin/src/shared.ts b/heft-plugins/heft-rspack-plugin/src/shared.ts new file mode 100644 index 00000000000..dde089b2141 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/src/shared.ts @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license. +// See LICENSE in the project root for license information. + +import type * as TRspack from '@rspack/core'; +import type * as TRspackDevServer from '@rspack/dev-server'; +import type { + AsyncParallelHook, + AsyncSeriesBailHook, + AsyncSeriesHook, + AsyncSeriesWaterfallHook +} from 'tapable'; + +import type { HeftConfiguration, IHeftTaskSession } from '@rushstack/heft'; + +/** + * @beta + */ +export type RspackCoreImport = typeof import('@rspack/core'); + +/** + * The environment passed into the Rspack configuration function. Loosely based + * on the default Rspack environment options, specified here: + * https://rspack.rs/plugins/webpack/environment-plugin#options + * + * @beta + */ +export interface IRspackConfigurationFnEnvironment { + /** + * Whether or not the run is in production mode. Synonym of + * {@link IRspackConfigurationFnEnvironment.production}. + */ + prod: boolean; + /** + * Whether or not the run is in production mode. Synonym of + * {@link IRspackConfigurationFnEnvironment.prod}. + */ + production: boolean; + + // Non-standard environment options + /** + * The task session provided to the plugin. + */ + taskSession: IHeftTaskSession; + /** + * The Heft configuration provided to the plugin. + */ + heftConfiguration: HeftConfiguration; + /** + * The resolved Rspack package. + */ + rspack: RspackCoreImport; +} + +/** + * @beta + */ +export interface IRspackConfigurationWithDevServer extends TRspack.Configuration { + devServer?: TRspackDevServer.Configuration; +} + +/** + * @beta + */ +export type IRspackConfiguration = TRspack.Configuration | TRspack.Configuration[]; + +/** + * @beta + */ +export interface IRspackPluginAccessorHooks { + /** + * A hook that allows for loading custom configurations used by the Rspack + * plugin. If a tap returns a value other than `undefined` before stage {@link STAGE_LOAD_LOCAL_CONFIG}, + * it will suppress loading from the Rspack config file. To provide a fallback behavior in the + * absence of a local config file, tap this hook with a `stage` value greater than {@link STAGE_LOAD_LOCAL_CONFIG}. + * + * @remarks + * Tapable event handlers can return `false` instead of `undefined` to suppress + * other handlers from creating a configuration object, and prevent Rspack from running. + */ + readonly onLoadConfiguration: AsyncSeriesBailHook<[], IRspackConfiguration | undefined | false>; + /** + * A hook that allows for modification of the loaded configuration used by the Rspack + * plugin. If no configuration was loaded, this hook will not be called. + */ + readonly onConfigure: AsyncSeriesHook<[IRspackConfiguration], never>; + /** + * A hook that provides the finalized configuration that will be used by Rspack. + * If no configuration was loaded, this hook will not be called. + */ + readonly onAfterConfigure: AsyncParallelHook<[IRspackConfiguration], never>; + /** + * A hook that provides the stats output from Rspack. If no configuration is loaded, + * this hook will not be called. + */ + readonly onEmitStats: AsyncParallelHook<[TRspack.Stats | TRspack.MultiStats], never>; + /** + * A hook that allows for customization of the file watcher options. If not running in watch mode, this hook will not be called. + */ + readonly onGetWatchOptions: AsyncSeriesWaterfallHook< + [Parameters[0], Readonly], + never + >; +} + +/** + * @beta + */ +export interface IRspackPluginAccessorParameters { + /** + * Whether or not serve mode was enabled by passing the `--serve` flag. + */ + readonly isServeMode: boolean; +} + +/** + * @beta + */ +export interface IRspackPluginAccessor { + /** + * Hooks that are called at various points in the Rspack plugin lifecycle. + */ + readonly hooks: IRspackPluginAccessorHooks; + /** + * Parameters that are provided by the Rspack plugin. + */ + readonly parameters: IRspackPluginAccessorParameters; +} + +/** + * The stage in the `onLoadConfiguration` hook at which the config will be loaded from the local + * rspack config file. + * @beta + */ +export const STAGE_LOAD_LOCAL_CONFIG: 1000 = 1000; + +/** + * @beta + */ +export const PLUGIN_NAME: 'rspack-plugin' = 'rspack-plugin'; diff --git a/heft-plugins/heft-rspack-plugin/tsconfig.json b/heft-plugins/heft-rspack-plugin/tsconfig.json new file mode 100644 index 00000000000..e64ab1d2405 --- /dev/null +++ b/heft-plugins/heft-rspack-plugin/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "./node_modules/local-node-rig/profiles/default/tsconfig-base.json", + "compilerOptions": { + "lib": [ + "DOM" + ], + "module": "nodenext", + "moduleResolution": "nodenext" + } +} \ No newline at end of file diff --git a/heft-plugins/heft-webpack5-plugin/src/Webpack5Plugin.ts b/heft-plugins/heft-webpack5-plugin/src/Webpack5Plugin.ts index d2d264d404c..bd42b10cbe7 100644 --- a/heft-plugins/heft-webpack5-plugin/src/Webpack5Plugin.ts +++ b/heft-plugins/heft-webpack5-plugin/src/Webpack5Plugin.ts @@ -142,6 +142,7 @@ export default class Webpack5Plugin implements IHeftTaskPlugin { const addressInfo: AddressInfo | string | undefined = server.server?.address() as AddressInfo; if (addressInfo) { - const address: string = - typeof addressInfo === 'string' ? addressInfo : `${addressInfo.address}:${addressInfo.port}`; - taskSession.logger.terminal.writeLine(`Started Webpack Dev Server at https://${address}`); + let url: string; + if (typeof addressInfo === 'string') { + url = addressInfo; + } else { + const address: string = + addressInfo.family === 'IPv6' + ? `[${addressInfo.address}]:${addressInfo.port}` + : `${addressInfo.address}:${addressInfo.port}`; + url = `https://${address}/`; + } + taskSession.logger.terminal.writeLine(`Started Webpack Dev Server at ${url}`); } } }; @@ -476,13 +485,13 @@ export default class Webpack5Plugin implements IHeftTaskPlugin