feat: dioxus service#274
Conversation
Introduce @wdio/native-core as the shared infrastructure package for WebdriverIO native desktop services. Phase 0 of the Dioxus service roadmap; consumed by both @wdio/tauri-service and @wdio/electron-service in subsequent commits. Extracted modules (all from packages/tauri-service/src/ unless noted): - logWriter.ts: file output. Reconciled the Tauri superset (LogWriterContext, prefixedMessage) with Electron's async close(); parametric on serviceName. StandaloneLogWriter et al re-exported as deprecated aliases for Electron compat. - logLevel.ts: shouldLog + LOG_LEVEL_PRIORITY (bit-for-bit identical in both services' logForwarder.ts, so promoted to core). - logCapture.ts: stream-based capture skeleton. Refactored to be callback-driven (onLine) so each service can inject its own parser and forwarder. The Tauri-only forwardLog/parseLogLine imports are gone. - portManager.ts: port allocation. Generic; only the createLogger argument changed. - driverProcess.ts: subprocess lifecycle. Parametric on driverName (defaults to 'driver') so log messages and error strings can say 'tauri-driver' or 'wdio-dioxus-driver'. The forwardLog wiring moved out to an onLogLine callback. startupMarkers/errorMarkers are now caller-supplied instead of hardcoded. - driverPool.ts: lifecycle pool. ensureTauriDriver and getWebKitWebDriverPath calls removed; the caller resolves driver paths first and passes them via DriverStartConfig. Pure lifecycle manager. Intentionally NOT extracted (audit showed these are too framework- specific to unify): - logForwarder.ts: Tauri's [Tauri:Backend] prefix scheme + multiremote instance-ID injection vs Electron's [Electron:MainProcess] plain form. The only shared logic was shouldLog/priority, which is now in logLevel.ts. - logParser.ts: Tauri parses text lines with Rust log conventions; Electron parses CDP Runtime.consoleAPICalled events. Different domains. - diagnostics.ts: Each service's diagnostics file is already a thin wrapper around @wdio/native-utils helpers + framework-specific checks (Tauri checks tauri-driver / WebKit; Electron checks electron / chromium versions). No shared surface beyond what native-utils already provides. The spike/ directory (Phase -1 Dioxus automation API check) is gitignored. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Move validateDeeplinkUrl, getPlatformCommand, and executeDeeplinkCommand out of packages/tauri-service/src/commands/triggerDeeplink.ts into @wdio/native-core/deeplink. These three helpers are framework-agnostic (URL validation, rundll32/open/gio-open invocation) and will be reused by @wdio/dioxus-service's triggerDeeplink command. The service-specific orchestration (browser.execute injection for embedded mode, env-var- driven mode switching) stays in each service's own file. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace packages/tauri-service/src/logWriter.ts with a thin service-name-binding shim around @wdio/native-core's logWriter. Tauri callers keep their zero-arg API (getLogWriter(), closeLogWriter(), isLogWriterInitialized()) — only the close path is now async, since the old sync void close() never actually waited for stream.end() to flush. Test fixups: the two tests that exercised the close path now await closeLogWriter() / writer.close(), and the mocked stream.end() invokes its callback so the underlying promise resolves. All 627 existing Tauri unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…core Replace packages/tauri-service/src/portManager.ts with a re-export shim around @wdio/native-core/portManager (the implementation is identical and now lives in core). Have packages/tauri-service/src/logForwarder.ts import shouldLog from @wdio/native-core/logLevel instead of duplicating the priority table and predicate inline. The function is re-exported under the same name so existing tauri-service callers don't change. All 627 existing Tauri unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…-core Replace packages/electron-service/src/logWriter.ts with a re-export shim around @wdio/native-core. Have packages/electron-service/src/logForwarder.ts import shouldLog from native-core instead of duplicating the priority table inline. Promote the deprecated StandaloneLogWriter alias in native-core from a type alias (const StandaloneLogWriter = LogWriter) to a real subclass that auto-binds the 'electron-service' service name. This keeps the Electron call sites that directly instantiate the class (e.g. `new StandaloneLogWriter()`) and the tests that assert `instanceof StandaloneLogWriter` working unchanged. Also update getStandaloneLogWriter to install the StandaloneLogWriter subclass into the singleton map explicitly so the instanceof identity holds regardless of call order. All 507 existing Electron unit tests pass. All 627 existing Tauri unit tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace the inline implementations of validateDeeplinkUrl, getPlatformCommand, and executeDeeplinkCommand in packages/tauri-service/src/commands/triggerDeeplink.ts with re-exports from @wdio/native-core. Local call sites within the file use the imports directly. Drops the node:child_process spawn dependency from this file. ~110 LOC removed in favour of the shared implementation. All 627 existing Tauri unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce 'external' as the canonical driverProvider value in @wdio/native-types and @wdio/tauri-service. The legacy 'official' value is accepted as a deprecated alias: the launcher's mergeOptions step normalises it to 'external' and emits a one-time deprecation warning. 'official' will be removed in @wdio/tauri-service v2. Rationale: 'official' is honest for Tauri's canonical upstream tauri-driver crate, but ambiguous as we add Dioxus's forked wdio-dioxus-driver. 'external' describes the deployment topology (external driver process vs in-process 'embedded' server) and scales to any future framework. See @wdio/native-core's design notes in packages/native-core/src/driverProcess.ts. User-facing messages and internal defaults updated to suggest 'external'. Type narrowings in service.ts and session.ts widened to accept both. All 627 existing Tauri unit tests pass — no behavioural change. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace packages/tauri-service/src/logCapture.ts's inline readline/marker plumbing with a thin wrapper around @wdio/native-core/logCapture. The Tauri-side LogCaptureOptions shape (with options: TauriServiceOptions) is preserved unchanged so the three existing call sites (driverProcess.ts, crabnebulaBackend.ts, embeddedProvider.ts) don't need updating. The wrapper translates options into core's callback API by binding parseLogLine + forwardLog into an onLine callback, then supplies Tauri's startup/error markers. Tauri's own driverProcess.ts and driverPool.ts continue to use their original implementations. Migrating those to wrappers around core broke 14 unit tests that mock at the local-module boundary (the wrapper delegates spawn() to core, beyond the test's mock reach). Deferred to a follow-up PR that comes with proper test migration — for now, core's versions are framework-agnostic implementations Dioxus will use, and Tauri's local versions remain duplicates of the same logic. All 627 existing Tauri unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add Vitest specs for the three smallest extracted modules: - logLevel.spec.ts (7 tests): LOG_LEVEL_PRIORITY ordering invariants and shouldLog truth table. - deeplink.spec.ts (12 tests): validateDeeplinkUrl (accept custom, reject http/https/file/malformed), getPlatformCommand (per-platform + unsupported), executeDeeplinkCommand (spawn + env forwarding). - portManager.spec.ts: full Tauri-side suite copied verbatim — the implementation moved here so the canonical tests should live here too. Tauri's copy stays in place for the migration window; both pass since Tauri's portManager.ts is a re-export shim around core. 29 specs in @wdio/native-core/test/ all pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace packages/tauri-service/src/driverPool.ts with a Tauri-flavoured wrapper around @wdio/native-core's DriverPool. The wrapper handles the Tauri-specific resolution that core can't do generically: ensureTauriDriver(), getWebKitWebDriverPath(), and the parseLogLine + forwardLog bound onLogLine callback. Tauri call sites in launcher.ts keep their existing API surface. Delete packages/tauri-service/src/driverProcess.ts entirely. Confirmed no external imports — driverPool was the only consumer and now delegates through core. The canonical DriverProcess implementation lives in @wdio/native-core/driverProcess. Update test/driverPool.spec.ts to mock '@wdio/native-core' instead of '../src/driverProcess.js'. The replacement mock is a Map-backed in-memory fake DriverPool that preserves the test's existing mockIsRunning / mockProcPid / mockStop control knobs and supports startDriver / stopDriver / stopAll / getDriver / getDriverProcess / getStatus / getRunningPids semantics. All 22 driverPool spec cases pass. All 627 existing Tauri unit tests pass. All 507 existing Electron unit tests pass. All 29 native-core unit tests pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add @wdio/native-core/baseLauncher exposing the two pieces of shared launcher state every native desktop service needs: a PortManager and a DriverPool. Subclasses (the future DioxusLaunchService, eventually a retrofitted TauriLaunchService) inherit the constructor-time setup of these fields and the protected stopAllDrivers helper. Intentionally minimal in v1 — the priority is giving subclasses a place to inherit shared state from, not designing the full launcher API up front. As @wdio/dioxus-service materialises we'll see which patterns are genuinely shared (vs incidentally similar) and migrate them here. Add four unit tests covering default + custom base ports, empty-pool status, and stopAllDrivers on an empty pool. @wdio/native-core test suite now 33 tests across 4 files (baseLauncher, deeplink, logLevel, portManager). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Release Preview — no release
Updated automatically by ReleaseKit |
Greptile SummaryThis PR introduces
Confidence Score: 3/5Safe to merge for the structural refactoring and deprecation shim, but the deeplink module carries forward a spawn-error swallowing bug that will silently succeed even when the OS handler binary is not found. The packages/native-core/src/deeplink.ts (spawn error swallowing), packages/tauri-service/src/driverPool.ts and packages/tauri-service/src/logCapture.ts (duplicated marker constants)
|
| Filename | Overview |
|---|---|
| packages/native-core/src/deeplink.ts | New shared deeplink module extracted from tauri-service; carries forward a pre-existing resolve-before-error race in executeDeeplinkCommand where process.nextTick resolves the promise before async spawn errors can fire. |
| packages/native-core/src/driverPool.ts | New generic driver lifecycle pool; driverName is not stored in DriverInfo or forwarded through stopDriver/stopAll, causing all stop-phase log messages to read "driver" regardless of the actual service. |
| packages/native-core/src/driverProcess.ts | Generalized from tauri-service with driverName, onLogLine, startupMarkers/errorMarkers as caller-supplied; poll-for-readiness and SIGTERM/SIGKILL shutdown logic transfer cleanly. |
| packages/native-core/src/logCapture.ts | Callback-driven stream capture skeleton; onLine JSDoc incorrectly states "non-empty line" but the implementation passes all lines including blank ones to the callback. |
| packages/native-core/src/logWriter.ts | Reconciled from Tauri and Electron implementations; adds per-service singleton map, async close(), and backward-compat StandaloneLogWriter/getStandaloneLogWriter aliases for Electron. |
| packages/native-core/src/portManager.ts | Extracted unchanged from tauri-service with only the createLogger argument updated; rollback-on-failure in allocatePortPair is preserved correctly. |
| packages/tauri-service/src/driverPool.ts | Tauri wrapper around core DriverPool; TAURI_STARTUP_MARKERS and TAURI_ERROR_MARKERS are duplicated here and in logCapture.ts, creating a maintenance hazard if markers need to change. |
| packages/tauri-service/src/logCapture.ts | Tauri wrapper around core createLogCapture; markers duplicated from driverPool.ts and the embedded log-capture path still works correctly. |
| packages/tauri-service/src/logWriter.ts | Thin shim delegating to native-core; closeLogWriter correctly changed to async to match native-core's stream-flush semantics, test updated accordingly. |
| packages/tauri-service/src/launcher.ts | Adds normaliseDriverProvider() to emit a one-shot deprecation warning and translate 'official' → 'external' before options reach ensureTauriDriver; module-level flag ensures single warning emission. |
| packages/native-types/src/shared.ts | Adds 'external' to DriverProviderConfig union and updates default/docs; 'official' retained as deprecated alias for one release cycle. |
| packages/native-core/src/baseLauncher.ts | New minimal abstract base class providing shared PortManager and DriverPool state for native desktop service launchers; intentionally thin as noted in the comments. |
Sequence Diagram
sequenceDiagram
participant Launcher as TauriLaunchService
participant TauriPool as tauri-service/DriverPool
participant CorePool as native-core/DriverPool
participant DP as native-core/DriverProcess
participant LC as native-core/createLogCapture
participant Parser as tauri-service/parseLogLine
participant Fwd as tauri-service/forwardLog
Launcher->>TauriPool: startDriver(config)
TauriPool->>TauriPool: ensureTauriDriver(serviceOptions)
TauriPool->>TauriPool: getWebKitWebDriverPath()
TauriPool->>CorePool: startDriver(config + driverPath + onLogLine + markers)
CorePool->>DP: start(DriverStartOptions)
DP->>LC: "createLogCapture({stream, startupMarkers, errorMarkers, onLine})"
LC-->>DP: ReadlineInterface
DP-->>CorePool: DriverProcessInfo
CorePool-->>TauriPool: DriverInfo
TauriPool-->>Launcher: DriverInfo
Note over LC,Fwd: Per log line from driver stdout/stderr
LC->>TauriPool: onLogLine(line, instanceId)
TauriPool->>Parser: parseLogLine(line)
Parser-->>TauriPool: "ParsedLogLine | null"
TauriPool->>Fwd: forwardLog(source, level, message, minLevel, prefixedMessage, instanceId)
Comments Outside Diff (2)
-
packages/native-core/src/deeplink.ts, line 418-449 (link)Async spawn error silently swallowed after
nextTickresolveprocess.nextTick(() => resolve())runs before libuv I/O events (including the child processerrorevent). Ifspawnreturns a process object but the binary is not found on PATH (ENOENT), theerrorevent fires after the promise has already resolved — therejectcall in theerrorhandler becomes a no-op. Callers receive a fulfilled promise even though the deeplink command never executed. This bug was present in the originaltauri-servicecode but now affects every future consumer of this shared module.A reliable fix is to listen for the
errorevent synchronously beforeunref()and only resolve oncespawnhas had a tick to surface synchronous-style errors, or use a short-settled race. At minimum, consider removing thenextTickwrapper and resolving immediately — the existingerrorhandler would then have a chance to reject first if the event fires on the next tick. -
packages/native-core/src/driverPool.ts, line 541-572 (link)driverNamenot propagated throughDriverPool— stop-phase logs always read "driver"DriverPool.stopDriver()andstopAll()callinfo.process.stop()without the driver name.DriverProcess.stop(driverName = 'driver')defaults to'driver', so all shutdown log messages ("Stopping driver...","driver process exited","driver did not stop gracefully...") lose the service-specific label that was used during startup. NeitherDriverInfonorDriverPoolstores thedriverNamefromDriverStartConfig. AddingdriverNametoDriverInfoand forwarding it instopDriver/stopAllwould keep the diagnostic context consistent across the lifecycle.
Reviews (1): Last reviewed commit: "feat(native-core): introduce BaseLaunche..." | Re-trigger Greptile
| /** | ||
| * Called for every non-empty line consumed from the stream. The service | ||
| * is responsible for parsing, filtering, and forwarding. Receives the | ||
| * raw line and the `instanceId` (if any) so the service's forwarder can | ||
| * attach multiremote context. | ||
| */ | ||
| onLine: (line: string, instanceId: string | undefined) => void; |
There was a problem hiding this comment.
The JSDoc says "Called for every non-empty line" but the implementation calls
onLine for all lines from the readline interface, including blank ones. Tauri's wrapper handles this because parseLogLine returns null for blank lines, but new consumers of this API (e.g. the Dioxus service) may be surprised that empty lines are forwarded. Either filter empty lines in createLogCapture or correct the JSDoc.
| /** | |
| * Called for every non-empty line consumed from the stream. The service | |
| * is responsible for parsing, filtering, and forwarding. Receives the | |
| * raw line and the `instanceId` (if any) so the service's forwarder can | |
| * attach multiremote context. | |
| */ | |
| onLine: (line: string, instanceId: string | undefined) => void; | |
| /** | |
| * Called for every line consumed from the stream (including blank lines). | |
| * The service is responsible for parsing, filtering, and forwarding. | |
| * Receives the raw line and the `instanceId` (if any) so the service's | |
| * forwarder can attach multiremote context. | |
| */ | |
| onLine: (line: string, instanceId: string | undefined) => void; |
|
|
||
| const TAURI_STARTUP_MARKERS = ['tauri-driver started', 'listening on'] as const; |
There was a problem hiding this comment.
Duplicated startup/error marker constants
TAURI_STARTUP_MARKERS and TAURI_ERROR_MARKERS are defined identically in both packages/tauri-service/src/driverPool.ts (used for the external driver via core.startDriver) and packages/tauri-service/src/logCapture.ts (used for embedded/CrabNebula mode via the Tauri wrapper). If a marker string needs to change, both sites must be updated in sync. These could be exported from a single shared location (e.g. a constants.ts within tauri-service) to eliminate the duplication.
Introduce @wdio/native-core as the shared infrastructure package for
WebdriverIO native desktop services. Phase 0 of the Dioxus service
roadmap; consumed by both @wdio/tauri-service and @wdio/electron-service
in subsequent commits.
Extracted modules (all from packages/tauri-service/src/ unless noted):
(LogWriterContext, prefixedMessage) with Electron's async close();
parametric on serviceName. StandaloneLogWriter et al re-exported as
deprecated aliases for Electron compat.
in both services' logForwarder.ts, so promoted to core).
callback-driven (onLine) so each service can inject its own parser
and forwarder. The Tauri-only forwardLog/parseLogLine imports are
gone.
argument changed.
(defaults to 'driver') so log messages and error strings can say
'tauri-driver' or 'wdio-dioxus-driver'. The forwardLog wiring moved
out to an onLogLine callback. startupMarkers/errorMarkers are now
caller-supplied instead of hardcoded.
getWebKitWebDriverPath calls removed; the caller resolves driver
paths first and passes them via DriverStartConfig. Pure lifecycle
manager.
Intentionally NOT extracted (audit showed these are too framework-
specific to unify):
instance-ID injection vs Electron's [Electron:MainProcess] plain
form. The only shared logic was shouldLog/priority, which is now
in logLevel.ts.
Electron parses CDP Runtime.consoleAPICalled events. Different
domains.
wrapper around @wdio/native-utils helpers + framework-specific
checks (Tauri checks tauri-driver / WebKit; Electron checks
electron / chromium versions). No shared surface beyond what
native-utils already provides.
The spike/ directory (Phase -1 Dioxus automation API check) is
gitignored.
Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com