Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
e919b68
fix(providers): point each export condition's types at its own declar…
claude Aug 7, 2026
56640f9
test(exports): flag condition branches that declare no types at all
claude Aug 8, 2026
6a5ea1f
docs: note that TypeScript applies the browser condition only on opt-in
claude Aug 9, 2026
4489fbb
test: guard exports targets against having no source entry
claude Aug 10, 2026
d216ba3
test(exports): check every implementation key and cross-sibling order
claude Aug 13, 2026
8269124
test(exports): catch a browser condition that names the node bundle (…
sroussey Aug 13, 2026
d82726c
test(exports): close two holes in the exports-pairing guard
claude Aug 14, 2026
d02a046
fix(providers): restore the runtime-agnostic exports the browser barr…
sroussey Aug 15, 2026
c794427
test(exports): widen both export guards one directory down
claude Aug 16, 2026
3e57e17
fix(providers): close the two runtime-barrel defects the widened guar…
claude Aug 16, 2026
f4ce54b
Merge pull request #813 from workglow-dev/claude/optimistic-goldberg-…
sroussey Aug 16, 2026
9cfee82
test(exports): resolve a bare shim's target before exempting it
claude Aug 17, 2026
67dbdd9
chore(workglow): drop the four browser shims whose targets never split
claude Aug 17, 2026
88e0d81
chore(providers): drop the inert browser build from the two server pr…
claude Aug 17, 2026
6203303
test(exports): compare barrel surfaces in both directions
claude Aug 17, 2026
a4b11c8
test(exports): compile the browser condition instead of only reading it
claude Aug 17, 2026
ad3209a
Merge pull request #837 from workglow-dev/claude/export-guard-coverage
sroussey Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ jobs:
- name: Typecheck co-located tests
run: bun run typecheck:tests

# The only compiler run in CI that resolves a provider or meta-package
# subpath under the `browser` condition. Every other browser-split guard
# reads manifests and barrels as TEXT, so an `export *` dropped from both
# halves of a pair passes them all and surfaces only in a downstream
# browser app. Must follow the budget guard, which is what emits the
# `dist/*.d.ts` this program resolves against.
- name: Typecheck the browser condition
run: bun run typecheck:browser

test-discovery:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion docs/technical/18-multi-runtime-abstraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,10 @@ Resolution rules:
Each condition block includes a `"types"` field so that TypeScript resolves the correct `.d.ts`
file for the platform. This is critical because the type declarations differ per platform — for
example, the browser entry exports `Worker` as `globalThis.Worker` while the Node entry exports
a `WorkerPolyfill` that wraps `worker_threads`.
a `WorkerPolyfill` that wraps `worker_threads`. That resolution is not automatic for `tsc`:
`"browser"` is not one of TypeScript's built-in conditions, so a browser consumer must set
`"customConditions": ["browser"]` in its tsconfig or it type-checks the browser bundle against
the node declarations. See [Conditional Exports](./19-build-system.md#conditional-exports).

### Sub-path exports

Expand Down
36 changes: 36 additions & 0 deletions docs/technical/19-build-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,42 @@ declarations. This is important because `.d.ts` files may differ across platform
the browser build exports `globalThis.Worker` while the Node build exports a `WorkerPolyfill`
with a different constructor signature.

**A block's `"types"` must name the declaration of the implementation in that same block.**
`tsgo` compiles all of `src/**/*`, so every entry file already has its own `.d.ts` in `dist` —
there is never a reason to borrow another target's. A block that points `"browser"` at the node
`.d.ts` type-checks browser consumers against the node build: they get autocomplete and
successful compiles for symbols that are `undefined` at runtime, because the browser bundle
never exported them. `packages/test/src/test/util/ExportTypesPairing.test.ts` walks every
condition branch of every workspace manifest and fails on any pair that has drifted apart.

**TypeScript applies the `"browser"` condition only when the consumer asks for it.** Its condition
set is `["import", "types"]` under `moduleResolution: "bundler"` and `["node", "import", "types"]`
under `node16`/`nodenext` — `"browser"` is in neither. A Vite or webpack app therefore _bundles_
`dist/browser.js` while `tsc` resolves the outer `"types"` and type-checks it against the node
declaration. Correct manifests do not fix that on their own; they move the drift from the manifest
to the consumer's tsconfig. Browser consumers must opt in:

```json
{
"compilerOptions": {
"moduleResolution": "bundler",
"customConditions": ["browser"]
}
}
```

`examples/web/tsconfig.json` is the in-repo example. Downstream apps that bundle for the browser
need the same line — without it, `import { _testOnly } from "@workglow/openai/ai"` compiles with
full autocomplete and is `undefined` at runtime.

CI proves that condition still resolves. The `Typecheck the browser condition` step in
`test.yml`'s `typecheck-budget` job runs `bun run typecheck:browser`, which compiles
`packages/test/src/browser-conditions/browserConditionResolution.types.ts` under
`customConditions: ["browser"]` — the only compiler run in CI that resolves a provider or
meta-package subpath that way. Every other browser-split guard reads manifests and barrels as
text, so an `export *` dropped from **both** halves of a pair passes them all and surfaces only
in a downstream browser app.

---

## Developer Workflow
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"build:types": "turbo run build-types --concurrency=15",
"typecheck:budget": "bun scripts/typecheck-budget.ts",
"typecheck:tests": "for f in packages/*/tsconfig.test.json; do echo \"typecheck $f\" && npx tsgo -p \"$f\" || exit 1; done",
"typecheck:browser": "npx tsgo -p packages/test/tsconfig.browser-conditions.json",
"clean": "rm -rf node_modules packages/*/node_modules packages/*/*tsbuildinfo packages/*/dist packages/*/src/**/*\\.d\\.ts packages/*/src/**/*\\.map integrations/*/node_modules integrations/*/dist integrations/*/src/**/*\\.d\\.ts integrations/*/src/**/*\\.map examples/*/node_modules examples/*/dist examples/*/src/**/*\\.d\\.ts examples/*/src/**/*\\.map .turbo */*/.turbo",
"dev": "turbo run dev --concurrency=15",
"docs": "typedoc",
Expand Down
3 changes: 2 additions & 1 deletion packages/test/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"node-llama-cpp": "catalog:",
"pg": "catalog:",
"playwright": "catalog:",
"vitest": "catalog:"
"vitest": "catalog:",
"workglow": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/**
* @license
* Copyright 2026 Steven Roussey <sroussey@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*/

/**
* The browser condition, COMPILED rather than parsed.
*
* This is the only place in the repo where a compiler resolves a provider or
* meta-package subpath under `customConditions: ["browser"]`. Everything else
* that checks the browser split reads manifests and barrels AS TEXT:
* `ExportTypesPairing.test.ts` walks `exports` maps, `ExportBarrelParity.test.ts`
* diffs `export` statements. Both are blind to the failure that actually
* reaches a consumer — an `export *` dropped from BOTH halves of a pair keeps
* barrel parity, keeps the manifest self-consistent, builds clean, and shows up
* only when a downstream browser app upgrades and gets `TS2305`.
*
* The file never RUNS. It is type-only, and the assertion is that it RESOLVES
* and COMPILES.
*
* Two kinds of assertion, and the negatives are what make the positives mean
* anything:
*
* - POSITIVE — `typeof Ns.Name` for a symbol each subpath must export. A
* dropped re-export fails with `TS2339`.
* - NEGATIVE — `@ts-expect-error` on a node-only symbol (`_testOnly`). If
* `customConditions` silently stopped applying, every positive would still
* pass against the NODE declarations and only these would notice: the
* expected error would not occur and each becomes `TS2578`.
*
* Compiled by `bun run typecheck:browser` (`tsconfig.browser-conditions.json`)
* and deliberately NEVER by `packages/test`'s own `build-types`, which runs
* under node conditions where `_testOnly` resolves and every negative control
* would be a `TS2578` build failure. `packages/test/tsconfig.json` excludes
* this directory for exactly that reason.
*
* The subpath list is not maintained by hand alone: a guard in
* `ExportTypesPairing.test.ts` derives the expected set from the `providers/*`
* and `packages/workglow` manifests — EVERY subpath that really splits, with no
* exemption list — so a new provider that ships a browser split fails until
* this file names it. `packages/*` is out of scope on purpose: `examples/web`
* already compiles a slice of it under `customConditions`.
*/

import type * as AwsJobQueue from "@workglow/aws/job-queue";
import type * as Cactus from "@workglow/cactus/ai";
import type * as CactusRuntime from "@workglow/cactus/ai-runtime";
import type * as DeepSeek from "@workglow/deepseek/ai";
import type * as DeepSeekRuntime from "@workglow/deepseek/ai-runtime";
import type * as DuckDbStorage from "@workglow/duckdb/storage";
import type * as Ollama from "@workglow/ollama/ai";
import type * as OllamaRuntime from "@workglow/ollama/ai-runtime";
import type * as OpenAi from "@workglow/openai/ai";
import type * as OpenAiRuntime from "@workglow/openai/ai-runtime";
import type * as OpenRouter from "@workglow/openrouter/ai";
import type * as OpenRouterRuntime from "@workglow/openrouter/ai-runtime";
import type * as PostgresJobQueue from "@workglow/postgres/job-queue";
import type * as PostgresStorage from "@workglow/postgres/storage";
import type * as PostgresText from "@workglow/postgres/text";
import type * as SqliteJobQueue from "@workglow/sqlite/job-queue";
import type * as SqliteStorage from "@workglow/sqlite/storage";
import type * as SupabaseJobQueue from "@workglow/supabase/job-queue";
import type * as SupabaseStorage from "@workglow/supabase/storage";
import type * as Xai from "@workglow/xai/ai";
import type * as XaiRuntime from "@workglow/xai/ai-runtime";
import type * as Workglow from "workglow";
import type * as WorkglowDeepSeek from "workglow/deepseek";
import type * as WorkglowOllama from "workglow/ollama";
import type * as WorkglowOpenAi from "workglow/openai";
import type * as WorkglowOpenRouter from "workglow/openrouter";
import type * as WorkglowWorker from "workglow/worker";
import type * as WorkglowXai from "workglow/xai";

/** A symbol each subpath must still export under the browser condition. */
type Resolved = string;

// --- providers: `./ai` -------------------------------------------------------
export type OpenAiProvider = Resolved & typeof OpenAi.OPENAI;
export type OpenRouterProvider = Resolved & typeof OpenRouter.OPENROUTER;
export type XaiProvider = Resolved & typeof Xai.XAI;
export type OllamaProvider = Resolved & typeof Ollama.OLLAMA;
export type DeepSeekProvider = Resolved & typeof DeepSeek.DEEPSEEK;
export type CactusProvider = Resolved & typeof Cactus.LOCAL_CACTUS;

// --- providers: `./ai-runtime` ----------------------------------------------
export type OpenAiInline = typeof OpenAiRuntime.registerOpenAiInline;
export type OpenRouterInline = typeof OpenRouterRuntime.registerOpenRouterInline;
export type XaiInline = typeof XaiRuntime.registerXaiInline;
export type OllamaInline = typeof OllamaRuntime.registerOllamaInline;
export type DeepSeekInline = typeof DeepSeekRuntime.registerDeepSeekInline;
export type CactusInline = typeof CactusRuntime.registerCactusInline;

// --- storage / job-queue providers, which split on the same convention ------
/**
* `@workglow/aws/job-queue`'s browser build is a throw-on-load stub exporting
* nothing (`export {}`), so there is no symbol to name and that it RESOLVES
* under the browser condition is the whole assertion. Spelled as a `keyof` (so
* `never`) rather than a bare import, because organize-imports prunes an import
* nothing references and would silently delete the case.
*/
export type AwsJobQueueSurface = keyof typeof AwsJobQueue;

export type DuckDbTabular = typeof DuckDbStorage.DuckDbTabularStorage;
export type PostgresQueue = typeof PostgresJobQueue.PostgresQueueStorage;
export type PostgresKv = typeof PostgresStorage.PostgresKvStorage;
export type PostgresFts = typeof PostgresText.PostgresFtsTextIndex;
export type SqliteQueue = typeof SqliteJobQueue.SqliteQueueStorage;
export type SqliteKv = typeof SqliteStorage.SqliteKvStorage;
export type SupabaseQueue = typeof SupabaseJobQueue.SupabaseQueueStorage;
export type SupabaseKv = typeof SupabaseStorage.SupabaseKvStorage;

// --- the meta-package -------------------------------------------------------
export type WorkglowTaskRegistry = typeof Workglow.TaskRegistry;
export type WorkglowWorkerRegistry = typeof WorkglowWorker.AiProviderRegistry;

/**
* The meta-package's vendor shims, which are the two-hop case: `packages/workglow`'s
* own `tsgo` run compiles `src/openai.browser.ts` under NODE conditions, so the
* browser path THROUGH the shim is checked nowhere else. Here the emitted
* `dist/openai.browser.d.ts` is a pass-through that gets re-resolved under
* `browser` at the provider hop.
*/
export type WorkglowOpenAiProvider = Resolved & typeof WorkglowOpenAi.OPENAI;
export type WorkglowOpenRouterProvider = Resolved & typeof WorkglowOpenRouter.OPENROUTER;
export type WorkglowXaiProvider = Resolved & typeof WorkglowXai.XAI;
export type WorkglowOllamaProvider = Resolved & typeof WorkglowOllama.OLLAMA;
export type WorkglowDeepSeekProvider = Resolved & typeof WorkglowDeepSeek.DEEPSEEK;

/**
* Negative controls. `_testOnly` is node-only by design (see
* `INTENTIONAL_NODE_ONLY` in `ExportBarrelParity.test.ts`), so resolving it
* here would prove the browser condition was never applied.
*
* Deliberately NOT asserted for `@workglow/cactus`, which exports `_testOnly`
* from BOTH barrels.
*/
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type OpenAiTestOnly = typeof OpenAi._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type OpenRouterTestOnly = typeof OpenRouter._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type XaiTestOnly = typeof Xai._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type OllamaTestOnly = typeof Ollama._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type DeepSeekTestOnly = typeof DeepSeek._testOnly;

// The same five through the meta-package's shims, which is the hop that proves
// the shim's `browser` condition is doing real work rather than aliasing node.
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type WorkglowOpenAiTestOnly = typeof WorkglowOpenAi._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type WorkglowOpenRouterTestOnly = typeof WorkglowOpenRouter._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type WorkglowXaiTestOnly = typeof WorkglowXai._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type WorkglowOllamaTestOnly = typeof WorkglowOllama._testOnly;
// @ts-expect-error `_testOnly` is node-only, so the browser condition must not resolve it
export type WorkglowDeepSeekTestOnly = typeof WorkglowDeepSeek._testOnly;
Loading
Loading