Cory Policht
← Back to the Devlog
DEVLOG · 2026-07-09 .. 2026-07-12 · RELEASE

ADP UI and client extract as standalone packages.

The Orchestration UI splits apart: the typed API client and reusable component surface are carved out into standalone @adp/client and @adp/ui packages at v0.2.0, surfacing subtle Vite vs. strict Node ESM boundary mismatches.

WINDOW
Start2026-07-09
End2026-07-12
Kindrelease
Projectsadp-ui, adp-client
Delivered
  • adp-client
    Standalone client extraction and strict Node ESM compatibility
    Extracts @adp/client into an independent library, introducing post-build extension rewriting and safe environment readers so consumer projects can import it outside Vite.
    adp-client · 605ec5eadp-client · v0.2.0adp-client · v0.2.1
  • adp-client
    Explicit redirect URI plumbing through Keycloak login flows
    Passes redirectUri explicitly through the authentication client to eliminate post-login redirect loops (IP-E12-01) and cuts v0.3.0.
    adp-client · #1adp-client · 02b93e1adp-client · v0.3.0
  • adp-ui
    Standalone UI package extraction and platform dependency resolution
    Extracts @adp/ui as an independent component library consuming @adp/client, resolves optional platform binary dependencies in package-lock.json, and tags v0.2.0.
    adp-ui · 146c6ecadp-ui · 4a6dc7badp-ui · v0.2.0

When the orchestration interface was first built, the API client, state stores, and UI views all lived inside ADP-Orchestration-UI. That kept iteration fast early on, but as other tools—such as IDE extensions and test harnesses—needed to talk to the orchestration backend, keeping client logic bundled into a single web app became a bottleneck.

During this window, that monolith was cleanly separated. The core API client was extracted into @adp/client, while the visual components were separated into @adp/ui, each published as standalone packages at v0.2.0.

Extracting code out of an established SPA into an npm package immediately exposed hidden bundler assumptions. The original codebase was built entirely within Vite. When @adp/[email protected] was first installed into downstream consumers running standard Node test runners, it failed in two distinct ways (605ec5e):

  1. Top-level bundler globals: Multiple modules read import.meta.env.VITE_* at the root level for API URLs and Keycloak settings. In standard Node ESM or externalized Vitest execution, import.meta.env is undefined, causing an immediate runtime crash on package import. This was solved by routing all lookups through a defensive readViteEnv() wrapper that returns undefined outside Vite, allowing normal nullish-coalescing fallbacks to operate.
  2. Missing ESM file extensions: With "moduleResolution": "bundler", TypeScript emits relative import paths without .js extensions. Vite resolves these effortlessly, but strict Node ESM refuses to load extensionless specifiers. A dedicated post-build step (scripts/fix-esm-extensions.mjs) was added to rewrite relative imports in dist/ to include explicit .js extensions, verified directly via npm pack and strict Node imports.

Following the initial packaging fix, the client tackled authentication edge cases. PR #1 addressed a persistent post-login redirect loop in Keycloak (IP-E12-01). Rather than relying on ambient window location during OAuth handshakes, the client now requires and preserves an explicit redirectUri through all login and token exchange flows, bumping @adp/client to v0.3.0.

Meanwhile, @adp/ui completed its separation (146c6ec), resolved missing optional platform binary entries in its lockfile (4a6dc7b), and successfully cut v0.2.0 backed by the corrected client library.

Lessons learned

  • Client SDKs extracted from bundlers must not assume bundler runtime globals. Code written inside Vite frequently references import.meta.env without a second thought. Once published as an npm package consumed by server scripts or Node CLI tools, those bare accesses throw TypeError. Guard environment reads with defensive helper functions.
  • TypeScript bundler module resolution does not generate valid Node ESM specifiers. Relying on "moduleResolution": "bundler" means tsc emits extensionless import specifiers that break outside bundlers. If a library needs to run under native Node ESM, run an explicit post-processing step to append .js extensions to emitted files.
  • Pass redirect targets explicitly through authentication client abstractions. Implicitly guessing post-auth redirect paths based on browser window state breaks down across different hosting routes. Threading redirectUri explicitly through login methods prevents circular auth loops.