diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb74cf47..2aadb45d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing to WebdriverIO Cross-Platform Testing Services +# Contributing to WebdriverIO Desktop & Mobile Testing Services Thank you for your interest in contributing! This document provides guidelines and instructions for contributing to this project. @@ -279,6 +279,16 @@ When contributing to the Electron service: - Test with both Electron Forge and Builder - Test with ESM and CJS configurations +### Tauri Service + +When contributing to the Tauri service: + +- Maintain backward compatibility with tauri-driver +- Test on Windows, macOS, and Linux +- Test with multiremote configurations +- Ensure plugin communication works correctly +- Test with various Tauri configuration patterns + ### Flutter Service When contributing to the Flutter service: diff --git a/README.md b/README.md index 637ae937..789325b7 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,19 @@ -# WebdriverIO Cross-Platform Testing Services +# WebdriverIO Desktop & Mobile Testing Services -> Integration services for cross-platform testing on Electron, Tauri, Flutter, and Neutralino with WebdriverIO +> Specialized WebdriverIO services for testing Electron and Tauri applications ## Overview -This monorepo contains WebdriverIO service packages for testing desktop and mobile applications across multiple frameworks: +This monorepo provides specialized WebdriverIO services for testing desktop and mobile applications across modern frameworks. Our services enable comprehensive end-to-end testing with automatic binary management, API mocking, and seamless integration with WebdriverIO's testing ecosystem. -- **Electron Service** - Test Electron applications with automatic binary detection, CDP bridge for main process access, and comprehensive API mocking -- **Tauri Service** - Test Tauri applications with official tauri-driver integration and multiremote support -- **Flutter Service** - Test Flutter apps on iOS, Android, Windows, macOS, and Linux with Appium integration (coming soon) -- **Neutralino Service** - Test Neutralino.js applications with WebSocket API bridge (coming soon) +### Current Services + +- **Electron Service** - Production-ready testing for Electron applications with automatic binary detection, CDP bridge for main process access, comprehensive API mocking, and window management +- **Tauri Service** - Full-featured testing for Tauri applications with official tauri-driver integration, multiremote support, and plugin-based architecture + +### Future Services + +We aim to add Flutter and Neutralino integrations in the near future to expand cross-platform testing capabilities. ## Quick Start @@ -18,179 +22,187 @@ This monorepo contains WebdriverIO service packages for testing desktop and mobi pnpm install # Build all packages -pnpm turbo build +pnpm build # Run tests -pnpm turbo test +pnpm test # Run linting -pnpm turbo lint +pnpm lint ``` ## Project Structure ``` desktop-mobile-testing/ -├── packages/ # Service packages -│ ├── electron-service/ # @wdio/electron-service -│ ├── tauri-service/ # @wdio/tauri-service -│ ├── electron-cdp-bridge/ # @wdio/electron-cdp-bridge -│ ├── electron-types/ # @wdio/electron-types -│ ├── native-utils/ # @wdio/native-utils -│ └── bundler/ # @wdio/bundler -├── fixtures/ # Test fixtures and example apps -│ ├── e2e-apps/ # E2E test applications -│ └── package-tests/ # Package test applications -├── e2e/ # E2E test scenarios -├── docs/ # Documentation -└── scripts/ # Build and utility scripts +├── packages/ # Service packages +│ ├── electron-service/ # Electron service implementation +│ ├── tauri-service/ # Tauri service implementation +│ ├── electron-cdp-bridge/ # Chrome DevTools Protocol bridge +│ ├── native-utils/ # Cross-platform utilities +│ ├── native-types/ # TypeScript type definitions +│ ├── bundler/ # Build tool for packaging +│ └── tauri-plugin/ # Tauri plugin for backend access +├── fixtures/ # Test fixtures and example apps +│ ├── e2e-apps/ # E2E test applications +│ ├── package-tests/ # Package integration tests +│ └── config-formats/ # Configuration format test fixtures +├── e2e/ # End-to-end test suites +│ ├── test/ # Test specifications +│ │ ├── electron/ # Electron E2E tests +│ │ └── tauri/ # Tauri E2E tests +│ └── scripts/ # Test execution scripts +├── docs/ # Documentation +└── scripts/ # Build and utility scripts ``` -## Packages +## Services ### Electron Service -Test Electron applications with WebdriverIO. +Production-ready WebdriverIO service for testing Electron applications with advanced features. - 📦 **Package**: `@wdio/electron-service` - 📖 **Docs**: [packages/electron-service/README.md](packages/electron-service/README.md) -- ✨ **Features**: Binary detection, CDP bridge, API mocking, window management +- ✨ **Features**: + - Automatic Electron binary detection and management + - CDP bridge for main process API access + - Comprehensive API mocking and stubbing + - Window management and lifecycle control + - Deep link testing support + - Multi-instance testing capabilities ### Tauri Service -Test Tauri applications with WebdriverIO. +Full-featured WebdriverIO service for testing Tauri applications with native integration. - 📦 **Package**: `@wdio/tauri-service` - 📖 **Docs**: [packages/tauri-service/README.md](packages/tauri-service/README.md) -- ✨ **Features**: tauri-driver integration, multiremote support, binary detection - -### Shared Utilities +- ✨ **Features**: + - Official tauri-driver integration + - Multiremote testing support + - Plugin-based architecture + - Automatic binary detection + - Advanced execute capabilities -Common utilities shared across all framework services. +### Supporting Packages -- 📦 **Package**: `@wdio/native-utils` -- 📖 **Docs**: [packages/@wdio/native-utils/README.md](packages/@wdio/native-utils/README.md) +- **@wdio/native-utils** - Cross-platform utilities for binary detection and config parsing +- **@wdio/native-types** - TypeScript type definitions for Electron and Tauri APIs +- **@wdio/electron-cdp-bridge** - Chrome DevTools Protocol bridge for main process communication +- **@wdio/bundler** - Build tool for packaging and bundling service packages +- **@wdio/tauri-plugin** - Tauri plugin providing backend access capabilities for testing ## Development ### Requirements - Node.js 18 LTS or 20 LTS -- pnpm 10.12+ +- pnpm 10.27.0+ ### Setup ```bash -# Install pnpm globally if you don't have it -npm install -g pnpm - # Install dependencies pnpm install # Build all packages -pnpm turbo build +pnpm build ``` ### Commands ```bash # Development -pnpm dev # Watch mode for development pnpm build # Build all packages +pnpm dev # Watch mode for development +pnpm clean # Clean all build artifacts + +# Testing pnpm test # Run all tests +pnpm test:unit # Run unit tests only +pnpm test:integration # Run integration tests pnpm test:coverage # Run tests with coverage +pnpm test:package # Run package integration tests +pnpm test:package:electron # Test Electron package integration +pnpm test:package:tauri # Test Tauri package integration # Code Quality -pnpm lint # Lint all packages -pnpm lint:fix # Lint and auto-fix +pnpm lint # Lint and format check +pnpm lint:fix # Auto-fix linting issues pnpm format # Format code with Biome pnpm typecheck # Type check all packages -# Package-specific commands -pnpm --filter @wdio/native-utils build -pnpm --filter @wdio/electron-service test -pnpm --filter @wdio/tauri-service test - # E2E Testing -pnpm e2e # Run all E2E tests -pnpm e2e:electron-builder # Run Electron builder tests -pnpm e2e:electron-forge # Run Electron forge tests -pnpm e2e:electron-no-binary # Run Electron no-binary tests -pnpm e2e:tauri # Run Tauri tests +pnpm e2e # Run all E2E tests +pnpm e2e:electron-builder # Electron builder E2E tests +pnpm e2e:electron-forge # Electron forge E2E tests +pnpm e2e:electron-no-binary # Electron no-binary E2E tests +pnpm e2e:tauri # Tauri E2E tests +pnpm e2e:standalone # Standalone mode tests +pnpm e2e:multiremote # Multiremote tests + +# Package Management +pnpm release # Release packages +pnpm catalog:update # Update dependency catalogs +pnpm backport # Run backport script ``` -### Adding a New Package - -See [docs/package-structure.md](docs/package-structure.md) for guidelines on creating new packages. - ## Testing -This project maintains 80%+ test coverage across all packages. Tests are organized as: - -- **Unit tests**: Fast, isolated tests for individual modules -- **Integration tests**: Tests for package interactions -- **E2E tests**: End-to-end tests with real applications +### Test Commands ```bash -# Run all tests -pnpm test - -# Run tests for specific package -pnpm --filter @wdio/electron-service test -pnpm --filter @wdio/tauri-service test - -# Run with coverage -pnpm test:coverage - -# Run E2E tests -pnpm e2e # All E2E tests -pnpm e2e:electron-builder # Electron builder E2E -pnpm e2e:electron-forge # Electron forge E2E -pnpm e2e:tauri # Tauri E2E - -# Run package tests (isolated test apps) -pnpm test:package # Both Electron and Tauri -pnpm test:package:electron # Electron only -pnpm test:package:tauri # Tauri only +# Core Testing +pnpm test # Run all unit and integration tests +pnpm test:unit # Unit tests only +pnpm test:integration # Integration tests only +pnpm test:coverage # Run with coverage reporting + +# Package Integration Testing +pnpm test:package # Test published packages in isolation +pnpm test:package:electron # Electron package integration tests +pnpm test:package:tauri # Tauri package integration tests + +# End-to-End Testing +pnpm e2e # All E2E test suites +pnpm e2e:electron-builder # Electron builder applications +pnpm e2e:electron-forge # Electron forge applications +pnpm e2e:electron-no-binary # Electron without pre-built binaries +pnpm e2e:tauri # Tauri applications +pnpm e2e:standalone # Standalone mode testing +pnpm e2e:multiremote # Multiremote browser testing ``` ## Contributing -We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. - -### Development Workflow - -1. Fork and clone the repository -2. Create a new branch: `git checkout -b feature/my-feature` -3. Make your changes -4. Run tests: `pnpm test` -5. Run linting: `pnpm lint:fix` -6. Commit your changes (pre-commit hooks will run automatically) -7. Push and create a pull request +We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines. ## Architecture -This monorepo uses: +This monorepo is built with modern development tools and practices: -- **pnpm workspaces** - Efficient package management and linking -- **Turborepo** - Fast, incremental builds with smart caching -- **TypeScript** - Type-safe development with dual ESM/CJS builds -- **Vitest** - Fast unit and integration testing -- **Biome** - Fast formatting and linting -- **GitHub Actions** - Multi-platform CI/CD +### Tech Stack -See [docs/architecture.md](docs/architecture.md) for more details (coming soon). +- **📦 pnpm workspaces** - Efficient monorepo package management +- **⚡ Turborepo** - Fast, incremental builds with intelligent caching +- **🔷 TypeScript** - Type-safe development with dual ESM/CJS builds +- **🧪 Vitest** - Fast unit and integration testing framework +- **🎨 Biome** - High-performance formatting and linting +- **🤖 GitHub Actions** - Comprehensive CI/CD with multi-platform testing ## License MIT License - see [LICENSE](LICENSE) for details. -## Links +## Community & Support -- [WebdriverIO](https://webdriver.io) -- [Documentation](https://webdriver.io/docs/gettingstarted) -- [WebdriverIO Community](https://github.com/webdriverio-community) +- [WebdriverIO](https://webdriver.io) - Main WebdriverIO project +- [WebdriverIO Docs](https://webdriver.io/docs/gettingstarted) - Official documentation +- [WebdriverIO Community](https://github.com/webdriverio-community) - Community resources +- [GitHub Issues](https://github.com/webdriverio/desktop-mobile-testing/issues) - Bug reports and feature requests ---- +## Related Projects -**Starting commit**: `e728cf1` (chore: add agentos) +- [wdio-electron-service](https://github.com/webdriverio-community/wdio-electron-service) - Legacy Electron service repo +- [tauri-driver](https://github.com/elvis-epx/tauri-driver) - Official Tauri WebDriver implementation diff --git a/fixtures/config-formats/builder-extends-array/base.config.js b/fixtures/config-formats/builder-extends-array/base.config.js new file mode 100644 index 00000000..985d74da --- /dev/null +++ b/fixtures/config-formats/builder-extends-array/base.config.js @@ -0,0 +1,6 @@ +module.exports = { + directories: { + output: 'base-dist', + }, + executableName: 'base-name', +}; diff --git a/fixtures/config-formats/builder-extends-array/electron-builder.config.js b/fixtures/config-formats/builder-extends-array/electron-builder.config.js new file mode 100644 index 00000000..574fc51c --- /dev/null +++ b/fixtures/config-formats/builder-extends-array/electron-builder.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: ['./base.config.js', './override.config.js'], + productName: 'builder-extends-array', +}; diff --git a/fixtures/config-formats/builder-extends-array/override.config.js b/fixtures/config-formats/builder-extends-array/override.config.js new file mode 100644 index 00000000..8a60039c --- /dev/null +++ b/fixtures/config-formats/builder-extends-array/override.config.js @@ -0,0 +1,5 @@ +module.exports = { + directories: { + output: 'override-dist', + }, +}; diff --git a/fixtures/config-formats/builder-extends-array/package.json b/fixtures/config-formats/builder-extends-array/package.json new file mode 100644 index 00000000..d0ed499b --- /dev/null +++ b/fixtures/config-formats/builder-extends-array/package.json @@ -0,0 +1,7 @@ +{ + "name": "builder-extends-array", + "version": "1.0.0", + "devDependencies": { + "electron-builder": "^24.0.0" + } +} diff --git a/fixtures/config-formats/builder-extends-nested/electron-builder.config.js b/fixtures/config-formats/builder-extends-nested/electron-builder.config.js new file mode 100644 index 00000000..e8efc948 --- /dev/null +++ b/fixtures/config-formats/builder-extends-nested/electron-builder.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: './parent.config.js', + productName: 'builder-extends-nested', +}; diff --git a/fixtures/config-formats/builder-extends-nested/grandparent.config.js b/fixtures/config-formats/builder-extends-nested/grandparent.config.js new file mode 100644 index 00000000..4e41efb4 --- /dev/null +++ b/fixtures/config-formats/builder-extends-nested/grandparent.config.js @@ -0,0 +1,5 @@ +module.exports = { + directories: { + output: 'grandparent-dist', + }, +}; diff --git a/fixtures/config-formats/builder-extends-nested/package.json b/fixtures/config-formats/builder-extends-nested/package.json new file mode 100644 index 00000000..8a4972d5 --- /dev/null +++ b/fixtures/config-formats/builder-extends-nested/package.json @@ -0,0 +1,7 @@ +{ + "name": "builder-extends-nested", + "version": "1.0.0", + "devDependencies": { + "electron-builder": "^24.0.0" + } +} diff --git a/fixtures/config-formats/builder-extends-nested/parent.config.js b/fixtures/config-formats/builder-extends-nested/parent.config.js new file mode 100644 index 00000000..9d620ebb --- /dev/null +++ b/fixtures/config-formats/builder-extends-nested/parent.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: './grandparent.config.js', + executableName: 'parent-name', +}; diff --git a/fixtures/config-formats/builder-extends-single/base.config.js b/fixtures/config-formats/builder-extends-single/base.config.js new file mode 100644 index 00000000..ddceb514 --- /dev/null +++ b/fixtures/config-formats/builder-extends-single/base.config.js @@ -0,0 +1,6 @@ +module.exports = { + directories: { + output: 'custom-dist', + }, + executableName: 'base-executable', +}; diff --git a/fixtures/config-formats/builder-extends-single/electron-builder.config.js b/fixtures/config-formats/builder-extends-single/electron-builder.config.js new file mode 100644 index 00000000..b67e43b4 --- /dev/null +++ b/fixtures/config-formats/builder-extends-single/electron-builder.config.js @@ -0,0 +1,4 @@ +module.exports = { + extends: './base.config.js', + productName: 'builder-extends-single', +}; diff --git a/fixtures/config-formats/builder-extends-single/package.json b/fixtures/config-formats/builder-extends-single/package.json new file mode 100644 index 00000000..82d3a878 --- /dev/null +++ b/fixtures/config-formats/builder-extends-single/package.json @@ -0,0 +1,7 @@ +{ + "name": "builder-extends-single", + "version": "1.0.0", + "devDependencies": { + "electron-builder": "^24.0.0" + } +} diff --git a/packages/electron-service/docs/configuration.md b/packages/electron-service/docs/configuration.md index c9458f1c..618f142f 100644 --- a/packages/electron-service/docs/configuration.md +++ b/packages/electron-service/docs/configuration.md @@ -113,6 +113,15 @@ See the [Deeplink Testing guide](./deeplink-testing.md) for complete setup instr Type: `string` +### `electronBuilderConfig`: + +Path to a custom electron-builder configuration file. This is useful when you have multiple configurations (e.g., `electron-builder-staging.config.js`) that extend a common base configuration, and you need to tell the service which one to use for binary path detection. + +When this option is provided, the service will load this specific configuration file and resolve any `extends` chain to determine the output settings. + +Type: `string` +Example: `'config/electron-builder-staging.config.js'` + ### `clearMocks`: Calls .mockClear() on all mocked APIs before each test. This will clear mock history, but not reset its implementation. @@ -368,6 +377,9 @@ If you want to manually set this value, you can specify the [`appBinaryPath`](#a - `package.json` (config values are read from `build`) - `electron-builder.{json,json5,yaml,yml,toml,js,ts,mjs,cjs,mts,cts}` - `electron-builder.config.{json,json5,yaml,yml,toml,js,ts,mjs,cjs,mts,cts}` +- Custom config file specified via [`electronBuilderConfig`](#electronbuilderconfig) + +**Note:** The service supports the [`extends`](https://www.electron.build/configuration/configuration#extends) option in electron-builder configurations. It will recursively resolve and merge extended configurations to determine the final build settings. ##### Electron Forge diff --git a/packages/electron-service/src/launcher.ts b/packages/electron-service/src/launcher.ts index cf33d76f..96e17737 100644 --- a/packages/electron-service/src/launcher.ts +++ b/packages/electron-service/src/launcher.ts @@ -141,6 +141,7 @@ export default class ElectronLaunchService implements Services.ServiceInstance { appEntryPoint, appArgs = ['--no-sandbox'], apparmorAutoInstall: capApparmorAutoInstall, + electronBuilderConfig, } = Object.assign({}, this.#globalOptions, cap[CUSTOM_CAPABILITY_NAME]); // Use capability-level apparmorAutoInstall if provided, otherwise keep the existing value @@ -166,7 +167,7 @@ export default class ElectronLaunchService implements Services.ServiceInstance { // Neither provided - use auto-detection log.info('No app binary specified, attempting to detect one...'); try { - const appBuildInfo = await getAppBuildInfo(pkg); + const appBuildInfo = await getAppBuildInfo(pkg, electronBuilderConfig); try { // Use the detailed binary path function for better error handling diff --git a/packages/native-types/src/electron.ts b/packages/native-types/src/electron.ts index d518c4c5..b0f3f3f7 100644 --- a/packages/native-types/src/electron.ts +++ b/packages/native-types/src/electron.ts @@ -164,6 +164,13 @@ export interface ElectronServiceOptions { * @default false */ apparmorAutoInstall?: boolean | 'sudo'; + /** + * Path to a custom electron-builder configuration file (relative to project root). + * Useful when you have multiple configs (e.g., staging, production) that extend + * a common base config. + * @example 'config/electron-builder-staging.config.js' + */ + electronBuilderConfig?: string; } export type ElectronServiceGlobalOptions = Pick< @@ -178,6 +185,7 @@ export type ElectronServiceGlobalOptions = Pick< | 'logDir' | 'appBinaryPath' | 'appEntryPoint' + | 'electronBuilderConfig' > & { rootDir?: string; /** @@ -210,6 +218,7 @@ export type ElectronType = typeof Electron; export type ElectronInterface = keyof ElectronType; export type BuilderConfig = { + extends?: string | string[] | null; productName?: string; directories?: { output?: string }; executableName?: string; diff --git a/packages/native-utils/package.json b/packages/native-utils/package.json index 6af9088a..2d0158eb 100644 --- a/packages/native-utils/package.json +++ b/packages/native-utils/package.json @@ -41,6 +41,7 @@ "@electron/packager": "^19.0.1", "@wdio/logger": "catalog:default", "debug": "^4.4.3", + "deepmerge-ts": "^7.1.5", "esbuild": "^0.27.2", "find-versions": "^6.0.0", "json5": "^2.2.3", @@ -59,6 +60,10 @@ "vitest": "^4.0.16" }, "files": ["dist", "LICENSE"], + "publishConfig": { + "access": "public", + "provenance": true + }, "repository": { "type": "git", "url": "https://github.com/webdriverio/desktop-mobile-testing.git", diff --git a/packages/native-utils/src/appBuildInfo.ts b/packages/native-utils/src/appBuildInfo.ts index d10ed0c2..8397a5e1 100644 --- a/packages/native-utils/src/appBuildInfo.ts +++ b/packages/native-utils/src/appBuildInfo.ts @@ -17,12 +17,15 @@ const log = createLogger('electron-service', 'config'); * @param pkg normalized package.json * @returns promise resolving to the app build information */ -export async function getAppBuildInfo(pkg: NormalizedReadResult): Promise { +export async function getAppBuildInfo( + pkg: NormalizedReadResult, + electronBuilderConfig?: string, +): Promise { const forgeDependencyDetected = Object.keys(pkg.packageJson.devDependencies || {}).includes('@electron-forge/cli'); const builderDependencyDetected = Object.keys(pkg.packageJson.devDependencies || {}).includes('electron-builder'); const forgeConfig = forgeDependencyDetected ? await getForgeConfig(pkg) : undefined; - const builderConfig = builderDependencyDetected ? await getBuilderConfig(pkg) : undefined; + const builderConfig = builderDependencyDetected ? await getBuilderConfig(pkg, electronBuilderConfig) : undefined; const isForge = typeof forgeConfig !== 'undefined'; const isBuilder = typeof builderConfig !== 'undefined'; diff --git a/packages/native-utils/src/config/builder.ts b/packages/native-utils/src/config/builder.ts index 1626b7d8..09cef236 100644 --- a/packages/native-utils/src/config/builder.ts +++ b/packages/native-utils/src/config/builder.ts @@ -1,5 +1,6 @@ import path from 'node:path'; import type { BuilderBuildInfo, BuilderConfig } from '@wdio/native-types'; +import { deepmerge as deepMerge } from 'deepmerge-ts'; import type { NormalizedReadResult } from 'read-package-up'; import { APP_NAME_DETECTION_ERROR } from '../constants.js'; import { createLogger } from '../log.js'; @@ -7,10 +8,30 @@ import { readConfig } from './read.js'; const log = createLogger('electron-service', 'config'); -export async function getConfig(pkg: NormalizedReadResult): Promise { +export async function getConfig( + pkg: NormalizedReadResult, + customConfigPath?: string, +): Promise { const rootDir = path.dirname(pkg.path); let builderConfig: BuilderConfig = pkg.packageJson.build; - if (!builderConfig) { + let configDir = rootDir; + + // If custom config path provided, use it directly + if (customConfigPath) { + try { + const configPath = path.isAbsolute(customConfigPath) ? customConfigPath : path.join(rootDir, customConfigPath); + log.debug(`Using custom config file: ${configPath}`); + const config = await readConfig(path.basename(configPath), path.dirname(configPath)); + if (!config) { + throw new Error(`Failed to read config file: ${configPath}`); + } + builderConfig = config.result as BuilderConfig; + configDir = path.dirname(configPath); + } catch (e) { + log.error(`Failed to read custom config file: ${customConfigPath}`); + throw e; + } + } else if (!builderConfig) { // if builder config is not found in the package.json, attempt to read `electron-builder.{yaml, yml, json, json5, toml}` // see also https://www.electron.build/configuration.html try { @@ -23,11 +44,20 @@ export async function getConfig(pkg: NormalizedReadResult): Promise = new Set(), +): Promise { + if (!config.extends) { + return config; + } + + const extendsList = Array.isArray(config.extends) ? config.extends : [config.extends]; + let mergedConfig: BuilderConfig = {}; + + for (const extendPath of extendsList) { + // Skip built-in presets (e.g., 'react-cra') or null - we don't need to resolve those + // as electron-builder handles them internally at build time + if (!extendPath || (!extendPath.startsWith('.') && !extendPath.startsWith('/'))) { + log.debug(`Skipping built-in preset or invalid path: ${extendPath}`); + continue; + } + + const resolvedPath = path.resolve(currentDir, extendPath); + + // Detect circular references + if (visited.has(resolvedPath)) { + log.warn(`Circular extends reference detected: ${resolvedPath}`); + continue; + } + visited.add(resolvedPath); + + try { + const extendedResult = await readConfig(path.basename(resolvedPath), path.dirname(resolvedPath)); + if (extendedResult?.result) { + const extendedConfig = extendedResult.result as BuilderConfig; + // Recursively resolve extends in the parent config + const resolvedParent = await resolveExtendsChain(extendedConfig, path.dirname(resolvedPath), visited); + // Merge parent config (earlier configs get overwritten by later ones in the list, + // but here we are iterating extendsList. Usually extends is applied sequentially. + // However, standard intuitive inheritance is: base <- child. + // If extends is an array: [base1, base2]. + // The electron-builder docs say: "The latter allows to mixin a config from multiple other configs, as if you Object.assign them" + // So base2 overrides base1, and child overrides base2. + // We are building 'mergedConfig' which represents the combination of all bases. + mergedConfig = deepMerge(mergedConfig, resolvedParent); + } + } catch (error) { + log.warn(`Failed to resolve extends config at ${resolvedPath}: ${(error as Error).message}`); + } + } + + // The current config takes precedence over extended configs + // Remove the extends property from the merged result + const { extends: _, ...currentWithoutExtends } = config; + return deepMerge(mergedConfig, currentWithoutExtends); +} diff --git a/packages/native-utils/test/config/builder.spec.ts b/packages/native-utils/test/config/builder.spec.ts index 2c25f26b..e0cc8c99 100644 --- a/packages/native-utils/test/config/builder.spec.ts +++ b/packages/native-utils/test/config/builder.spec.ts @@ -103,4 +103,52 @@ describe('getConfig', () => { await expect(() => getConfig(pkg)).rejects.toThrowError(APP_NAME_DETECTION_ERROR); }); }); + + describe('extends support', () => { + it('should resolve single extends config', async () => { + const pkg = await getFixturePackageJson('config-formats', 'builder-extends-single'); + const config = await getConfig(pkg); + + expect(config).toStrictEqual({ + appName: 'builder-extends-single', + config: { + productName: 'builder-extends-single', + directories: { output: 'custom-dist' }, + executableName: 'base-executable', + }, + isBuilder: true, + isForge: false, + }); + }); + + it('should resolve array extends with proper precedence', async () => { + const pkg = await getFixturePackageJson('config-formats', 'builder-extends-array'); + const config = await getConfig(pkg); + + // override.config.js should override base.config.js output dir + // main config productName takes final precedence + expect(config?.config.directories?.output).toBe('override-dist'); + expect(config?.config.executableName).toBe('base-name'); + expect(config?.config.productName).toBe('builder-extends-array'); + }); + + it('should resolve nested extends chains', async () => { + const pkg = await getFixturePackageJson('config-formats', 'builder-extends-nested'); + const config = await getConfig(pkg); + + expect(config?.config.directories?.output).toBe('grandparent-dist'); + expect(config?.config.executableName).toBe('parent-name'); + expect(config?.config.productName).toBe('builder-extends-nested'); + }); + + it('should use custom config path when provided', async () => { + const pkg = await getFixturePackageJson('config-formats', 'builder-extends-single'); + // Pass the complete path to the config file + const customConfigPath = path.join(path.dirname(pkg.path), 'electron-builder.config.js'); + const config = await getConfig(pkg, customConfigPath); + + expect(config?.config.productName).toBe('builder-extends-single'); + expect(config?.config.executableName).toBe('base-executable'); + }); + }); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 76f59d97..4362bd8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -891,6 +891,9 @@ importers: debug: specifier: ^4.4.3 version: 4.4.3(supports-color@8.1.1) + deepmerge-ts: + specifier: ^7.1.5 + version: 7.1.5 esbuild: specifier: ^0.27.2 version: 0.27.2