Skip to content

feat: dioxus service#274

Closed
goosewobbler wants to merge 11 commits into
mainfrom
feat/dioxus-foundation
Closed

feat: dioxus service#274
goosewobbler wants to merge 11 commits into
mainfrom
feat/dioxus-foundation

Conversation

@goosewobbler
Copy link
Copy Markdown
Contributor

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

goosewobbler and others added 11 commits May 11, 2026 16:27
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>
@goosewobbler goosewobbler changed the title feat: dioxus support feat: dioxus foundation May 11, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Release Preview — no release

No bump label detected.
Reason: No release labels found (need bump:* or release:stable)
Note: Add bump:patch, bump:minor, or bump:major to trigger a release.


Updated automatically by ReleaseKit

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR introduces @wdio/native-core as a shared infrastructure package for WebdriverIO native desktop services, extracting common modules (log writer, log level, log capture, port manager, driver process, driver pool, base launcher, and deeplink helpers) from tauri-service and electron-service so they can be consumed by a future @wdio/dioxus-service. It also renames the driverProvider: 'official' value to 'external' with a one-cycle deprecation shim.

  • @wdio/native-core package: Seven modules extracted/generalized from tauri-service/electron-service; Tauri and Electron services are updated to thin wrapper shims that delegate to core and re-export the previous names for backward compatibility.
  • driverProvider rename: 'official''external' across native-types, tauri-service types, and user-facing docs; a normaliseDriverProvider() helper in launcher.ts emits a one-shot deprecation warning and converts the legacy value at merge time.
  • Log capture refactor: createLogCapture becomes callback-driven (onLine, startupMarkers, errorMarkers) so each service injects its own parser and forwarder without core needing any knowledge of service-specific log formats.

Confidence Score: 3/5

Safe 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 executeDeeplinkCommand function resolves its promise via process.nextTick before libuv can deliver an async error event (e.g. ENOENT). This was a pre-existing issue in tauri-service, but it now ships as shared infrastructure consumed by the Dioxus service and any future consumer — so a silent spawn failure will affect all three services with no caller-visible signal. All other changes are well-structured extractions with correct backward-compat shims.

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)

Important Files Changed

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)
Loading

Comments Outside Diff (2)

  1. packages/native-core/src/deeplink.ts, line 418-449 (link)

    P1 Async spawn error silently swallowed after nextTick resolve

    process.nextTick(() => resolve()) runs before libuv I/O events (including the child process error event). If spawn returns a process object but the binary is not found on PATH (ENOENT), the error event fires after the promise has already resolved — the reject call in the error handler becomes a no-op. Callers receive a fulfilled promise even though the deeplink command never executed. This bug was present in the original tauri-service code but now affects every future consumer of this shared module.

    A reliable fix is to listen for the error event synchronously before unref() and only resolve once spawn has had a tick to surface synchronous-style errors, or use a short-settled race. At minimum, consider removing the nextTick wrapper and resolving immediately — the existing error handler would then have a chance to reject first if the event fires on the next tick.

    Fix in Claude Code Fix in Cursor

  2. packages/native-core/src/driverPool.ts, line 541-572 (link)

    P2 driverName not propagated through DriverPool — stop-phase logs always read "driver"

    DriverPool.stopDriver() and stopAll() call info.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. Neither DriverInfo nor DriverPool stores the driverName from DriverStartConfig. Adding driverName to DriverInfo and forwarding it in stopDriver/stopAll would keep the diagnostic context consistent across the lifecycle.

    Fix in Claude Code Fix in Cursor

Fix All in Claude Code Fix All in Cursor

Reviews (1): Last reviewed commit: "feat(native-core): introduce BaseLaunche..." | Re-trigger Greptile

Comment on lines +47 to +53
/**
* 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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
/**
* 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;

Fix in Claude Code Fix in Cursor

Comment on lines 30 to +31

const TAURI_STARTUP_MARKERS = ['tauri-driver started', 'listening on'] as const;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Fix in Claude Code Fix in Cursor

@goosewobbler goosewobbler changed the title feat: dioxus foundation feat: dioxus service May 11, 2026
@goosewobbler goosewobbler deleted the feat/dioxus-foundation branch May 11, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant