Skip to content

Commit 2027b4b

Browse files
committed
refactor: services ready before setup; assets fully declarative
Invert the wire-services lifecycle so services are constructed and ready BEFORE any setup() runs, and setup consumes them synchronously. - adapters (initiate/build/mcp/embedded) install-loop -> ready() -> setup - hub: two passes — dock+collect across all devframes (+ new initHub({ services }) host-level channel) -> ready() once -> setups -> configure -> ui.setup; installDevframe/prepareDevframe split accordingly - ready() is internal; install() stays as the dynamic escape hatch (immediate post-ready construct) - descriptor options deep-merge by default (objects recurse, arrays union-dedupe, scalars last-wins); both shipped services drop their custom mergeOptions (hook kept as override) - drop the first-connect safety net + DF0071 Consumers: - assets declares both services (service-open with the factory-computed managed dir as an allowed root) — no imperative install in setup; drops its open-in-editor/reveal-in-folder RPCs (+ DP_ASSETS_0008) and the client calls service-open directly with a new dev-only absolute AssetInfo.fsPath (reveal passes the parent dir) - messages harness mirrors the new ordering Docs: guide/services.md rewritten to the pre-setup lifecycle; DF0071 page removed; error pages reworded.
1 parent 4f64284 commit 2027b4b

43 files changed

Lines changed: 335 additions & 388 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/errors/DF0066.md‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,29 +10,27 @@ outline: deep
1010
1111
## Cause
1212

13-
Wire services are deduplicated by npm package name: the first installation wins, and later installs of the same package return the existing node API. Option sets from multiple installers only merge **before** the `ctx.services.ready()` barrier fires — an install that arrives after the service was constructed can no longer influence its configuration, so any options it carried are dropped with this warning.
13+
Wire services are deduplicated by npm package name: the first installation wins, and later installs of the same package return the existing node API. Declared services are constructed once **before setup runs**, deep-merging every declarer's options. Calling `ctx.services.install()` for an already-constructed package — the dynamic escape hatch used after that point — can no longer influence its configuration, so any options it carries are dropped with this warning.
1414

1515
## Example
1616

1717
```ts
18-
await ctx.services.ready()
19-
20-
// ✗ The service is already constructed; { themes } is ignored.
21-
await ctx.services.install(createShikiService({ themes }))
18+
// The package is already declared (and constructed pre-setup) elsewhere.
19+
// ✗ This late install can't merge; { themes } is ignored.
20+
ctx.services.install(createShikiService({ themes }))
2221
```
2322

2423
## Fix
2524

26-
Install the service (or declare it in `DevframeDefinition.services`) before the barrier — a host's explicit installs during setup/`configure` naturally run before the adapter fires `ready()`, so its options join the merge:
25+
Declare the service so its options join the pre-setup merge — on the plugin's `DevframeDefinition.services`, or host-wide via `initHub({ services })`:
2726

2827
```ts
29-
await initHub({
30-
async configure(ctx) {
31-
ctx.services.install(createShikiService({ themes })) // ✓ merges
32-
},
28+
initHub({
29+
services: [createShikiService({ themes })], // ✓ merges before setup
30+
devframes: [/* … */],
3331
})
3432
```
3533

3634
## Source
3735

38-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — `install()`/the barrier flush warn when an already-installed package is installed again.
36+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — `installPackage` warns when an already-installed package is installed again.

‎docs/errors/DF0067.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ outline: deep
1010
1111
## Cause
1212

13-
A service descriptor marked `required: true` names a package that could not be resolved and imported at the `ctx.services.ready()` barrier. Descriptors resolve against the declaring plugin's own dependencies first (then the workspace root), so this usually means the service package is missing from the declarer's `dependencies`, or isn't installed.
13+
A service descriptor marked `required: true` names a package that could not be resolved and imported when services are constructed before setup. Descriptors resolve against the declaring plugin's own dependencies first (then the workspace root), so this usually means the service package is missing from the declarer's `dependencies`, or isn't installed.
1414

