├── .gitignore ├── dist ├── browser.d.ts ├── constants.d.ts ├── loader.d.ts ├── index.d.ts ├── aut-runner.d.ts ├── browser.js ├── dynamic-import.d.ts ├── makeDefaultRspackConfig.d.ts ├── index.js ├── constants.js ├── makeRspackConfig.d.ts ├── aut-runner.js ├── CypressCTRspackPlugin.d.ts ├── dynamic-import.js ├── createRspackDevServer.d.ts ├── createRspackDevServer.js ├── devServer.d.ts ├── helpers │ ├── sourceRelativeRspackModules.d.ts │ └── sourceRelativeRspackModules.js ├── makeDefaultRspackConfig.js ├── devServer.js ├── loader.js ├── CypressCTRspackPlugin.js └── makeRspackConfig.js ├── .vscode └── settings.json ├── src ├── browser.ts ├── index.ts ├── constants.ts ├── aut-runner.ts ├── dynamic-import.ts ├── createRspackDevServer.ts ├── makeDefaultRspackConfig.ts ├── loader.ts ├── devServer.ts ├── makeRspackConfig.ts ├── CypressCTRspackPlugin.ts └── helpers │ └── sourceRelativeRspackModules.ts ├── .prettierrc.js ├── .prettierignore ├── babel.config.js ├── cypress ├── component │ ├── TestComponent.tsx │ └── Test.cy.tsx ├── support │ ├── component-index.html │ └── component.ts └── config │ ├── rspack.config.js │ └── cypress.config.ts ├── .github └── workflows │ ├── changelog-ci.yml │ └── main.yml ├── eslint.config.mjs ├── test ├── test-helper │ └── createModuleMatrixResult.ts ├── __snapshots__ │ └── makeDefaultRspackConfig.spec.ts.snap └── makeDefaultRspackConfig.spec.ts ├── LICENSE ├── description.md ├── __snapshots__ └── makeRspackConfig.spec.ts.js ├── package.json ├── tsconfig.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | -------------------------------------------------------------------------------- /dist/browser.d.ts: -------------------------------------------------------------------------------- 1 | declare function render(): void; 2 | -------------------------------------------------------------------------------- /dist/constants.d.ts: -------------------------------------------------------------------------------- 1 | export declare const configFiles: string[]; 2 | -------------------------------------------------------------------------------- /dist/loader.d.ts: -------------------------------------------------------------------------------- 1 | export default function loader(this: unknown): string; 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | function render() { 2 | require('!!./loader.js!./browser.js') 3 | } 4 | 5 | render() 6 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 100, 3 | singleQuote: true, 4 | semi: false, 5 | } 6 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { devServer } from './devServer'; 2 | export { devServer }; 3 | export default devServer; 4 | -------------------------------------------------------------------------------- /dist/aut-runner.d.ts: -------------------------------------------------------------------------------- 1 | export declare function init(importPromises: Array<() => Promise>, parent?: Window): void; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { devServer } from './devServer' 2 | 3 | export { devServer } 4 | 5 | export default devServer 6 | -------------------------------------------------------------------------------- /dist/browser.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function render() { 3 | require('!!./loader.js!./browser.js'); 4 | } 5 | render(); 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | dist 4 | pnpm-lock.yaml 5 | 6 | CODEOWNERS 7 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript'], 3 | } 4 | -------------------------------------------------------------------------------- /dist/dynamic-import.d.ts: -------------------------------------------------------------------------------- 1 | export declare const dynamicImport: (module: string) => Promise; 2 | export declare const dynamicAbsoluteImport: (filePath: string) => Promise; 3 | -------------------------------------------------------------------------------- /cypress/component/TestComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export const TestComponent = ({ title, text }) => { 4 | return ( 5 |
6 |

{title}

7 | 8 |

{text}