1515
Descriptors without `required` degrade instead: the missing service is skipped and clients observe `services.has(pkg) === false`.
1616

@@ -19,7 +19,7 @@ Descriptors without `required` degrade instead: the missing service is skipped a
1919
```ts
2020
defineDevframe({
2121
services: [
22-
// ✗ Throws at the ready() barrier when the package isn't installed.
22+
// ✗ Throws before setup when the package isn't installed.
2323
{ package: '@devframes/service-shiki', required: true },
2424
],
2525
})
@@ -31,4 +31,4 @@ Install the service package next to whoever declares it — a plugin declaring i
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush throws when a `required` descriptor's package fails to import.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction throws when a `required` descriptor's package fails to import.

‎docs/errors/DF0068.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ outline: deep
1010
1111
## Cause
1212

13-
A service descriptor marked `required: true` declares a `version` range, and the version of the service that actually resolved falls outside it. The range is checked at the `ctx.services.ready()` barrier against the resolved definition's own `version`.
13+
A service descriptor marked `required: true` declares a `version` range, and the version of the service that actually resolved falls outside it. The range is checked when services are constructed before setup against the resolved definition's own `version`.
1414

1515
Without `required`, the same mismatch installs the service anyway and warns with [`DF0069`](/errors/DF0069).
1616

@@ -31,4 +31,4 @@ Align the installed service package with the declared range (update whichever si
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush checks each descriptor's `version` range against the resolved definition.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction checks each descriptor's `version` range against the resolved definition.

‎docs/errors/DF0069.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,4 @@ Align the installed service package with the declared range to silence the warni
3131

3232
## Source
3333

34-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the barrier flush checks each descriptor's `version` range against the resolved definition.
34+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — the pre-setup construction checks each descriptor's `version` range against the resolved definition.

‎docs/errors/DF0070.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ A service package's default export must be its `create<X>Service` factory, retur
3333

3434
## Source
3535

36-
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — `install()` validates its input; the barrier flush validates imported factories and the definitions they return.
36+
- [`packages/devframe/src/node/host-services.ts`](https://github.com/devframes/devframe/blob/main/packages/devframe/src/node/host-services.ts) — `install()` validates its input; the pre-setup construction validates imported factories and the definitions they return.

‎docs/errors/DF0071.md‎

Lines changed: 0 additions & 28 deletions
This file was deleted.

‎docs/guide/services.md‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -101,29 +101,36 @@ export default function createOpenService(options?: OpenServiceOptions): Devfram
101101

102102
Two declaration merges make it fully typed for consumers: the fully-qualified RPC ids go into `DevframeRpcServerFunctions`, and the package → scope mapping into `DevframeServicesScopeRegistry` (so a client's `services.get()` returns a scoped, typed RPC handle).
103103

104-
### Installing
104+
### Declaring
105105

106-
A host with the factory at hand installs explicitly; a plugin declares what it consumes on its definition and the adapter resolves the package **against the plugin's own dependencies**:
106+
Services are **declarative**. A plugin lists what it consumes on its definition; a host lists shared ones on `initHub`. The adapter resolves each package — for a plugin, **against the plugin's own dependencies** — and constructs it:
107107

108108
```ts
109-
// host side (e.g. inside initHub's configure)
110-
ctx.services.install(createShikiService({ themes }))
111-
112-
// plugin side — declarative
109+
// plugin side — on the definition
113110
defineDevframe({
114111
services: [
115112
{ package: '@devframes/service-open' },
116113
{ package: '@devframes/service-shiki', version: '^1', options: { langs: ['vue'] } },
117114
],
118115
})
116+
117+
// host side — shared services on initHub
118+
initHub({
119+
services: [createShikiService({ themes })],
120+
devframes: [/* … */],
121+
})
119122
```
120123

121124
Entries are optional by default — a package that isn't installed is skipped and clients see `has() === false`. Mark an entry `required: true` to fail hard instead ([`DF0067`](https://devfra.me/errors/DF0067) on a missing package, [`DF0068`](https://devfra.me/errors/DF0068) on an unsatisfied `version` range; without it a range mismatch only warns with [`DF0069`](https://devfra.me/errors/DF0069)).
122125

123-
Installs queue until the adapter fires the `ctx.services.ready()` barrier after every devframe's setup has run. There each service is constructed **once**, with the option sets from every declarer merged — through the definition's `mergeOptions` when it declares one, otherwise shallow-merged in declaration order, so a host installing last wins. After the barrier, installing an already-installed package returns the existing API and warns ([`DF0066`](https://devfra.me/errors/DF0066)) when its options had to be ignored.
126+
### Lifecycle: ready before setup
127+
128+
Services are constructed and made ready **before any `setup(ctx)` runs**. The hub collects every declared service (across all devframes plus `initHub`), constructs each **once** — deep-merging the option sets from every declarer (objects recurse, arrays union-dedupe, scalars take the later value; a service may override with its own `mergeOptions`) — and only then runs the setups. So `setup(ctx)` can consume a service synchronously via `ctx.services.get(pkg)`, including one another devframe declared.
124129

125130
Server-side consumers get the node API from the same registry — `ctx.services.get('@devframes/service-open')` or `whenAvailable` — with no RPC hop.
126131

132+
Declarative covers the common case. For a service whose configuration is only known at runtime, `ctx.services.install(input)` is the dynamic escape hatch: after the pre-setup construction it builds immediately; re-installing an already-constructed package returns the existing API and warns ([`DF0066`](https://devfra.me/errors/DF0066)) if it carried options that can no longer merge.
133+
127134
### Feature-detecting on the client
128135

129136
Installed services are advertised through the `devframe:services` [shared state](./shared-state); the client mirrors it on `rpc.services`:

‎packages/devframe/src/adapters/build.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt
8888
mode: 'build',
8989
host,
9090
})
91+
// Services ready before setup, so setup can consume them synchronously.
9192
for (const input of d.services ?? [])
9293
void ctx.services.install(input, { resolveFrom: d.packageName })
93-
await d.setup(ctx)
9494
await ctx.services.ready()
95+
await d.setup(ctx)
9596

9697
await fs.mkdir(resolve(outDir, DEVFRAME_RPC_DUMP_DIRNAME), { recursive: true })
9798

‎packages/devframe/src/adapters/embedded.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ export interface CreateEmbeddedOptions {
1616
* effective default follows the hosted rule of `def.basePath ?? '/__<id>/'`.
1717
*/
1818
export async function createEmbedded(d: DevframeDefinition, options: CreateEmbeddedOptions): Promise<void> {
19-
// Declarative services queue before setup; the owning host fires the
20-
// `ctx.services.ready()` barrier (post-barrier registration installs
21-
// immediately).
19+
// Services ready before setup. `ready()` is idempotent: on an
20+
// already-running host it's a no-op and the fresh installs construct
21+
// immediately; on a not-yet-started one it fires the initial barrier.
2222
for (const input of d.services ?? [])
2323
void options.ctx.services.install(input, { resolveFrom: d.packageName })
24+
await options.ctx.services.ready()
2425
await d.setup(options.ctx)
2526
}

‎packages/devframe/src/adapters/initiate.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,12 @@ export function initDevframe(
289289
host: hostImpl,
290290
})
291291
const setupInfo: DevframeSetupInfo = { flags: options.flags ?? {} }
292-
// Declarative services queue ahead of setup (their promises resolve at
293-
// the ready() barrier below), resolving against the plugin's own deps.
292+
// Wire services are constructed and made ready BEFORE setup, so
293+
// `setup(ctx)` can consume them synchronously (`ctx.services.get`).
294294
for (const input of def.services ?? [])
295295
void context.services.install(input, { resolveFrom: def.packageName })
296-
await def.setup(context, setupInfo)
297296
await context.services.ready()
297+
await def.setup(context, setupInfo)
298298

299299
// Route-based MCP server (opt-in). Mounted before the SPA static
300300
// catch-all so the exact `<base>__mcp` route wins, and advertised in

0 commit comments

Comments
 (0)