9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /dist/makeDefaultRspackConfig.d.ts: -------------------------------------------------------------------------------- 1 | import { type Configuration } from '@rspack/core'; 2 | import type { CreateFinalRspackConfig } from './createRspackDevServer'; 3 | export declare function makeCypressRspackConfig(config: CreateFinalRspackConfig): Configuration; 4 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const configFiles = [ 2 | 'rspack.config.ts', 3 | 'rspack.config.js', 4 | 'rspack.config.mjs', 5 | 'rspack.config.cjs', 6 | 'webpack.config.ts', 7 | 'webpack.config.js', 8 | 'webpack.config.mjs', 9 | 'webpack.config.cjs', 10 | ] 11 | -------------------------------------------------------------------------------- /.github/workflows/changelog-ci.yml: -------------------------------------------------------------------------------- 1 | name: Changelog CI 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Run Changelog CI 15 | uses: saadmk11/changelog-ci@v1.1.2 16 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.devServer = void 0; 4 | const devServer_1 = require("./devServer"); 5 | Object.defineProperty(exports, "devServer", { enumerable: true, get: function () { return devServer_1.devServer; } }); 6 | exports.default = devServer_1.devServer; 7 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Components App 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /cypress/component/Test.cy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TestComponent } from './TestComponent' 3 | 4 | describe('Test.cy.tsx', () => { 5 | it('playground', () => { 6 | cy.mount() 7 | cy.contains('h1', 'TitleText').should('be.visible') 8 | cy.contains('p', 'BodyText').should('be.visible') 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /dist/constants.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.configFiles = void 0; 4 | exports.configFiles = [ 5 | 'rspack.config.ts', 6 | 'rspack.config.js', 7 | 'rspack.config.mjs', 8 | 'rspack.config.cjs', 9 | 'webpack.config.ts', 10 | 'webpack.config.js', 11 | 'webpack.config.mjs', 12 | 'webpack.config.cjs', 13 | ]; 14 | -------------------------------------------------------------------------------- /dist/makeRspackConfig.d.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from '@rspack/core'; 2 | import type { CreateFinalRspackConfig } from './createRspackDevServer'; 3 | export declare const CYPRESS_RSPACK_ENTRYPOINT: string; 4 | /** 5 | * Creates a rspack compatible rspack "configuration" to pass to the sourced rspack function 6 | */ 7 | export declare function makeRspackConfig(config: CreateFinalRspackConfig): Promise; 8 | -------------------------------------------------------------------------------- /src/aut-runner.ts: -------------------------------------------------------------------------------- 1 | /* eslint-env browser */ 2 | 3 | export function init( 4 | importPromises: Array<() => Promise>, 5 | parent: Window = window.opener || window.parent, 6 | ) { 7 | // @ts-expect-error 8 | const Cypress = (window.Cypress = parent.Cypress) 9 | 10 | if (!Cypress) { 11 | throw new Error('Tests cannot run without a reference to Cypress!') 12 | } 13 | 14 | Cypress.onSpecWindow(window, importPromises) 15 | Cypress.action('app:window:before:load', window) 16 | } 17 | -------------------------------------------------------------------------------- /dist/aut-runner.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* eslint-env browser */ 3 | Object.defineProperty(exports, "__esModule", { value: true }); 4 | exports.init = init; 5 | function init(importPromises, parent = window.opener || window.parent) { 6 | // @ts-expect-error 7 | const Cypress = (window.Cypress = parent.Cypress); 8 | if (!Cypress) { 9 | throw new Error('Tests cannot run without a reference to Cypress!'); 10 | } 11 | Cypress.onSpecWindow(window, importPromises); 12 | Cypress.action('app:window:before:load', window); 13 | } 14 | -------------------------------------------------------------------------------- /cypress/config/rspack.config.js: -------------------------------------------------------------------------------- 1 | const rspack = require('@rspack/core') 2 | 3 | module.exports = { 4 | plugins: [new rspack.ProvidePlugin({ process: 'process' })], 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.[tj]sx?$/, 9 | loader: 'builtin:swc-loader', 10 | options: { 11 | parser: { 12 | syntax: 'typescript', 13 | tsx: true, 14 | }, 15 | }, 16 | }, 17 | ], 18 | }, 19 | resolve: { 20 | extensions: ['.ts', '.tsx', '.js', '.jsx'], 21 | }, 22 | } 23 | -------------------------------------------------------------------------------- /dist/CypressCTRspackPlugin.d.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'events'; 2 | export interface CypressCTRspackPluginOptions { 3 | files: Cypress.Cypress['spec'][]; 4 | projectRoot: string; 5 | supportFile: string | false; 6 | devServerEvents: EventEmitter; 7 | indexHtmlFile: string; 8 | } 9 | export type CypressCTContextOptions = Omit; 10 | export interface CypressCTRspackContext { 11 | _cypress: CypressCTContextOptions; 12 | } 13 | export declare const normalizeError: (error: Error | string) => string; 14 | -------------------------------------------------------------------------------- /cypress/config/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | import { devServer } from '../../dist/devServer' 3 | 4 | export default defineConfig({ 5 | component: { 6 | video: false, 7 | screenshotOnRunFailure: false, 8 | supportFile: './support/component.ts', 9 | specPattern: './component/*.cy.{tsx,jsx,js,ts}', 10 | indexHtmlFile: '../support/component-index.html', 11 | devServer(devServerConfig) { 12 | return devServer({ 13 | ...devServerConfig, 14 | framework: 'react', 15 | rspackConfig: require('./rspack.config.js'), 16 | }) 17 | }, 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | 13 | - name: Use Node.js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: 'lts/*' 17 | 18 | - name: Install pnpm 19 | run: npm install -g pnpm 20 | 21 | - name: Install dependencies 22 | run: pnpm install 23 | 24 | - name: Lint 25 | run: pnpm lint 26 | 27 | - name: Typecheck 28 | run: pnpm check-ts 29 | 30 | - name: Test 31 | run: pnpm run test 32 | 33 | - name: Build 34 | run: pnpm run build 35 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tsParser from '@typescript-eslint/parser' 2 | import path from 'node:path' 3 | import { fileURLToPath } from 'node:url' 4 | import js from '@eslint/js' 5 | import { FlatCompat } from '@eslint/eslintrc' 6 | 7 | const __filename = fileURLToPath(import.meta.url) 8 | const __dirname = path.dirname(__filename) 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all, 13 | }) 14 | 15 | export default [ 16 | { 17 | ignores: ['**/dist', '**/node_modules'], 18 | }, 19 | ...compat.extends('eslint-config-prettier'), 20 | { 21 | languageOptions: { 22 | parser: tsParser, 23 | }, 24 | }, 25 | ] 26 | -------------------------------------------------------------------------------- /src/dynamic-import.ts: -------------------------------------------------------------------------------- 1 | import { pathToFileURL } from 'url' 2 | 3 | // tsc will compile `import(...)` calls to require unless a different tsconfig.module value 4 | // is used (e.g. module=node16). To change this, we would also have to change the ts-node behavior when requiring the 5 | // Cypress config file. This hack for keeping dynamic imports from being converted works across all 6 | // of our supported node versions 7 | const _dynamicImport = new Function('specifier', 'return import(specifier)') 8 | 9 | export const dynamicImport = (module: string) => _dynamicImport(module) as Promise 10 | 11 | export const dynamicAbsoluteImport = (filePath: string) => { 12 | return dynamicImport(pathToFileURL(filePath).href) as Promise 13 | } 14 | -------------------------------------------------------------------------------- /test/test-helper/createModuleMatrixResult.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import type { SourceRelativeRspackResult } from '../../src/helpers/sourceRelativeRspackModules' 3 | 4 | const moduleSources = { 5 | rspack: '@rspack/dev-server', 6 | rspackDevServer: '@rspack/dev-server', 7 | } as const 8 | 9 | export function createModuleMatrixResult(): SourceRelativeRspackResult { 10 | return { 11 | framework: null, 12 | rspack: resolveModule('rspack'), 13 | rspackDevServer: resolveModule('rspackDevServer'), 14 | } 15 | } 16 | 17 | function resolveModule(name: K) { 18 | return { 19 | importPath: path.dirname(require.resolve(`${moduleSources[name]}/package.json`)), 20 | packageJson: require(`${moduleSources[name]}/package.json`), 21 | module: require(`${moduleSources[name]}`), 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /dist/dynamic-import.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.dynamicAbsoluteImport = exports.dynamicImport = void 0; 4 | const url_1 = require("url"); 5 | // tsc will compile `import(...)` calls to require unless a different tsconfig.module value 6 | // is used (e.g. module=node16). To change this, we would also have to change the ts-node behavior when requiring the 7 | // Cypress config file. This hack for keeping dynamic imports from being converted works across all 8 | // of our supported node versions 9 | const _dynamicImport = new Function('specifier', 'return import(specifier)'); 10 | const dynamicImport = (module) => _dynamicImport(module); 11 | exports.dynamicImport = dynamicImport; 12 | const dynamicAbsoluteImport = (filePath) => { 13 | return (0, exports.dynamicImport)((0, url_1.pathToFileURL)(filePath).href); 14 | }; 15 | exports.dynamicAbsoluteImport = dynamicAbsoluteImport; 16 | -------------------------------------------------------------------------------- /dist/createRspackDevServer.d.ts: -------------------------------------------------------------------------------- 1 | import type { DevServerConfig } from './devServer'; 2 | import type { SourceRelativeRspackResult } from './helpers/sourceRelativeRspackModules'; 3 | /** 4 | * Takes the rspack / rspackDevServer modules, the configuration provide 5 | * from the framework override (if any), and the configuration provided 6 | * from the user config (if any) and makes the final config we want to 7 | * serve into rspack 8 | */ 9 | export interface CreateFinalRspackConfig { 10 | /** 11 | * Initial config passed to devServer 12 | */ 13 | devServerConfig: DevServerConfig; 14 | /** 15 | * Result of sourcing the rspack from the 16 | */ 17 | sourceRspackModulesResult: SourceRelativeRspackResult; 18 | /** 19 | * Framework-specific config overrides 20 | */ 21 | frameworkConfig?: unknown; 22 | } 23 | export declare function createRspackDevServer(config: CreateFinalRspackConfig): Promise<{ 24 | server: import("@rspack/dev-server").RspackDevServer; 25 | compiler: any; 26 | }>; 27 | -------------------------------------------------------------------------------- /test/__snapshots__/makeDefaultRspackConfig.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`makeCypressRspackConfig should return a valid Configuration object 1`] = ` 4 | [ 5 | HtmlRspackPlugin { 6 | "_args": [ 7 | { 8 | "filename": "index.html", 9 | "template": "path/to/project/path/to/indexHtmlFile", 10 | }, 11 | ], 12 | "affectedHooks": undefined, 13 | "name": "HtmlRspackPlugin", 14 | }, 15 | CypressCTRspackPlugin { 16 | "addCompilationHooks": [Function], 17 | "addLoaderContext": [Function], 18 | "beforeCompile": [Function], 19 | "compilation": null, 20 | "devServerEvents": EventEmitter { 21 | "_events": {}, 22 | "_eventsCount": 0, 23 | "_maxListeners": undefined, 24 | Symbol(shapeMode): false, 25 | Symbol(kCapture): false, 26 | }, 27 | "files": [], 28 | "indexHtmlFile": "path/to/indexHtmlFile", 29 | "onSpecsChange": [Function], 30 | "projectRoot": "path/to/project", 31 | "supportFile": "path/to/supportFile", 32 | }, 33 | ] 34 | `; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Clark Tomlinson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /description.md: -------------------------------------------------------------------------------- 1 | # Cypress Rspack Dev Server 2 | 3 | ## Project Overview 4 | 5 | This project implements a development server for Cypress using Rspack as the bundler. Here's a comprehensive breakdown of its architecture and components: 6 | 7 | ### Core Purpose 8 | - Development server specifically designed for Cypress component testing 9 | - Uses Rspack (Rust-based bundler) as a faster alternative to Webpack 10 | 11 | ### Key Components 12 | 13 | 1. **devServer.ts** 14 | - Main entry point 15 | - Handles server creation and configuration 16 | - Supports multiple frameworks 17 | - Provides flexible configuration system 18 | 19 | 2. **makeRspackConfig.ts** 20 | - Manages Rspack configuration generation 21 | - Handles custom plugin management 22 | - Merges configurations with user settings 23 | - Addresses Cypress-specific requirements 24 | 25 | 3. **CypressCTRspackPlugin.ts** 26 | - Custom Rspack plugin for Cypress Component Testing 27 | 28 | 4. **createRspackDevServer.ts** 29 | - Manages dev server instance creation and setup 30 | 31 | ### Notable Features 32 | - Framework-agnostic design 33 | - Smart configuration merging 34 | - Built-in optimization 35 | - Debug support through 'debug' library 36 | 37 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | // import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18' 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount) 37 | -------------------------------------------------------------------------------- /__snapshots__/makeRspackConfig.spec.ts.js: -------------------------------------------------------------------------------- 1 | exports[ 2 | 'makeRspackConfig ignores userland rspack `output.publicPath` and `devServer.overlay` with rspack-dev-server v4 1' 3 | ] = { 4 | output: { 5 | publicPath: '/test-public-path/', 6 | filename: '[name].js', 7 | }, 8 | devServer: { 9 | magicHtml: true, 10 | client: { 11 | progress: false, 12 | overlay: false, 13 | }, 14 | }, 15 | optimization: { 16 | emitOnErrors: true, 17 | sideEffects: false, 18 | splitChunks: { 19 | chunks: 'all', 20 | }, 21 | }, 22 | devtool: 'inline-source-map', 23 | mode: 'development', 24 | plugins: ['HtmlWebpackPlugin', 'CypressCTWebpackPlugin'], 25 | } 26 | 27 | exports[ 28 | 'makeWebpackConfig ignores userland rspack `output.publicPath` and `devServer.overlay` with rspack-dev-server v3 1' 29 | ] = { 30 | output: { 31 | publicPath: '/test-public-path/', 32 | filename: '[name].js', 33 | }, 34 | devServer: { 35 | progress: true, 36 | overlay: false, 37 | }, 38 | optimization: { 39 | noEmitOnErrors: false, 40 | sideEffects: false, 41 | splitChunks: { 42 | chunks: 'all', 43 | }, 44 | }, 45 | devtool: 'inline-source-map', 46 | mode: 'development', 47 | plugins: ['HtmlWebpackPlugin', 'CypressCTWebpackPlugin'], 48 | } 49 | -------------------------------------------------------------------------------- /dist/createRspackDevServer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.createRspackDevServer = createRspackDevServer; 4 | const makeRspackConfig_1 = require("./makeRspackConfig"); 5 | async function createRspackDevServer(config) { 6 | var _a; 7 | const { sourceRspackModulesResult: { rspack: { module: rspack }, rspackDevServer: { module: RspackDevServer }, }, devServerConfig: { cypressConfig: { devServerPublicPathRoute, isTextTerminal }, }, } = config; 8 | const finalRspackConfig = await (0, makeRspackConfig_1.makeRspackConfig)(config); 9 | const rspackCompiler = rspack(finalRspackConfig); 10 | const isOpenMode = !isTextTerminal; 11 | const rspackDevServerConfig = Object.assign(Object.assign({ host: '127.0.0.1', port: 'auto' }, finalRspackConfig.devServer), { devMiddleware: { 12 | publicPath: devServerPublicPathRoute, 13 | stats: (_a = finalRspackConfig.stats) !== null && _a !== void 0 ? _a : 'minimal', 14 | }, hot: false, 15 | // Only enable file watching & reload when executing tests in `open` mode 16 | liveReload: isOpenMode, client: { overlay: false } }); 17 | const server = new RspackDevServer(rspackDevServerConfig, rspackCompiler); 18 | return { server, compiler: rspackCompiler }; 19 | } 20 | -------------------------------------------------------------------------------- /dist/devServer.d.ts: -------------------------------------------------------------------------------- 1 | import type { EventEmitter } from 'events'; 2 | import type { RspackDevServer } from '@rspack/dev-server'; 3 | import type { Configuration } from '@rspack/core'; 4 | export type Frameworks = Extract['framework']; 7 | type FrameworkConfig = { 8 | framework?: Exclude; 9 | } | { 10 | framework: 'angular'; 11 | options?: { 12 | projectConfig: Cypress.AngularDevServerProjectConfig; 13 | }; 14 | }; 15 | type ConfigHandler = Partial | (() => Partial | Promise>); 16 | export type DevServerConfig = { 17 | specs: Cypress.Spec[]; 18 | cypressConfig: Cypress.PluginConfigOptions; 19 | devServerEvents: EventEmitter; 20 | onConfigNotFound?: (devServer: 'rspack', cwd: string, lookedIn: string[]) => void; 21 | rspackConfig?: ConfigHandler; 22 | } & FrameworkConfig; 23 | /** 24 | * import { RspackDevServer } from '@rspack/dev-server' 25 | * 26 | * Creates & returns a RspackDevServer for serving files related 27 | * to Cypress Component Testing 28 | * 29 | * @param config 30 | */ 31 | export declare function devServer(devServerConfig: DevServerConfig): Promise; 32 | export declare namespace devServer { 33 | var create: (devServerConfig: DevServerConfig) => Promise<{ 34 | server: RspackDevServer; 35 | compiler: any; 36 | }>; 37 | } 38 | export {}; 39 | -------------------------------------------------------------------------------- /dist/helpers/sourceRelativeRspackModules.d.ts: -------------------------------------------------------------------------------- 1 | import Module from 'module'; 2 | import type { DevServerConfig } from '../devServer'; 3 | import type { RspackDevServer } from '@rspack/dev-server'; 4 | export type ModuleClass = typeof Module & { 5 | _load(id: string, parent: Module, isMain: boolean): any; 6 | _resolveFilename(request: string, parent: Module, isMain: boolean, options?: { 7 | paths: string[]; 8 | }): string; 9 | _cache: Record; 10 | }; 11 | export interface PackageJson { 12 | name: string; 13 | version: string; 14 | } 15 | export interface SourcedDependency { 16 | importPath: string; 17 | packageJson: PackageJson; 18 | } 19 | export interface SourcedRspack extends SourcedDependency { 20 | module: Function; 21 | } 22 | export interface SourcedRspackDevServer extends SourcedDependency { 23 | module: { 24 | new (...args: unknown[]): RspackDevServer; 25 | }; 26 | } 27 | export interface SourceRelativeRspackResult { 28 | framework: SourcedDependency | null; 29 | rspack: SourcedRspack; 30 | rspackDevServer: SourcedRspackDevServer; 31 | } 32 | export declare function sourceFramework(config: DevServerConfig): SourcedDependency | null; 33 | export declare function sourceRspack(config: DevServerConfig, framework: SourcedDependency | null): SourcedRspack; 34 | export declare function sourceRspackDevServer(config: DevServerConfig, framework?: SourcedDependency | null): SourcedRspackDevServer; 35 | export declare function sourceDefaultRspackDependencies(config: DevServerConfig): SourceRelativeRspackResult; 36 | export declare function restoreLoadHook(): void; 37 | -------------------------------------------------------------------------------- /src/createRspackDevServer.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug' 2 | import type { Configuration } from '@rspack/dev-server' 3 | import type { DevServerConfig } from './devServer' 4 | import type { SourceRelativeRspackResult } from './helpers/sourceRelativeRspackModules' 5 | import { makeRspackConfig } from './makeRspackConfig' 6 | 7 | /** 8 | * Takes the rspack / rspackDevServer modules, the configuration provide 9 | * from the framework override (if any), and the configuration provided 10 | * from the user config (if any) and makes the final config we want to 11 | * serve into rspack 12 | */ 13 | export interface CreateFinalRspackConfig { 14 | /** 15 | * Initial config passed to devServer 16 | */ 17 | devServerConfig: DevServerConfig 18 | /** 19 | * Result of sourcing the rspack from the 20 | */ 21 | sourceRspackModulesResult: SourceRelativeRspackResult 22 | /** 23 | * Framework-specific config overrides 24 | */ 25 | frameworkConfig?: unknown 26 | } 27 | 28 | export async function createRspackDevServer(config: CreateFinalRspackConfig) { 29 | const { 30 | sourceRspackModulesResult: { 31 | rspack: { module: rspack }, 32 | rspackDevServer: { module: RspackDevServer }, 33 | }, 34 | devServerConfig: { 35 | cypressConfig: { devServerPublicPathRoute, isTextTerminal }, 36 | }, 37 | } = config 38 | 39 | const finalRspackConfig = await makeRspackConfig(config) 40 | const rspackCompiler = rspack(finalRspackConfig) 41 | 42 | const isOpenMode = !isTextTerminal 43 | const rspackDevServerConfig: Configuration = { 44 | host: '127.0.0.1', 45 | port: 'auto', 46 | ...finalRspackConfig.devServer, 47 | devMiddleware: { 48 | publicPath: devServerPublicPathRoute, 49 | stats: finalRspackConfig.stats ?? 'minimal', 50 | }, 51 | hot: false, 52 | // Only enable file watching & reload when executing tests in `open` mode 53 | liveReload: isOpenMode, 54 | client: { overlay: false }, 55 | } 56 | 57 | const server = new RspackDevServer(rspackDevServerConfig, rspackCompiler) 58 | 59 | return { server, compiler: rspackCompiler } 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cypress-rspack-dev-server", 3 | "version": "1.1.1", 4 | "description": "Launches Rspack Dev Server for Component Testing", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "pnpm tsc || echo 'built, with type errors'", 8 | "build-prod": "pnpm build", 9 | "check-ts": "pnpm tsc --noEmit", 10 | "dev": "pnpm with:comment tsc --watch", 11 | "clean": "rimraf dist", 12 | "cypress:run": "cypress run --component --project ./cypress --browser chrome --config-file config/cypress.config.ts", 13 | "cypress:run-with-comment": "pnpm with:comment cypress run --component --project ./cypress --browser chrome --config-file config/cypress.config.ts", 14 | "cypress:open": "cypress open --component --project ./cypress --browser chrome --config-file config/cypress.config.ts", 15 | "with:comment": "cross-env DEBUG=cypress-rspack-dev-server:*", 16 | "lint": "eslint .", 17 | "test": "jest" 18 | }, 19 | "dependencies": { 20 | "@rspack/cli": "1.4.11", 21 | "find-up": "6.3.0", 22 | "local-pkg": "0.4.1", 23 | "tslib": "^2.8.1", 24 | "webpack-merge": "^5.10.0" 25 | }, 26 | "devDependencies": { 27 | "@babel/core": "^7.28.0", 28 | "@babel/preset-env": "^7.28.0", 29 | "@babel/preset-typescript": "^7.27.1", 30 | "@eslint/eslintrc": "^3.3.1", 31 | "@eslint/js": "^9.33.0", 32 | "@jest/globals": "^29.7.0", 33 | "@rspack/core": "1.4.11", 34 | "@rspack/dev-server": "1.1.4", 35 | "@types/debug": "^4.1.12", 36 | "@types/fs-extra": "^11.0.4", 37 | "@types/jest": "^29.5.14", 38 | "@types/lodash": "^4.17.20", 39 | "@typescript-eslint/parser": "^8.39.1", 40 | "babel-jest": "^29.7.0", 41 | "cross-env": "^7.0.3", 42 | "cypress": "^14.5.4", 43 | "debug": "^4.4.1", 44 | "eslint": "^9.33.0", 45 | "eslint-config-prettier": "^9.1.2", 46 | "fs-extra": "9.1.0", 47 | "jest": "^29.7.0", 48 | "lodash": "^4.17.21", 49 | "path": "^0.12.7", 50 | "prettier": "^3.6.2", 51 | "react": "^18.3.1", 52 | "react-dom": "^18.3.1", 53 | "typescript": "^5.9.2", 54 | "webpack": "5.76.0" 55 | }, 56 | "files": [ 57 | "dist" 58 | ], 59 | "license": "MIT", 60 | "repository": { 61 | "type": "git", 62 | "url": "https://github.com/th3fallen/cypress-rspack-dev-server.git" 63 | }, 64 | "publishConfig": { 65 | "access": "public" 66 | }, 67 | "keywords": [ 68 | "rspack", 69 | "cypress", 70 | "dev-server", 71 | "component test" 72 | ], 73 | "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39" 74 | } 75 | -------------------------------------------------------------------------------- /src/makeDefaultRspackConfig.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import debugLib from 'debug' 3 | import { HtmlRspackPlugin, type Configuration } from '@rspack/core' 4 | import type { CreateFinalRspackConfig } from './createRspackDevServer' 5 | import { CypressCTRspackPlugin } from './CypressCTRspackPlugin' 6 | 7 | const debug = debugLib('cypress-rspack-dev-server:makeDefaultRspackConfig') 8 | 9 | const OUTPUT_PATH = path.join(__dirname, 'dist') 10 | 11 | const OsSeparatorRE = RegExp(`\\${path.sep}`, 'g') 12 | const posixSeparator = '/' 13 | 14 | export function makeCypressRspackConfig(config: CreateFinalRspackConfig): Configuration { 15 | const { 16 | devServerConfig: { 17 | cypressConfig: { 18 | justInTimeCompile, 19 | projectRoot, 20 | devServerPublicPathRoute, 21 | supportFile, 22 | indexHtmlFile, 23 | isTextTerminal: isRunMode, 24 | }, 25 | specs: files, 26 | devServerEvents, 27 | }, 28 | } = config 29 | 30 | const optimization: Record = { 31 | // To prevent files from being tree shaken by rspack, we set optimization.sideEffects: false ensuring that 32 | // rspack does not recognize the sideEffects flag in the package.json and thus files are not unintentionally 33 | // dropped during testing in production mode. 34 | sideEffects: false, 35 | splitChunks: { chunks: 'all' }, 36 | } 37 | 38 | const publicPath = 39 | path.sep === posixSeparator 40 | ? path.join(devServerPublicPathRoute, posixSeparator) 41 | : // The second line here replaces backslashes on windows with posix compatible slash 42 | // See https://github.com/cypress-io/cypress/issues/16097 43 | path.join(devServerPublicPathRoute, posixSeparator).replace(OsSeparatorRE, posixSeparator) 44 | 45 | const finalConfig: Configuration = { 46 | mode: 'development', 47 | optimization, 48 | output: { 49 | filename: '[name].[contenthash].js', 50 | path: OUTPUT_PATH, 51 | publicPath, 52 | }, 53 | plugins: [ 54 | new HtmlRspackPlugin({ 55 | template: path.posix.join(projectRoot, indexHtmlFile), 56 | filename: 'index.html', 57 | }), 58 | new CypressCTRspackPlugin({ 59 | files, 60 | projectRoot, 61 | devServerEvents, 62 | supportFile, 63 | indexHtmlFile, 64 | }), 65 | ], 66 | devtool: 'inline-source-map', 67 | } 68 | 69 | if (isRunMode) { 70 | // if justInTimeCompile is configured, we need to watch for file changes 71 | // as the spec entries are going to be updated per test 72 | const ignored = justInTimeCompile ? /node_modules/ : '**/*' 73 | 74 | // Disable file watching when executing tests in `run` mode 75 | finalConfig.watchOptions = { ignored } 76 | } 77 | 78 | return finalConfig 79 | } 80 | -------------------------------------------------------------------------------- /dist/makeDefaultRspackConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.makeCypressRspackConfig = makeCypressRspackConfig; 4 | const tslib_1 = require("tslib"); 5 | const path_1 = tslib_1.__importDefault(require("path")); 6 | const debug_1 = tslib_1.__importDefault(require("debug")); 7 | const core_1 = require("@rspack/core"); 8 | const CypressCTRspackPlugin_1 = require("./CypressCTRspackPlugin"); 9 | const debug = (0, debug_1.default)('cypress-rspack-dev-server:makeDefaultRspackConfig'); 10 | const OUTPUT_PATH = path_1.default.join(__dirname, 'dist'); 11 | const OsSeparatorRE = RegExp(`\\${path_1.default.sep}`, 'g'); 12 | const posixSeparator = '/'; 13 | function makeCypressRspackConfig(config) { 14 | const { devServerConfig: { cypressConfig: { justInTimeCompile, projectRoot, devServerPublicPathRoute, supportFile, indexHtmlFile, isTextTerminal: isRunMode, }, specs: files, devServerEvents, }, } = config; 15 | const optimization = { 16 | // To prevent files from being tree shaken by rspack, we set optimization.sideEffects: false ensuring that 17 | // rspack does not recognize the sideEffects flag in the package.json and thus files are not unintentionally 18 | // dropped during testing in production mode. 19 | sideEffects: false, 20 | splitChunks: { chunks: 'all' }, 21 | }; 22 | const publicPath = path_1.default.sep === posixSeparator 23 | ? path_1.default.join(devServerPublicPathRoute, posixSeparator) 24 | : // The second line here replaces backslashes on windows with posix compatible slash 25 | // See https://github.com/cypress-io/cypress/issues/16097 26 | path_1.default.join(devServerPublicPathRoute, posixSeparator).replace(OsSeparatorRE, posixSeparator); 27 | const finalConfig = { 28 | mode: 'development', 29 | optimization, 30 | output: { 31 | filename: '[name].[contenthash].js', 32 | path: OUTPUT_PATH, 33 | publicPath, 34 | }, 35 | plugins: [ 36 | new core_1.HtmlRspackPlugin({ 37 | template: path_1.default.posix.join(projectRoot, indexHtmlFile), 38 | filename: 'index.html', 39 | }), 40 | new CypressCTRspackPlugin_1.CypressCTRspackPlugin({ 41 | files, 42 | projectRoot, 43 | devServerEvents, 44 | supportFile, 45 | indexHtmlFile, 46 | }), 47 | ], 48 | devtool: 'inline-source-map', 49 | }; 50 | if (isRunMode) { 51 | // if justInTimeCompile is configured, we need to watch for file changes 52 | // as the spec entries are going to be updated per test 53 | const ignored = justInTimeCompile ? /node_modules/ : '**/*'; 54 | // Disable file watching when executing tests in `run` mode 55 | finalConfig.watchOptions = { ignored }; 56 | } 57 | return finalConfig; 58 | } 59 | -------------------------------------------------------------------------------- /test/makeDefaultRspackConfig.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect } from '@jest/globals' 2 | import EventEmitter from 'events' 3 | import { CreateFinalRspackConfig } from '../src/createRspackDevServer' 4 | import { makeCypressRspackConfig } from '../src/makeDefaultRspackConfig' 5 | import { createModuleMatrixResult } from './test-helper/createModuleMatrixResult' 6 | import { makeRspackConfig } from '../src/makeRspackConfig' 7 | 8 | describe('makeCypressRspackConfig', () => { 9 | // Returns a valid Configuration object with mode, optimization, output, plugins and devtool properties 10 | test('should return a valid Configuration object', () => { 11 | const config: CreateFinalRspackConfig = { 12 | devServerConfig: { 13 | cypressConfig: { 14 | projectRoot: 'path/to/project', 15 | devServerPublicPathRoute: '/public', 16 | supportFile: 'path/to/supportFile', 17 | indexHtmlFile: 'path/to/indexHtmlFile', 18 | isTextTerminal: true, 19 | } as Cypress.PluginConfigOptions, 20 | specs: [], 21 | devServerEvents: new EventEmitter(), 22 | }, 23 | sourceRspackModulesResult: createModuleMatrixResult(), 24 | } 25 | 26 | const result = makeCypressRspackConfig(config) 27 | 28 | expect(result.mode).toBe('development') 29 | expect(result.optimization).toEqual({ 30 | sideEffects: false, 31 | splitChunks: { chunks: 'all' }, 32 | }) 33 | expect(result.plugins).toMatchSnapshot() 34 | expect(result.devtool).toBe('inline-source-map') 35 | expect(result.optimization?.sideEffects).toBe(false) 36 | }) 37 | 38 | // test('should replace backslashes with posixSeparator in the publicPath when path.sep is not equal to posixSeparator', () => { 39 | // const originalPathSep = path.sep 40 | // path.sep = '\\' 41 | 42 | // const config: CreateFinalRspackConfig = { 43 | // devServerConfig: { 44 | // cypressConfig: { 45 | // projectRoot: 'path/to/project', 46 | // devServerPublicPathRoute: '/public', 47 | // supportFile: 'path/to/supportFile', 48 | // indexHtmlFile: 'path/to/indexHtmlFile', 49 | // isTextTerminal: true, 50 | // } as Cypress.PluginConfigOptions, 51 | // specs: ['path/to/spec1', 'path/to/spec2'], 52 | // devServerEvents: new EventEmitter(), 53 | // framework: 'react', 54 | // }, 55 | // sourceRspackModulesResult: createModuleMatrixResult(), 56 | // } 57 | 58 | // const result = makeCypressRspackConfig(config) 59 | 60 | // expect(result.output.publicPath).toBe('/public/') 61 | // path.sep = originalPathSep 62 | // }) 63 | }) 64 | 65 | describe('experimentalJustInTimeCompile', () => { 66 | const devServerConfig: CreateFinalRspackConfig['devServerConfig'] = { 67 | specs: [], 68 | cypressConfig: { 69 | projectRoot: '.', 70 | indexHtmlFile: 'path/to/supportFile', 71 | devServerPublicPathRoute: '/test-public-path', 72 | justInTimeCompile: true, 73 | baseUrl: null, 74 | } as Cypress.PluginConfigOptions, 75 | rspackConfig: { 76 | entry: { main: 'src/index.js' }, 77 | }, 78 | devServerEvents: new EventEmitter(), 79 | } 80 | 81 | describe('run mode', () => { 82 | beforeEach(() => { 83 | devServerConfig.cypressConfig.isTextTerminal = true 84 | }) 85 | 86 | it('enables watching', async () => { 87 | const actual = await makeRspackConfig({ 88 | devServerConfig, 89 | sourceRspackModulesResult: createModuleMatrixResult(), 90 | }) 91 | 92 | expect(actual.watchOptions?.ignored).toStrictEqual(/node_modules/) 93 | }) 94 | }) 95 | }) 96 | -------------------------------------------------------------------------------- /dist/devServer.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.devServer = devServer; 4 | const tslib_1 = require("tslib"); 5 | const debug_1 = tslib_1.__importDefault(require("debug")); 6 | const createRspackDevServer_1 = require("./createRspackDevServer"); 7 | const sourceRelativeRspackModules_1 = require("./helpers/sourceRelativeRspackModules"); 8 | const debug = (0, debug_1.default)('cypress-rspack-dev-server:devServer'); 9 | /** 10 | * import { RspackDevServer } from '@rspack/dev-server' 11 | * 12 | * Creates & returns a RspackDevServer for serving files related 13 | * to Cypress Component Testing 14 | * 15 | * @param config 16 | */ 17 | function devServer(devServerConfig) { 18 | return new Promise(async (resolve, reject) => { 19 | const result = (await devServer.create(devServerConfig)); 20 | result.server 21 | .start() 22 | .then(() => { 23 | if (!result.server.options.port) { 24 | return reject(new Error(`Expected port ${result.server.options.port} to be a number`)); 25 | } 26 | debug('Component testing rspack server started on port %s', result.server.options.port); 27 | resolve({ 28 | port: result.server.options.port, 29 | // Close is for unit testing only. We kill this child process which will handle the closing of the server 30 | close: async (done) => { 31 | debug('closing dev server'); 32 | result.server 33 | .stop() 34 | .then(() => done === null || done === void 0 ? void 0 : done()) 35 | .catch(done) 36 | .finally(() => { 37 | debug('closed dev server'); 38 | }); 39 | }, 40 | }); 41 | }) 42 | .catch(reject); 43 | }); 44 | } 45 | const thirdPartyDefinitionPrefixes = { 46 | // matches @org/cypress-ct-* 47 | namespacedPrefixRe: /^@.+?\/cypress-ct-.+/, 48 | globalPrefix: 'cypress-ct-', 49 | }; 50 | function isThirdPartyDefinition(framework) { 51 | return (framework.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) || 52 | thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(framework)); 53 | } 54 | async function getPreset(devServerConfig) { 55 | const defaultRspackModules = () => ({ 56 | sourceRspackModulesResult: (0, sourceRelativeRspackModules_1.sourceDefaultRspackDependencies)(devServerConfig), 57 | }); 58 | // Third party library (eg solid-js, lit, etc) 59 | if (devServerConfig.framework && isThirdPartyDefinition(devServerConfig.framework)) { 60 | return defaultRspackModules(); 61 | } 62 | switch (devServerConfig.framework) { 63 | // todo - add support for other frameworks 64 | case 'next': 65 | // return await nextHandler(devServerConfig) 66 | case 'angular': 67 | // return await angularHandler(devServerConfig) 68 | case 'react': 69 | case 'vue': 70 | case 'svelte': 71 | case undefined: 72 | return defaultRspackModules(); 73 | default: 74 | throw new Error(`Unexpected framework ${devServerConfig.framework}, please visit https://on.cypress.io/component-framework-configuration to see a list of supported frameworks`); 75 | } 76 | } 77 | /** 78 | * Synchronously create the rspack server instance, without starting. 79 | * Useful for testing 80 | * 81 | * @internal 82 | */ 83 | devServer.create = async function (devServerConfig) { 84 | const { frameworkConfig, sourceRspackModulesResult } = await getPreset(devServerConfig); 85 | const { server, compiler } = await (0, createRspackDevServer_1.createRspackDevServer)({ 86 | devServerConfig, 87 | frameworkConfig, 88 | sourceRspackModulesResult, 89 | }); 90 | return { server, compiler }; 91 | }; 92 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "resolveJsonModule": true, 5 | "target": "ES2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */, 6 | "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | "lib": [ 8 | "es2015", 9 | "dom" 10 | ] /* Specify library files to be included in the compilation: */, 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 14 | "declaration": true /* Generates corresponding '.d.ts' file. */, 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "dist" /* Redirect output structure to the directory. */, 18 | // "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true /* Enable all strict type-checking options. */, 27 | // "noImplicitAny": true, 28 | 29 | /* Module Resolution Options */ 30 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 31 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 32 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 33 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 34 | // "typeRoots": [], /* List of folders to include type definitions from. */ 35 | "types": ["cypress"] /* Type declaration files to be included in compilation. */, 36 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 37 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 38 | 39 | /* Source Map Options */ 40 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 41 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 42 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 43 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 44 | 45 | /* Experimental Options */ 46 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 47 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 48 | "esModuleInterop": true, 49 | /** Allows us to strip internal types sourced from rspack */ 50 | "stripInternal": true, 51 | }, 52 | "include": ["src"], 53 | "exclude": ["node_modules", "*.js"] 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cypress-rspack-dev-server 2 | 3 | ![Changelog CI Status](https://github.com/th3fallen/cypress-rspack-dev-server/workflows/Changelog%20CI/badge.svg) 4 | 5 | Based off the amazing work of the cypress team at https://github.com/cypress-io/cypress/blob/develop/npm/webpack-dev-server/ 6 | 7 | Implements the APIs for Cypress Component-testing with Rust-based web bundler [Rspack](https://www.rspack.dev/)'s dev server. 8 | 9 | ## Installation 10 | 11 | Install the library to your devDependencies 12 | 13 | ```bash 14 | npm install -D cypress-rspack-dev-server 15 | ``` 16 | 17 | ## Usage 18 | 19 | ```ts 20 | import { devServer } from "cypress-rspack-dev-server"; 21 | import { defineConfig } from "cypress"; 22 | 23 | export default defineConfig({ 24 | component: { 25 | devServer(devServerConfig) { 26 | return devServer({ 27 | ...devServerConfig, 28 | framework: "react", 29 | rspackConfig: require("./rspack.config.js"), 30 | }); 31 | }, 32 | }, 33 | }); 34 | ``` 35 | 36 | ## Dev server parameters 37 | 38 | | Option | NOTES | 39 | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 40 | | framework | `react`, currently only `react`, will support other frameworks | 41 | | cypressConfig | [Cypress Plugin Configuration](https://github.com/cypress-io/cypress/blob/6766a146dbcad98da2777d4005bc182c9d0475b8/cli/types/cypress.d.ts#L3539) | 42 | | specs | Array of [Cypress Spec](https://github.com/cypress-io/cypress/blob/6766a146dbcad98da2777d4005bc182c9d0475b8/cli/types/cypress.d.ts#L292C19-L292C19) | 43 | | devServerEvents | [Nodejs EventEmitter](https://nodejs.org/en/learn/asynchronous-work/the-nodejs-event-emitter) | 44 | | rspackConfig (Optional) | [Rspack Configuration](https://github.com/web-infra-dev/rspack/blob/12dbc8659f9e9bd16b4bba7ee6135e63364f3975/packages/rspack/src/config/zod.ts#L1180C3-L1180C3), can be `require` from rspack config file | 45 | | onConfigNotFound (Optional) | The callback function when config not found | 46 | 47 | ## Rsbuild Support 48 | 49 | ``` 50 | import config from './rsbuild.config.ts'; 51 | 52 | async devServer(devServerConfig) { 53 | const rsbuild = await createRsbuild({ rsbuildConfig: config }); 54 | const rsbuildConfigs = await rsbuild.initConfigs(); 55 | 56 | const rspackConfig = rsbuildConfigs[0]; 57 | 58 | rspackConfig?.module?.rules?.push({ 59 | test: /\.(ts|tsx)$/, 60 | exclude: /node_modules/, 61 | use: { 62 | loader: 'babel-loader', 63 | options: { 64 | plugins: ['istanbul'], 65 | }, 66 | }, 67 | enforce: 'post', 68 | }); 69 | return devServer({ 70 | ...devServerConfig, 71 | framework: 'react', 72 | rspackConfig: rspackConfig, 73 | }); 74 | } 75 | ``` 76 | 77 | ## Migration to v1 78 | 79 | In version 1, we supports the [Cypress 14 's justInTimeCompile](https://docs.cypress.io/app/references/changelog#14-0-0), the specs structure has been updated. 80 | If you still use Cypress <= 13, please use the version 0.0.x. 81 | 82 | ## License 83 | 84 | [![license](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/cypress-io/cypress/blob/develop/LICENSE) 85 | 86 | This project is licensed under the terms of the [MIT license](/LICENSE). 87 | 88 | ```` 89 | -------------------------------------------------------------------------------- /dist/loader.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /* global Cypress */ 3 | /// 4 | Object.defineProperty(exports, "__esModule", { value: true }); 5 | exports.default = loader; 6 | const tslib_1 = require("tslib"); 7 | const debug_1 = tslib_1.__importDefault(require("debug")); 8 | const path = tslib_1.__importStar(require("path")); 9 | const debug = (0, debug_1.default)('cypress-rspack-dev-server:rspack'); 10 | /** 11 | * @param {ComponentSpec} file spec to create import string from. 12 | * @param {string} filename name of the spec file - this is the same as file.name 13 | * @param {string} chunkName rspack chunk name. eg: 'spec-0' 14 | * @param {string} projectRoot absolute path to the project root. eg: /Users//my-app 15 | */ 16 | const makeImport = (file, filename, chunkName, projectRoot) => { 17 | // If we want to rename the chunks, we can use this 18 | const magicComments = chunkName ? `/* rspackChunkName: "${chunkName}" */` : ''; 19 | return `"${filename}": { 20 | shouldLoad: () => { 21 | const newLoad = new URLSearchParams(document.location.search).get("specPath") === "${file.absolute}"; 22 | const oldLoad = decodeURI(document.location.pathname).includes("${file.absolute}"); 23 | return newLoad || oldLoad; 24 | }, 25 | load: () => import(${magicComments} "${file.absolute}"), 26 | absolute: "${file.absolute.split(path.sep).join(path.posix.sep)}", 27 | relative: "${file.relative.split(path.sep).join(path.posix.sep)}", 28 | relativeUrl: "/__cypress/src/${chunkName}.js", 29 | }`; 30 | }; 31 | /** 32 | * Creates a object mapping a spec file to an object mapping 33 | * the spec name to the result of `makeImport`. 34 | * 35 | * @returns {Record} 36 | * { 37 | * "App.spec.js": { 38 | * shouldLoad: () => { 39 | const newLoad = new URLSearchParams(document.location.search).get("specPath") === "${ 40 | file.absolute 41 | }"; 42 | const oldLoad = decodeURI(document.location.pathname).includes("${file.absolute}"); 43 | return newLoad | oldLoad; 44 | }, 45 | * load: () => { 46 | * return import("/Users/projects/my-app/cypress/component/App.spec.js" \/* rspackChunkName: "spec-0" *\/) 47 | * }, 48 | * chunkName: "spec-0" 49 | * } 50 | * } 51 | */ 52 | function buildSpecs(projectRoot, files = []) { 53 | if (!Array.isArray(files)) 54 | return `{}`; 55 | debug(`projectRoot: ${projectRoot}, files: ${files.map((f) => f.absolute).join(',')}`); 56 | return `{${files 57 | .map((f, i) => { 58 | return makeImport(f, f.name, `spec-${i}`, projectRoot); 59 | }) 60 | .join(',')}}`; 61 | } 62 | // Runs the tests inside the iframe 63 | function loader() { 64 | const ctx = this; 65 | // A spec added after the dev-server is created won't 66 | // be included in the compilation. Disabling the caching of this loader ensures 67 | // we regenerate our specs and include any new ones in the compilation. 68 | ctx.cacheable(false); 69 | const { files, projectRoot, supportFile } = ctx._cypress; 70 | const supportFileAbsolutePath = supportFile 71 | ? JSON.stringify(path.resolve(projectRoot, supportFile)) 72 | : undefined; 73 | const supportFileRelativePath = supportFile 74 | ? JSON.stringify(path.relative(projectRoot, supportFileAbsolutePath || '')) 75 | : undefined; 76 | const result = ` 77 | var allTheSpecs = ${buildSpecs(projectRoot, files)}; 78 | 79 | var { init } = require(${JSON.stringify(require.resolve('./aut-runner'))}) 80 | 81 | var scriptLoaders = Object.values(allTheSpecs).reduce( 82 | (accSpecLoaders, specLoader) => { 83 | if (specLoader.shouldLoad()) { 84 | accSpecLoaders.push(specLoader) 85 | } 86 | return accSpecLoaders 87 | }, []) 88 | 89 | if (${!!supportFile}) { 90 | var supportFile = { 91 | absolute: ${supportFileAbsolutePath}, 92 | relative: ${supportFileRelativePath}, 93 | load: () => import(/* rspackChunkName: "cypress-support-file" */ ${supportFileAbsolutePath}), 94 | } 95 | scriptLoaders.unshift(supportFile) 96 | } 97 | 98 | init(scriptLoaders) 99 | `; 100 | return result; 101 | } 102 | -------------------------------------------------------------------------------- /src/loader.ts: -------------------------------------------------------------------------------- 1 | /* global Cypress */ 2 | /// 3 | 4 | import debugFn from 'debug' 5 | import * as path from 'path' 6 | import type { LoaderContext } from '@rspack/core' 7 | import type { CypressCTRspackContext } from './CypressCTRspackPlugin' 8 | 9 | const debug = debugFn('cypress-rspack-dev-server:rspack') 10 | 11 | /** 12 | * @param {ComponentSpec} file spec to create import string from. 13 | * @param {string} filename name of the spec file - this is the same as file.name 14 | * @param {string} chunkName rspack chunk name. eg: 'spec-0' 15 | * @param {string} projectRoot absolute path to the project root. eg: /Users//my-app 16 | */ 17 | const makeImport = ( 18 | file: Cypress.Cypress['spec'], 19 | filename: string, 20 | chunkName: string, 21 | projectRoot: string, 22 | ) => { 23 | // If we want to rename the chunks, we can use this 24 | const magicComments = chunkName ? `/* rspackChunkName: "${chunkName}" */` : '' 25 | 26 | return `"${filename}": { 27 | shouldLoad: () => { 28 | const newLoad = new URLSearchParams(document.location.search).get("specPath") === "${ 29 | file.absolute 30 | }"; 31 | const oldLoad = decodeURI(document.location.pathname).includes("${file.absolute}"); 32 | return newLoad || oldLoad; 33 | }, 34 | load: () => import(${magicComments} "${file.absolute}"), 35 | absolute: "${file.absolute.split(path.sep).join(path.posix.sep)}", 36 | relative: "${file.relative.split(path.sep).join(path.posix.sep)}", 37 | relativeUrl: "/__cypress/src/${chunkName}.js", 38 | }` 39 | } 40 | 41 | /** 42 | * Creates a object mapping a spec file to an object mapping 43 | * the spec name to the result of `makeImport`. 44 | * 45 | * @returns {Record} 46 | * { 47 | * "App.spec.js": { 48 | * shouldLoad: () => { 49 | const newLoad = new URLSearchParams(document.location.search).get("specPath") === "${ 50 | file.absolute 51 | }"; 52 | const oldLoad = decodeURI(document.location.pathname).includes("${file.absolute}"); 53 | return newLoad | oldLoad; 54 | }, 55 | * load: () => { 56 | * return import("/Users/projects/my-app/cypress/component/App.spec.js" \/* rspackChunkName: "spec-0" *\/) 57 | * }, 58 | * chunkName: "spec-0" 59 | * } 60 | * } 61 | */ 62 | function buildSpecs(projectRoot: string, files: Cypress.Cypress['spec'][] = []): string { 63 | if (!Array.isArray(files)) return `{}` 64 | 65 | debug(`projectRoot: ${projectRoot}, files: ${files.map((f) => f.absolute).join(',')}`) 66 | 67 | return `{${files 68 | .map((f, i) => { 69 | return makeImport(f, f.name, `spec-${i}`, projectRoot) 70 | }) 71 | .join(',')}}` 72 | } 73 | 74 | // Runs the tests inside the iframe 75 | export default function loader(this: unknown) { 76 | const ctx = this as CypressCTRspackContext & LoaderContext 77 | 78 | // A spec added after the dev-server is created won't 79 | // be included in the compilation. Disabling the caching of this loader ensures 80 | // we regenerate our specs and include any new ones in the compilation. 81 | ctx.cacheable(false) 82 | const { files, projectRoot, supportFile } = ctx._cypress 83 | 84 | const supportFileAbsolutePath = supportFile 85 | ? JSON.stringify(path.resolve(projectRoot, supportFile)) 86 | : undefined 87 | const supportFileRelativePath = supportFile 88 | ? JSON.stringify(path.relative(projectRoot, supportFileAbsolutePath || '')) 89 | : undefined 90 | const result = ` 91 | var allTheSpecs = ${buildSpecs(projectRoot, files)}; 92 | 93 | var { init } = require(${JSON.stringify(require.resolve('./aut-runner'))}) 94 | 95 | var scriptLoaders = Object.values(allTheSpecs).reduce( 96 | (accSpecLoaders, specLoader) => { 97 | if (specLoader.shouldLoad()) { 98 | accSpecLoaders.push(specLoader) 99 | } 100 | return accSpecLoaders 101 | }, []) 102 | 103 | if (${!!supportFile}) { 104 | var supportFile = { 105 | absolute: ${supportFileAbsolutePath}, 106 | relative: ${supportFileRelativePath}, 107 | load: () => import(/* rspackChunkName: "cypress-support-file" */ ${supportFileAbsolutePath}), 108 | } 109 | scriptLoaders.unshift(supportFile) 110 | } 111 | 112 | init(scriptLoaders) 113 | ` 114 | 115 | return result 116 | } 117 | -------------------------------------------------------------------------------- /src/devServer.ts: -------------------------------------------------------------------------------- 1 | import debugLib from 'debug' 2 | import type { EventEmitter } from 'events' 3 | import type { RspackDevServer } from '@rspack/dev-server' 4 | import type { Compiler, Configuration } from '@rspack/core' 5 | 6 | import { createRspackDevServer } from './createRspackDevServer' 7 | import { 8 | sourceDefaultRspackDependencies, 9 | SourceRelativeRspackResult, 10 | } from './helpers/sourceRelativeRspackModules' 11 | 12 | const debug = debugLib('cypress-rspack-dev-server:devServer') 13 | 14 | export type Frameworks = Extract< 15 | Cypress.DevServerConfigOptions, 16 | { bundler: 'webpack' } 17 | >['framework'] 18 | 19 | type FrameworkConfig = 20 | | { 21 | framework?: Exclude 22 | } 23 | | { 24 | framework: 'angular' 25 | options?: { 26 | projectConfig: Cypress.AngularDevServerProjectConfig 27 | } 28 | } 29 | 30 | type ConfigHandler = 31 | | Partial 32 | | (() => Partial | Promise>) 33 | 34 | export type DevServerConfig = { 35 | specs: Cypress.Spec[] 36 | cypressConfig: Cypress.PluginConfigOptions 37 | devServerEvents: EventEmitter 38 | onConfigNotFound?: (devServer: 'rspack', cwd: string, lookedIn: string[]) => void 39 | rspackConfig?: ConfigHandler // Derived from the user's rspack config 40 | } & FrameworkConfig 41 | 42 | /** 43 | * @internal 44 | */ 45 | type DevServerCreateResult = { 46 | server: RspackDevServer 47 | compiler: Compiler 48 | } 49 | 50 | /** 51 | * import { RspackDevServer } from '@rspack/dev-server' 52 | * 53 | * Creates & returns a RspackDevServer for serving files related 54 | * to Cypress Component Testing 55 | * 56 | * @param config 57 | */ 58 | export function devServer( 59 | devServerConfig: DevServerConfig, 60 | ): Promise { 61 | return new Promise(async (resolve, reject) => { 62 | const result = (await devServer.create(devServerConfig)) as DevServerCreateResult 63 | 64 | result.server 65 | .start() 66 | .then(() => { 67 | if (!result.server.options.port) { 68 | return reject(new Error(`Expected port ${result.server.options.port} to be a number`)) 69 | } 70 | 71 | debug('Component testing rspack server started on port %s', result.server.options.port) 72 | 73 | resolve({ 74 | port: result.server.options.port as number, 75 | // Close is for unit testing only. We kill this child process which will handle the closing of the server 76 | close: async (done) => { 77 | debug('closing dev server') 78 | result.server 79 | .stop() 80 | .then(() => done?.()) 81 | .catch(done) 82 | .finally(() => { 83 | debug('closed dev server') 84 | }) 85 | }, 86 | }) 87 | }) 88 | .catch(reject) 89 | }) 90 | } 91 | 92 | type PresetHandlerResult = { 93 | frameworkConfig: Configuration 94 | sourceRspackModulesResult: SourceRelativeRspackResult 95 | } 96 | 97 | type Optional = Pick, K> & Omit 98 | 99 | const thirdPartyDefinitionPrefixes = { 100 | // matches @org/cypress-ct-* 101 | namespacedPrefixRe: /^@.+?\/cypress-ct-.+/, 102 | globalPrefix: 'cypress-ct-', 103 | } 104 | 105 | function isThirdPartyDefinition(framework: string) { 106 | return ( 107 | framework.startsWith(thirdPartyDefinitionPrefixes.globalPrefix) || 108 | thirdPartyDefinitionPrefixes.namespacedPrefixRe.test(framework) 109 | ) 110 | } 111 | 112 | async function getPreset( 113 | devServerConfig: DevServerConfig, 114 | ): Promise> { 115 | const defaultRspackModules = () => ({ 116 | sourceRspackModulesResult: sourceDefaultRspackDependencies(devServerConfig), 117 | }) 118 | 119 | // Third party library (eg solid-js, lit, etc) 120 | if (devServerConfig.framework && isThirdPartyDefinition(devServerConfig.framework)) { 121 | return defaultRspackModules() 122 | } 123 | 124 | switch (devServerConfig.framework) { 125 | // todo - add support for other frameworks 126 | case 'next': 127 | // return await nextHandler(devServerConfig) 128 | 129 | case 'angular': 130 | // return await angularHandler(devServerConfig) 131 | 132 | case 'react': 133 | case 'vue': 134 | case 'svelte': 135 | case undefined: 136 | return defaultRspackModules() 137 | 138 | default: 139 | throw new Error( 140 | `Unexpected framework ${ 141 | (devServerConfig as any).framework 142 | }, please visit https://on.cypress.io/component-framework-configuration to see a list of supported frameworks`, 143 | ) 144 | } 145 | } 146 | 147 | /** 148 | * Synchronously create the rspack server instance, without starting. 149 | * Useful for testing 150 | * 151 | * @internal 152 | */ 153 | devServer.create = async function (devServerConfig: DevServerConfig) { 154 | const { frameworkConfig, sourceRspackModulesResult } = await getPreset(devServerConfig) 155 | 156 | const { server, compiler } = await createRspackDevServer({ 157 | devServerConfig, 158 | frameworkConfig, 159 | sourceRspackModulesResult, 160 | }) 161 | 162 | return { server, compiler } 163 | } 164 | -------------------------------------------------------------------------------- /dist/CypressCTRspackPlugin.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CypressCTRspackPlugin = exports.normalizeError = void 0; 4 | const tslib_1 = require("tslib"); 5 | const isEqual_1 = tslib_1.__importDefault(require("lodash/isEqual")); 6 | const fs_extra_1 = tslib_1.__importDefault(require("fs-extra")); 7 | const path_1 = tslib_1.__importDefault(require("path")); 8 | const debug_1 = tslib_1.__importDefault(require("debug")); 9 | const debug = (0, debug_1.default)('cypress-rspack-dev-server:rspackPlugin'); 10 | const normalizeError = (error) => { 11 | return typeof error === 'string' ? error : error.message; 12 | }; 13 | exports.normalizeError = normalizeError; 14 | /** 15 | * A rspack compatible Cypress Component Testing Plugin 16 | * 17 | * @internal 18 | */ 19 | class CypressCTRspackPlugin { 20 | constructor(options) { 21 | this.files = []; 22 | this.compilation = null; 23 | this.addLoaderContext = (loaderContext) => { 24 | ; 25 | loaderContext._cypress = { 26 | files: this.files, 27 | projectRoot: this.projectRoot, 28 | supportFile: this.supportFile, 29 | indexHtmlFile: this.indexHtmlFile, 30 | }; 31 | }; 32 | this.beforeCompile = async (_compilationParams, callback) => { 33 | if (!this.compilation) { 34 | callback(); 35 | return; 36 | } 37 | // Ensure we don't try to load files that have been removed from the file system 38 | // but have not yet been detected by the onSpecsChange handler 39 | const foundFiles = await Promise.all(this.files.map(async (file) => { 40 | try { 41 | const exists = await fs_extra_1.default.pathExists(file.absolute); 42 | return exists ? file : null; 43 | } 44 | catch (e) { 45 | return null; 46 | } 47 | })); 48 | this.files = foundFiles.filter((file) => file !== null); 49 | callback(); 50 | }; 51 | /* 52 | * `rspack --watch` watches the existing specs and their dependencies for changes. 53 | * When new specs are created, we need to trigger a recompilation to add the new specs 54 | * as dependencies. This hook informs rspack that `component-index.html` has been "updated on disk", 55 | * causing a recompilation (and pulling the new specs in as dependencies). We use the component 56 | * index file because we know that it will be there since the project is using Component Testing. 57 | * 58 | * We were using `browser.js` before to cause a recompilation but we ran into an 59 | * issue with MacOS Ventura that will not allow us to write to files inside of our application bundle. 60 | * 61 | * See https://github.com/cypress-io/cypress/issues/24398 62 | */ 63 | this.onSpecsChange = async (specs) => { 64 | var _a; 65 | if (!this.compilation || (0, isEqual_1.default)(specs.specs, this.files)) { 66 | return; 67 | } 68 | this.files = specs.specs; 69 | const inputFileSystem = this.compilation.inputFileSystem; 70 | // TODO: don't use a sync fs method here 71 | const utimesSync = (_a = inputFileSystem.fileSystem.utimesSync) !== null && _a !== void 0 ? _a : fs_extra_1.default.utimesSync; 72 | utimesSync(path_1.default.join(this.projectRoot, this.indexHtmlFile), new Date(), new Date()); 73 | }; 74 | /** 75 | * The rspack compiler generates a new `compilation` each time it compiles, so 76 | * we have to apply hooks to it fresh each time 77 | * 78 | * @param compilation `RspackCompilation` 79 | */ 80 | this.addCompilationHooks = (compilation) => { 81 | this.compilation = compilation; 82 | // still use legacy `webpack` here since in version 0.x.x does not have rspack 83 | const compiler = ('rspack' in this.compilation.compiler 84 | ? this.compilation.compiler.rspack 85 | : this.compilation.compiler.webpack); 86 | const loader = compiler.NormalModule.getCompilationHooks(compilation).loader; 87 | loader.tap('CypressCTPlugin', this.addLoaderContext); 88 | }; 89 | this.files = options.files; 90 | this.supportFile = options.supportFile; 91 | this.projectRoot = options.projectRoot; 92 | this.devServerEvents = options.devServerEvents; 93 | this.indexHtmlFile = options.indexHtmlFile; 94 | } 95 | /** 96 | * The plugin's entrypoint, called once by rspack when the compiler is initialized. 97 | */ 98 | apply(compiler) { 99 | const _compiler = compiler; 100 | this.devServerEvents.on('dev-server:specs:changed', this.onSpecsChange); 101 | _compiler.hooks.beforeCompile.tapAsync('CypressCTPlugin', this.beforeCompile); 102 | _compiler.hooks.compilation.tap('CypressCTPlugin', (compilation) => this.addCompilationHooks(compilation)); 103 | _compiler.hooks.done.tap('CypressCTPlugin', () => { 104 | this.devServerEvents.emit('dev-server:compile:success'); 105 | }); 106 | } 107 | } 108 | exports.CypressCTRspackPlugin = CypressCTRspackPlugin; 109 | -------------------------------------------------------------------------------- /src/makeRspackConfig.ts: -------------------------------------------------------------------------------- 1 | import { debug as debugFn } from 'debug' 2 | import * as path from 'path' 3 | import { merge } from 'webpack-merge' 4 | import { importModule } from 'local-pkg' 5 | import type { Configuration, EntryObject } from '@rspack/core' 6 | import { makeCypressRspackConfig } from './makeDefaultRspackConfig' 7 | import type { CreateFinalRspackConfig } from './createRspackDevServer' 8 | import { configFiles } from './constants' 9 | import { dynamicImport } from './dynamic-import' 10 | 11 | const debug = debugFn('cypress-rspack-dev-server:makeRspackConfig') 12 | 13 | const removeList = [ 14 | // We provide a webpack-html-plugin config pinned to a specific version (4.x) 15 | // that we have tested and are confident works with all common configurations. 16 | // https://github.com/cypress-io/cypress/issues/15865 17 | 'HtmlWebpackPlugin', 18 | 19 | // the rspack's internal html plugin 20 | 'HtmlRspackPlugin', 21 | 22 | // We already reload when rspack recompiles (via listeners on 23 | // devServerEvents). Removing this plugin can prevent double-refreshes 24 | // in some setups. 25 | 'HotModuleReplacementPlugin', 26 | ] 27 | 28 | export const CYPRESS_RSPACK_ENTRYPOINT = path.resolve(__dirname, 'browser.js') 29 | 30 | /** 31 | * Removes and/or modifies certain plugins known to conflict 32 | * when used with @rspack/dev-server. 33 | */ 34 | function modifyRspackConfigForCypress(rspackConfig: Partial) { 35 | if (rspackConfig?.plugins) { 36 | rspackConfig.plugins = rspackConfig.plugins.filter((plugin) => { 37 | if (plugin) { 38 | let pluginName: string = '' 39 | try { 40 | // NOTE: this is to be compatible the old version HtmlRspackPlugin, to get its correct name 41 | // sth changed for HtmlRspackPlugin in 1.0.1 which would cause the error during calling `raw` 42 | pluginName = 43 | 'raw' in plugin ? plugin.raw({ options: { output: {} } }).name : plugin.constructor.name 44 | } catch { 45 | pluginName = plugin.constructor.name 46 | } 47 | return !removeList.includes(pluginName) 48 | } 49 | return false 50 | }) 51 | } 52 | 53 | delete rspackConfig.entry 54 | delete rspackConfig.output 55 | 56 | return rspackConfig 57 | } 58 | 59 | async function getRspackConfigFromProjectRoot(projectRoot: string) { 60 | const { findUp } = await dynamicImport('find-up') 61 | 62 | return await findUp(configFiles, { cwd: projectRoot }) 63 | } 64 | 65 | /** 66 | * Creates a rspack compatible rspack "configuration" to pass to the sourced rspack function 67 | */ 68 | export async function makeRspackConfig(config: CreateFinalRspackConfig): Promise { 69 | let userRspackConfig = config.devServerConfig.rspackConfig 70 | const frameworkRspackConfig = config.frameworkConfig as Partial 71 | const { 72 | cypressConfig: { projectRoot, supportFile }, 73 | specs: files, 74 | framework, 75 | } = config.devServerConfig 76 | 77 | if (!userRspackConfig && !frameworkRspackConfig) { 78 | debug('Not user or framework rspack config received. Trying to automatically source it') 79 | 80 | const configFile = await getRspackConfigFromProjectRoot(projectRoot) 81 | 82 | if (configFile) { 83 | debug('found rspack config %s', configFile) 84 | const sourcedConfig = await importModule(configFile) 85 | 86 | debug('config contains %o', sourcedConfig) 87 | if (sourcedConfig && typeof sourcedConfig === 'object') { 88 | userRspackConfig = sourcedConfig.default ?? sourcedConfig 89 | } 90 | } 91 | 92 | if (!userRspackConfig) { 93 | debug('could not find rspack.config!') 94 | if (config.devServerConfig?.onConfigNotFound) { 95 | config.devServerConfig.onConfigNotFound('rspack', projectRoot, configFiles) 96 | // The config process will be killed from the parent, but we want to early exit so we don't get 97 | // any additional errors related to not having a config 98 | process.exit(0) 99 | } else { 100 | throw new Error( 101 | `Your Cypress devServer config is missing a required rspackConfig property, since we could not automatically detect one.\n 102 | Please add one to your ${config.devServerConfig.cypressConfig.configFile}`, 103 | ) 104 | } 105 | } 106 | } 107 | 108 | userRspackConfig = 109 | typeof userRspackConfig === 'function' ? await userRspackConfig() : userRspackConfig 110 | 111 | const userAndFrameworkRspackConfig = modifyRspackConfigForCypress( 112 | merge(frameworkRspackConfig ?? {}, userRspackConfig ?? {}), 113 | ) 114 | 115 | debug( 116 | `User passed in user and framework rspack config with values %o`, 117 | userAndFrameworkRspackConfig, 118 | ) 119 | debug(`New rspack entries %o`, files) 120 | debug(`Project root`, projectRoot) 121 | debug(`Support file`, supportFile) 122 | 123 | const mergedConfig = merge(userAndFrameworkRspackConfig, makeCypressRspackConfig(config)) 124 | 125 | // Some frameworks (like Next.js) change this value which changes the path we would need to use to fetch our spec. 126 | // (eg, http://localhost:xxxx//static/chunks/spec-.js). Deleting this key to normalize 127 | // the spec URL to `*/spec-.js` which we need to know up-front so we can fetch the sourcemaps. 128 | delete mergedConfig.output?.chunkFilename 129 | 130 | // Angular loads global styles and polyfills via script injection in the index.html 131 | if (framework === 'angular') { 132 | mergedConfig.entry = { 133 | ...(mergedConfig.entry as EntryObject), 134 | 'cypress-entry': CYPRESS_RSPACK_ENTRYPOINT, 135 | } 136 | } else { 137 | mergedConfig.entry = CYPRESS_RSPACK_ENTRYPOINT 138 | } 139 | 140 | debug('Merged rspack config %o', mergedConfig) 141 | 142 | return mergedConfig 143 | } 144 | -------------------------------------------------------------------------------- /src/CypressCTRspackPlugin.ts: -------------------------------------------------------------------------------- 1 | import type { Compilation, Compiler } from '@rspack/core' 2 | import type { EventEmitter } from 'events' 3 | import isEqual from 'lodash/isEqual' 4 | import fs, { PathLike } from 'fs-extra' 5 | import path from 'path' 6 | import debugLib from 'debug' 7 | 8 | const debug = debugLib('cypress-rspack-dev-server:rspackPlugin') 9 | 10 | type UtimesSync = ( 11 | path: PathLike, 12 | atime: string | number | Date, 13 | mtime: string | number | Date, 14 | ) => void 15 | 16 | type CypressSpecsWithOptions = { 17 | specs: Cypress.Cypress['spec'][] 18 | options: { neededForJustInTimeCompile?: boolean } 19 | } 20 | 21 | export interface CypressCTRspackPluginOptions { 22 | files: Cypress.Cypress['spec'][] 23 | projectRoot: string 24 | supportFile: string | false 25 | devServerEvents: EventEmitter 26 | indexHtmlFile: string 27 | } 28 | 29 | export type CypressCTContextOptions = Omit 30 | 31 | export interface CypressCTRspackContext { 32 | _cypress: CypressCTContextOptions 33 | } 34 | 35 | /** 36 | * @internal 37 | */ 38 | export type RspackCompilation = Compilation & { 39 | inputFileSystem: { 40 | fileSystem: { 41 | utimesSync: UtimesSync 42 | } 43 | } 44 | } 45 | 46 | export const normalizeError = (error: Error | string) => { 47 | return typeof error === 'string' ? error : error.message 48 | } 49 | 50 | /** 51 | * A rspack compatible Cypress Component Testing Plugin 52 | * 53 | * @internal 54 | */ 55 | export class CypressCTRspackPlugin { 56 | private files: Cypress.Cypress['spec'][] = [] 57 | private supportFile: string | false 58 | private compilation: RspackCompilation | null = null 59 | private indexHtmlFile: string 60 | 61 | private readonly projectRoot: string 62 | private readonly devServerEvents: EventEmitter 63 | 64 | constructor(options: CypressCTRspackPluginOptions) { 65 | this.files = options.files 66 | this.supportFile = options.supportFile 67 | this.projectRoot = options.projectRoot 68 | this.devServerEvents = options.devServerEvents 69 | this.indexHtmlFile = options.indexHtmlFile 70 | } 71 | 72 | private addLoaderContext = (loaderContext: object) => { 73 | ;(loaderContext as CypressCTRspackContext)._cypress = { 74 | files: this.files, 75 | projectRoot: this.projectRoot, 76 | supportFile: this.supportFile, 77 | indexHtmlFile: this.indexHtmlFile, 78 | } 79 | } 80 | 81 | private beforeCompile = async (_compilationParams: object, callback: Function) => { 82 | if (!this.compilation) { 83 | callback() 84 | 85 | return 86 | } 87 | 88 | // Ensure we don't try to load files that have been removed from the file system 89 | // but have not yet been detected by the onSpecsChange handler 90 | 91 | const foundFiles = await Promise.all( 92 | this.files.map(async (file) => { 93 | try { 94 | const exists = await fs.pathExists(file.absolute) 95 | 96 | return exists ? file : null 97 | } catch (e) { 98 | return null 99 | } 100 | }), 101 | ) 102 | 103 | this.files = foundFiles.filter((file) => file !== null) as Cypress.Spec[] 104 | 105 | callback() 106 | } 107 | 108 | /* 109 | * `rspack --watch` watches the existing specs and their dependencies for changes. 110 | * When new specs are created, we need to trigger a recompilation to add the new specs 111 | * as dependencies. This hook informs rspack that `component-index.html` has been "updated on disk", 112 | * causing a recompilation (and pulling the new specs in as dependencies). We use the component 113 | * index file because we know that it will be there since the project is using Component Testing. 114 | * 115 | * We were using `browser.js` before to cause a recompilation but we ran into an 116 | * issue with MacOS Ventura that will not allow us to write to files inside of our application bundle. 117 | * 118 | * See https://github.com/cypress-io/cypress/issues/24398 119 | */ 120 | private onSpecsChange = async (specs: CypressSpecsWithOptions) => { 121 | if (!this.compilation || isEqual(specs.specs, this.files)) { 122 | return 123 | } 124 | 125 | this.files = specs.specs 126 | const inputFileSystem = this.compilation.inputFileSystem 127 | // TODO: don't use a sync fs method here 128 | const utimesSync: UtimesSync = inputFileSystem.fileSystem.utimesSync ?? fs.utimesSync 129 | 130 | utimesSync(path.join(this.projectRoot, this.indexHtmlFile), new Date(), new Date()) 131 | } 132 | 133 | /** 134 | * The rspack compiler generates a new `compilation` each time it compiles, so 135 | * we have to apply hooks to it fresh each time 136 | * 137 | * @param compilation `RspackCompilation` 138 | */ 139 | private addCompilationHooks = (compilation: RspackCompilation) => { 140 | this.compilation = compilation 141 | 142 | // still use legacy `webpack` here since in version 0.x.x does not have rspack 143 | const compiler = ( 144 | 'rspack' in this.compilation.compiler 145 | ? this.compilation.compiler.rspack 146 | : (this.compilation.compiler as Compiler).webpack 147 | ) as Compiler['webpack'] 148 | 149 | const loader = compiler.NormalModule.getCompilationHooks(compilation).loader 150 | 151 | loader.tap('CypressCTPlugin', this.addLoaderContext) 152 | } 153 | 154 | /** 155 | * The plugin's entrypoint, called once by rspack when the compiler is initialized. 156 | */ 157 | apply(compiler: unknown): void { 158 | const _compiler = compiler as Compiler 159 | 160 | this.devServerEvents.on('dev-server:specs:changed', this.onSpecsChange) 161 | _compiler.hooks.beforeCompile.tapAsync('CypressCTPlugin', this.beforeCompile) 162 | _compiler.hooks.compilation.tap('CypressCTPlugin', (compilation) => 163 | this.addCompilationHooks(compilation as RspackCompilation), 164 | ) 165 | _compiler.hooks.done.tap('CypressCTPlugin', () => { 166 | this.devServerEvents.emit('dev-server:compile:success') 167 | }) 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /dist/makeRspackConfig.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.CYPRESS_RSPACK_ENTRYPOINT = void 0; 4 | exports.makeRspackConfig = makeRspackConfig; 5 | const tslib_1 = require("tslib"); 6 | const debug_1 = require("debug"); 7 | const path = tslib_1.__importStar(require("path")); 8 | const webpack_merge_1 = require("webpack-merge"); 9 | const local_pkg_1 = require("local-pkg"); 10 | const makeDefaultRspackConfig_1 = require("./makeDefaultRspackConfig"); 11 | const constants_1 = require("./constants"); 12 | const dynamic_import_1 = require("./dynamic-import"); 13 | const debug = (0, debug_1.debug)('cypress-rspack-dev-server:makeRspackConfig'); 14 | const removeList = [ 15 | // We provide a webpack-html-plugin config pinned to a specific version (4.x) 16 | // that we have tested and are confident works with all common configurations. 17 | // https://github.com/cypress-io/cypress/issues/15865 18 | 'HtmlWebpackPlugin', 19 | // the rspack's internal html plugin 20 | 'HtmlRspackPlugin', 21 | // We already reload when rspack recompiles (via listeners on 22 | // devServerEvents). Removing this plugin can prevent double-refreshes 23 | // in some setups. 24 | 'HotModuleReplacementPlugin', 25 | ]; 26 | exports.CYPRESS_RSPACK_ENTRYPOINT = path.resolve(__dirname, 'browser.js'); 27 | /** 28 | * Removes and/or modifies certain plugins known to conflict 29 | * when used with @rspack/dev-server. 30 | */ 31 | function modifyRspackConfigForCypress(rspackConfig) { 32 | if (rspackConfig === null || rspackConfig === void 0 ? void 0 : rspackConfig.plugins) { 33 | rspackConfig.plugins = rspackConfig.plugins.filter((plugin) => { 34 | if (plugin) { 35 | let pluginName = ''; 36 | try { 37 | // NOTE: this is to be compatible the old version HtmlRspackPlugin, to get its correct name 38 | // sth changed for HtmlRspackPlugin in 1.0.1 which would cause the error during calling `raw` 39 | pluginName = 40 | 'raw' in plugin ? plugin.raw({ options: { output: {} } }).name : plugin.constructor.name; 41 | } 42 | catch (_a) { 43 | pluginName = plugin.constructor.name; 44 | } 45 | return !removeList.includes(pluginName); 46 | } 47 | return false; 48 | }); 49 | } 50 | delete rspackConfig.entry; 51 | delete rspackConfig.output; 52 | return rspackConfig; 53 | } 54 | async function getRspackConfigFromProjectRoot(projectRoot) { 55 | const { findUp } = await (0, dynamic_import_1.dynamicImport)('find-up'); 56 | return await findUp(constants_1.configFiles, { cwd: projectRoot }); 57 | } 58 | /** 59 | * Creates a rspack compatible rspack "configuration" to pass to the sourced rspack function 60 | */ 61 | async function makeRspackConfig(config) { 62 | var _a, _b, _c; 63 | let userRspackConfig = config.devServerConfig.rspackConfig; 64 | const frameworkRspackConfig = config.frameworkConfig; 65 | const { cypressConfig: { projectRoot, supportFile }, specs: files, framework, } = config.devServerConfig; 66 | if (!userRspackConfig && !frameworkRspackConfig) { 67 | debug('Not user or framework rspack config received. Trying to automatically source it'); 68 | const configFile = await getRspackConfigFromProjectRoot(projectRoot); 69 | if (configFile) { 70 | debug('found rspack config %s', configFile); 71 | const sourcedConfig = await (0, local_pkg_1.importModule)(configFile); 72 | debug('config contains %o', sourcedConfig); 73 | if (sourcedConfig && typeof sourcedConfig === 'object') { 74 | userRspackConfig = (_a = sourcedConfig.default) !== null && _a !== void 0 ? _a : sourcedConfig; 75 | } 76 | } 77 | if (!userRspackConfig) { 78 | debug('could not find rspack.config!'); 79 | if ((_b = config.devServerConfig) === null || _b === void 0 ? void 0 : _b.onConfigNotFound) { 80 | config.devServerConfig.onConfigNotFound('rspack', projectRoot, constants_1.configFiles); 81 | // The config process will be killed from the parent, but we want to early exit so we don't get 82 | // any additional errors related to not having a config 83 | process.exit(0); 84 | } 85 | else { 86 | throw new Error(`Your Cypress devServer config is missing a required rspackConfig property, since we could not automatically detect one.\n 87 | Please add one to your ${config.devServerConfig.cypressConfig.configFile}`); 88 | } 89 | } 90 | } 91 | userRspackConfig = 92 | typeof userRspackConfig === 'function' ? await userRspackConfig() : userRspackConfig; 93 | const userAndFrameworkRspackConfig = modifyRspackConfigForCypress((0, webpack_merge_1.merge)(frameworkRspackConfig !== null && frameworkRspackConfig !== void 0 ? frameworkRspackConfig : {}, userRspackConfig !== null && userRspackConfig !== void 0 ? userRspackConfig : {})); 94 | debug(`User passed in user and framework rspack config with values %o`, userAndFrameworkRspackConfig); 95 | debug(`New rspack entries %o`, files); 96 | debug(`Project root`, projectRoot); 97 | debug(`Support file`, supportFile); 98 | const mergedConfig = (0, webpack_merge_1.merge)(userAndFrameworkRspackConfig, (0, makeDefaultRspackConfig_1.makeCypressRspackConfig)(config)); 99 | // Some frameworks (like Next.js) change this value which changes the path we would need to use to fetch our spec. 100 | // (eg, http://localhost:xxxx//static/chunks/spec-.js). Deleting this key to normalize 101 | // the spec URL to `*/spec-.js` which we need to know up-front so we can fetch the sourcemaps. 102 | (_c = mergedConfig.output) === null || _c === void 0 ? true : delete _c.chunkFilename; 103 | // Angular loads global styles and polyfills via script injection in the index.html 104 | if (framework === 'angular') { 105 | mergedConfig.entry = Object.assign(Object.assign({}, mergedConfig.entry), { 'cypress-entry': exports.CYPRESS_RSPACK_ENTRYPOINT }); 106 | } 107 | else { 108 | mergedConfig.entry = exports.CYPRESS_RSPACK_ENTRYPOINT; 109 | } 110 | debug('Merged rspack config %o', mergedConfig); 111 | return mergedConfig; 112 | } 113 | -------------------------------------------------------------------------------- /dist/helpers/sourceRelativeRspackModules.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | exports.sourceFramework = sourceFramework; 4 | exports.sourceRspack = sourceRspack; 5 | exports.sourceRspackDevServer = sourceRspackDevServer; 6 | exports.sourceDefaultRspackDependencies = sourceDefaultRspackDependencies; 7 | exports.restoreLoadHook = restoreLoadHook; 8 | const tslib_1 = require("tslib"); 9 | const module_1 = tslib_1.__importDefault(require("module")); 10 | const path_1 = tslib_1.__importDefault(require("path")); 11 | const debug_1 = tslib_1.__importDefault(require("debug")); 12 | const debug = (0, debug_1.default)('cypress-rspack-dev-server:sourceRelativeRspackModules'); 13 | const originalModuleLoad = module_1.default._load; 14 | const originalModuleResolveFilename = module_1.default._resolveFilename; 15 | const frameworkRspackMapper = { 16 | react: 'react', 17 | vue: undefined, 18 | next: 'next', 19 | angular: '@angular-devkit/build-angular', 20 | svelte: undefined, 21 | }; 22 | // Source the users framework from the provided projectRoot. The framework, if available, will serve 23 | // as the resolve base for rspack dependency resolution. 24 | function sourceFramework(config) { 25 | debug('Framework: Attempting to source framework for %s', config.cypressConfig.projectRoot); 26 | if (!config.framework) { 27 | debug('Framework: No framework provided'); 28 | return null; 29 | } 30 | const sourceOfRspack = frameworkRspackMapper[config.framework]; 31 | if (!sourceOfRspack) { 32 | debug('Not a higher-order framework so rspack dependencies should be resolvable from projectRoot'); 33 | return null; 34 | } 35 | const framework = {}; 36 | try { 37 | const frameworkJsonPath = require.resolve(`${sourceOfRspack}/package.json`, { 38 | paths: [config.cypressConfig.projectRoot], 39 | }); 40 | const frameworkPathRoot = path_1.default.dirname(frameworkJsonPath); 41 | // Want to make sure we're sourcing this from the user's code. Otherwise we can 42 | // warn and tell them they don't have their dependencies installed 43 | framework.importPath = frameworkPathRoot; 44 | framework.packageJson = require(frameworkJsonPath); 45 | debug('Framework: Successfully sourced framework - %o', framework); 46 | return framework; 47 | } 48 | catch (e) { 49 | debug('Framework: Failed to source framework - %s', e); 50 | // TODO 51 | return null; 52 | } 53 | } 54 | // Source the rspack module from the provided framework or projectRoot. We override the module resolution 55 | // so that other packages that import rspack resolve to the version we found. 56 | function sourceRspack(config, framework) { 57 | var _a; 58 | const searchRoot = (_a = framework === null || framework === void 0 ? void 0 : framework.importPath) !== null && _a !== void 0 ? _a : config.cypressConfig.projectRoot; 59 | debug('Rspack: Attempting to source rspack from %s', searchRoot); 60 | const rspack = {}; 61 | const rspackJsonPath = require.resolve('@rspack/core/package.json', { 62 | paths: [searchRoot], 63 | }); 64 | rspack.importPath = path_1.default.dirname(rspackJsonPath); 65 | rspack.packageJson = require(rspackJsonPath); 66 | rspack.module = require(rspack.importPath).rspack; 67 | debug('Rspack: Successfully sourced rspack - %o', rspack); 68 | module_1.default._load = function (request, parent, isMain) { 69 | if (request === 'rspack' || request.startsWith('rspack/')) { 70 | const resolvePath = require.resolve(request, { 71 | paths: [rspack.importPath], 72 | }); 73 | debug('Rspack: Module._load resolvePath - %s', resolvePath); 74 | return originalModuleLoad(resolvePath, parent, isMain); 75 | } 76 | return originalModuleLoad(request, parent, isMain); 77 | }; 78 | module_1.default._resolveFilename = function (request, parent, isMain, options) { 79 | if (request === 'rspack' || (request.startsWith('rspack/') && !(options === null || options === void 0 ? void 0 : options.paths))) { 80 | const resolveFilename = originalModuleResolveFilename(request, parent, isMain, { 81 | paths: [rspack.importPath], 82 | }); 83 | debug('Rspack: Module._resolveFilename resolveFilename - %s', resolveFilename); 84 | return resolveFilename; 85 | } 86 | return originalModuleResolveFilename(request, parent, isMain, options); 87 | }; 88 | return rspack; 89 | } 90 | // Source the @rspack/dev-server module from the provided framework or projectRoot. 91 | // If none is found, we fallback to the version bundled with this package. 92 | function sourceRspackDevServer(config, framework) { 93 | var _a; 94 | const searchRoot = (_a = framework === null || framework === void 0 ? void 0 : framework.importPath) !== null && _a !== void 0 ? _a : config.cypressConfig.projectRoot; 95 | debug('RspackDevServer: Attempting to source @rspack/dev-server from %s', searchRoot); 96 | const rspackDevServer = {}; 97 | let rspackDevServerJsonPath; 98 | try { 99 | rspackDevServerJsonPath = require.resolve('@rspack/dev-server/package.json', { 100 | paths: [searchRoot], 101 | }); 102 | } 103 | catch (e) { 104 | if (e.code !== 'MODULE_NOT_FOUND') { 105 | debug('RspackDevServer: Failed to source @rspack/dev-server - %s', e); 106 | throw e; 107 | } 108 | debug('RspackDevServer: Falling back to bundled version'); 109 | rspackDevServerJsonPath = require.resolve('@rspack/dev-server/package.json', { 110 | paths: [__dirname], 111 | }); 112 | } 113 | rspackDevServer.importPath = path_1.default.dirname(rspackDevServerJsonPath); 114 | rspackDevServer.packageJson = require(rspackDevServerJsonPath); 115 | rspackDevServer.module = require(rspackDevServer.importPath).RspackDevServer; 116 | debug('RspackDevServer: Successfully sourced @rspack/dev-server - %o', rspackDevServer); 117 | return rspackDevServer; 118 | } 119 | // Most frameworks follow a similar path for sourcing rspack dependencies so this is a utility to handle all the sourcing. 120 | function sourceDefaultRspackDependencies(config) { 121 | const framework = sourceFramework(config); 122 | const rspack = sourceRspack(config, framework); 123 | const rspackDevServer = sourceRspackDevServer(config, framework); 124 | return { framework, rspack, rspackDevServer }; 125 | } 126 | function restoreLoadHook() { 127 | ; 128 | module_1.default._load = originalModuleLoad; 129 | module_1.default._resolveFilename = originalModuleResolveFilename; 130 | } 131 | -------------------------------------------------------------------------------- /src/helpers/sourceRelativeRspackModules.ts: -------------------------------------------------------------------------------- 1 | import Module from 'module' 2 | import path from 'path' 3 | import type { DevServerConfig, Frameworks } from '../devServer' 4 | import debugFn from 'debug' 5 | import type { RspackDevServer } from '@rspack/dev-server' 6 | 7 | const debug = debugFn('cypress-rspack-dev-server:sourceRelativeRspackModules') 8 | 9 | export type ModuleClass = typeof Module & { 10 | _load(id: string, parent: Module, isMain: boolean): any 11 | _resolveFilename( 12 | request: string, 13 | parent: Module, 14 | isMain: boolean, 15 | options?: { paths: string[] }, 16 | ): string 17 | _cache: Record 18 | } 19 | 20 | export interface PackageJson { 21 | name: string 22 | version: string 23 | } 24 | 25 | export interface SourcedDependency { 26 | importPath: string 27 | packageJson: PackageJson 28 | } 29 | 30 | export interface SourcedRspack extends SourcedDependency { 31 | module: Function 32 | } 33 | 34 | export interface SourcedRspackDevServer extends SourcedDependency { 35 | module: { 36 | new (...args: unknown[]): RspackDevServer 37 | } 38 | } 39 | 40 | export interface SourceRelativeRspackResult { 41 | framework: SourcedDependency | null 42 | rspack: SourcedRspack 43 | rspackDevServer: SourcedRspackDevServer 44 | } 45 | 46 | const originalModuleLoad = (Module as ModuleClass)._load 47 | const originalModuleResolveFilename = (Module as ModuleClass)._resolveFilename 48 | 49 | type FrameworkRspackMapper = { [Property in Frameworks]: string | undefined } 50 | 51 | const frameworkRspackMapper: FrameworkRspackMapper = { 52 | react: 'react', 53 | vue: undefined, 54 | next: 'next', 55 | angular: '@angular-devkit/build-angular', 56 | svelte: undefined, 57 | } 58 | 59 | // Source the users framework from the provided projectRoot. The framework, if available, will serve 60 | // as the resolve base for rspack dependency resolution. 61 | export function sourceFramework(config: DevServerConfig): SourcedDependency | null { 62 | debug('Framework: Attempting to source framework for %s', config.cypressConfig.projectRoot) 63 | if (!config.framework) { 64 | debug('Framework: No framework provided') 65 | 66 | return null 67 | } 68 | 69 | const sourceOfRspack = frameworkRspackMapper[config.framework] 70 | 71 | if (!sourceOfRspack) { 72 | debug( 73 | 'Not a higher-order framework so rspack dependencies should be resolvable from projectRoot', 74 | ) 75 | 76 | return null 77 | } 78 | 79 | const framework = {} as SourcedDependency 80 | 81 | try { 82 | const frameworkJsonPath = require.resolve(`${sourceOfRspack}/package.json`, { 83 | paths: [config.cypressConfig.projectRoot], 84 | }) 85 | const frameworkPathRoot = path.dirname(frameworkJsonPath) 86 | 87 | // Want to make sure we're sourcing this from the user's code. Otherwise we can 88 | // warn and tell them they don't have their dependencies installed 89 | framework.importPath = frameworkPathRoot 90 | framework.packageJson = require(frameworkJsonPath) 91 | 92 | debug('Framework: Successfully sourced framework - %o', framework) 93 | 94 | return framework 95 | } catch (e) { 96 | debug('Framework: Failed to source framework - %s', e) 97 | 98 | // TODO 99 | return null 100 | } 101 | } 102 | 103 | // Source the rspack module from the provided framework or projectRoot. We override the module resolution 104 | // so that other packages that import rspack resolve to the version we found. 105 | export function sourceRspack( 106 | config: DevServerConfig, 107 | framework: SourcedDependency | null, 108 | ): SourcedRspack { 109 | const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot 110 | 111 | debug('Rspack: Attempting to source rspack from %s', searchRoot) 112 | 113 | const rspack = {} as SourcedRspack 114 | 115 | const rspackJsonPath: string = require.resolve('@rspack/core/package.json', { 116 | paths: [searchRoot], 117 | }) 118 | 119 | rspack.importPath = path.dirname(rspackJsonPath) 120 | rspack.packageJson = require(rspackJsonPath) 121 | rspack.module = require(rspack.importPath).rspack 122 | 123 | debug('Rspack: Successfully sourced rspack - %o', rspack) 124 | ;(Module as ModuleClass)._load = function (request, parent, isMain) { 125 | if (request === 'rspack' || request.startsWith('rspack/')) { 126 | const resolvePath = require.resolve(request, { 127 | paths: [rspack.importPath], 128 | }) 129 | 130 | debug('Rspack: Module._load resolvePath - %s', resolvePath) 131 | 132 | return originalModuleLoad(resolvePath, parent, isMain) 133 | } 134 | 135 | return originalModuleLoad(request, parent, isMain) 136 | } 137 | ;(Module as ModuleClass)._resolveFilename = function (request, parent, isMain, options) { 138 | if (request === 'rspack' || (request.startsWith('rspack/') && !options?.paths)) { 139 | const resolveFilename = originalModuleResolveFilename(request, parent, isMain, { 140 | paths: [rspack.importPath], 141 | }) 142 | 143 | debug('Rspack: Module._resolveFilename resolveFilename - %s', resolveFilename) 144 | 145 | return resolveFilename 146 | } 147 | 148 | return originalModuleResolveFilename(request, parent, isMain, options) 149 | } 150 | 151 | return rspack 152 | } 153 | 154 | // Source the @rspack/dev-server module from the provided framework or projectRoot. 155 | // If none is found, we fallback to the version bundled with this package. 156 | export function sourceRspackDevServer( 157 | config: DevServerConfig, 158 | framework?: SourcedDependency | null, 159 | ): SourcedRspackDevServer { 160 | const searchRoot = framework?.importPath ?? config.cypressConfig.projectRoot 161 | 162 | debug('RspackDevServer: Attempting to source @rspack/dev-server from %s', searchRoot) 163 | 164 | const rspackDevServer = {} as SourcedRspackDevServer 165 | let rspackDevServerJsonPath: string 166 | 167 | try { 168 | rspackDevServerJsonPath = require.resolve('@rspack/dev-server/package.json', { 169 | paths: [searchRoot], 170 | }) 171 | } catch (e) { 172 | if ((e as { code?: string }).code !== 'MODULE_NOT_FOUND') { 173 | debug('RspackDevServer: Failed to source @rspack/dev-server - %s', e) 174 | throw e 175 | } 176 | 177 | debug('RspackDevServer: Falling back to bundled version') 178 | 179 | rspackDevServerJsonPath = require.resolve('@rspack/dev-server/package.json', { 180 | paths: [__dirname], 181 | }) 182 | } 183 | 184 | rspackDevServer.importPath = path.dirname(rspackDevServerJsonPath) 185 | rspackDevServer.packageJson = require(rspackDevServerJsonPath) 186 | rspackDevServer.module = require(rspackDevServer.importPath).RspackDevServer 187 | 188 | debug('RspackDevServer: Successfully sourced @rspack/dev-server - %o', rspackDevServer) 189 | 190 | return rspackDevServer 191 | } 192 | 193 | // Most frameworks follow a similar path for sourcing rspack dependencies so this is a utility to handle all the sourcing. 194 | export function sourceDefaultRspackDependencies( 195 | config: DevServerConfig, 196 | ): SourceRelativeRspackResult { 197 | const framework = sourceFramework(config) 198 | const rspack = sourceRspack(config, framework) 199 | const rspackDevServer = sourceRspackDevServer(config, framework) 200 | 201 | return { framework, rspack, rspackDevServer } 202 | } 203 | 204 | export function restoreLoadHook() { 205 | ;(Module as ModuleClass)._load = originalModuleLoad 206 | ;(Module as ModuleClass)._resolveFilename = originalModuleResolveFilename 207 | } 208 | --------------------------------------------------------------------------------