Skip to content

Commit 6ead8b6

Browse files
committed
refactor: fold definition-services into ctx.services.install()
install()'s resolveFrom now also accepts an npm package name (the declaring plugin's packageName), expanded to that package's location so its declared services resolve against the plugin's own dependencies — adapters and the hub queue def.services with a plain loop, and the installDefinitionServices helper (whose awaited form could deadlock pre-barrier) is gone.
1 parent dd7f486 commit 6ead8b6

13 files changed

Lines changed: 60 additions & 65 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import {
1414
DEVFRAME_RPC_DUMP_MANIFEST_FILENAME,
1515
} from '../constants'
1616
import { createHostContext } from '../node/context'
17-
import { installDefinitionServices } from '../node/definition-services'
1817
import { diagnostics } from '../node/diagnostics'
1918
import { createH3DevframeHost } from '../node/host-h3'
2019
import { collectStaticRpcDump } from '../rpc/dump/static'
@@ -89,7 +88,8 @@ export async function createBuild(d: DevframeDefinition, options: CreateBuildOpt
8988
mode: 'build',
9089
host,
9190
})
92-
await installDefinitionServices(ctx, d)
91+
for (const input of d.services ?? [])
92+
void ctx.services.install(input, { resolveFrom: d.packageName })
9393
await d.setup(ctx)
9494
await ctx.services.ready()
9595

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { DevframeNodeContext } from '../types/context'
22
import type { DevframeDefinition } from '../types/devframe'
3-
import { installDefinitionServices } from '../node/definition-services'
43

54
export interface CreateEmbeddedOptions {
65
/** Target context the devframe is registered into. Required. */
@@ -20,6 +19,7 @@ export async function createEmbedded(d: DevframeDefinition, options: CreateEmbed
2019
// Declarative services queue before setup; the owning host fires the
2120
// `ctx.services.ready()` barrier (post-barrier registration installs
2221
// immediately).
23-
await installDefinitionServices(options.ctx, d)
22+
for (const input of d.services ?? [])
23+
void options.ctx.services.install(input, { resolveFrom: d.packageName })
2424
await d.setup(options.ctx)
2525
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { resolve } from 'pathe'
1616
import { joinURL } from 'ufo'
1717
import { DEVFRAME_CONNECTION_META_FILENAME } from '../constants'
1818
import { createHostContext } from '../node/context'
19-
import { installDefinitionServices } from '../node/definition-services'
2019
import { diagnostics } from '../node/diagnostics'
2120
import { createH3DevframeHost } from '../node/host-h3'
2221
import { createInstanceShell, resolveInstanceRegister } from '../node/instance-shell'
@@ -290,7 +289,10 @@ export function initDevframe(
290289
host: hostImpl,
291290
})
292291
const setupInfo: DevframeSetupInfo = { flags: options.flags ?? {} }
293-
await installDefinitionServices(context, def)
292+
// Declarative services queue ahead of setup (their promises resolve at
293+
// the ready() barrier below), resolving against the plugin's own deps.
294+
for (const input of def.services ?? [])
295+
void context.services.install(input, { resolveFrom: def.packageName })
294296
await def.setup(context, setupInfo)
295297
await context.services.ready()
296298

‎packages/devframe/src/adapters/mcp/build-server.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { Server } from '@modelcontextprotocol/server'
88
import { createHostContext } from 'devframe/node'
99
import { toAgentToolName } from 'devframe/utils/agent-tool-name'
1010
import { join } from 'pathe'
11-
import { installDefinitionServices } from '../../node/definition-services'
1211
import { diagnostics } from '../../node/diagnostics'
1312
import { formatMcpError, stringifyForMcp } from './stringify'
1413
import { argsToJsonSchema, returnToJsonSchema } from './to-json-schema'
@@ -117,7 +116,8 @@ export async function createMcpServer(
117116
mode: 'dev',
118117
host,
119118
})
120-
await installDefinitionServices(ctx, definition)
119+
for (const input of definition.services ?? [])
120+
void ctx.services.install(input, { resolveFrom: definition.packageName })
121121
await definition.setup(ctx)
122122
await ctx.services.ready()
123123

‎packages/devframe/src/internal/index.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,9 @@
3232
// - `diagnostics` — devframe core's structured diagnostics instance
3333
// (`DF00xx`), so a first-party integration built outside this package can
3434
// report against the same registered codes instead of minting its own.
35-
// - `installDefinitionServices` — queues a definition's declarative wire
36-
// services ahead of its `setup`; a host that installs devframes itself
37-
// (the hub's `installDevframe`) calls it exactly like the adapters do.
3835
export { normalizeBasePath, resolveBasePath } from '../adapters/_shared'
3936
export { coerceAgentPositionalArgs } from '../node/agent-args'
4037
export type { AgentArgsFallback } from '../node/agent-args'
41-
export { installDefinitionServices } from '../node/definition-services'
4238
export { diagnostics } from '../node/diagnostics'
4339
export { DevframeAgentHost } from '../node/host-agent'
4440
export * from '../node/host-h3'

‎packages/devframe/src/node/__tests__/services.test.ts‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,20 @@ describe('wire services (install / ready barrier)', () => {
208208
await expect(ctx.services.ready()).rejects.toThrowError(/Failed to import the required service package/)
209209
})
210210

211+
it('resolves a package-name resolveFrom through that package\'s own dependencies', async () => {
212+
const { ctx, dir } = await createCtx()
213+
// A "plugin" package whose own node_modules carries the service — the
214+
// declarative flow passes the plugin's packageName as resolveFrom.
215+
const pluginDir = join(dir, 'node_modules', '@test', 'plugin')
216+
mkdirSync(pluginDir, { recursive: true })
217+
writeFileSync(join(pluginDir, 'package.json'), JSON.stringify({ name: '@test/plugin', version: '0.0.0' }))
218+
writeFakeServicePackage(pluginDir, '@test/nested-svc', '1.0.0')
219+
220+
const install = ctx.services.install({ package: '@test/nested-svc' }, { resolveFrom: '@test/plugin' })
221+
await ctx.services.ready()
222+
await expect(install).resolves.toEqual({ imported: true, options: undefined })
223+
})
224+
211225
it('imports a descriptor package relative to resolveFrom and installs its factory', async () => {
212226
const { ctx, dir } = await createCtx()
213227
writeFakeServicePackage(dir, '@test/imported-svc', '2.0.0')

‎packages/devframe/src/node/definition-services.ts‎

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

‎packages/devframe/src/node/host-services.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import type {
88
DevframeServicesHost,
99
DevframeServicesState,
1010
} from 'devframe/types'
11+
import process from 'node:process'
1112
import { DEVFRAME_SERVICES_STATE_KEY } from 'devframe/constants'
1213
import { createDebug } from 'obug'
1314
import { diagnostics } from './diagnostics'
14-
import { importServicePackage, satisfiesVersionRange, shallowMergeOptionSets } from './services-install'
15+
import { expandResolveFrom, importServicePackage, satisfiesVersionRange, shallowMergeOptionSets } from './services-install'
1516

1617
const debug = createDebug('devframe:services')
1718

@@ -184,10 +185,11 @@ export class DevframeServicesHostImpl implements DevframeServicesHost {
184185
if (!def) {
185186
const descriptors = entries.map(entry => entry.input as DevframeServiceDescriptor)
186187
const required = descriptors.some(descriptor => descriptor.required === true)
188+
const cwd = this.context?.cwd ?? process.cwd()
187189
const resolveFroms = [
188-
...entries.map(entry => entry.resolveFrom),
190+
...entries.map(entry => entry.resolveFrom && expandResolveFrom(entry.resolveFrom, cwd)),
189191
this.context?.workspaceRoot,
190-
this.context?.cwd,
192+
cwd,
191193
]
192194
let mod: unknown
193195
try {

‎packages/devframe/src/node/services-install.ts‎

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createRequire } from 'node:module'
22
import { pathToFileURL } from 'node:url'
3-
import { join } from 'pathe'
3+
import { isAbsolute, join } from 'pathe'
44

55
/**
66
* Turn a `resolveFrom` value (a file path, a file URL like `import.meta.url`,
@@ -18,6 +18,29 @@ function toRequireBase(resolveFrom: string): string {
1818
return join(resolveFrom, '_devframe_resolve.js')
1919
}
2020

21+
/**
22+
* Normalize an `install()` `resolveFrom` into a resolution base. Paths and
23+
* file URLs pass through; a bare npm package name (the common case: the
24+
* declaring plugin's `packageName`) resolves to that package's location from
25+
* `cwd`, so a service it declares resolves against the plugin's own
26+
* dependencies. An unresolvable package name reads as no base (the caller's
27+
* workspace fallbacks apply).
28+
*/
29+
export function expandResolveFrom(resolveFrom: string, cwd: string): string | undefined {
30+
if (resolveFrom.startsWith('file://') || resolveFrom.startsWith('.') || isAbsolute(resolveFrom))
31+
return resolveFrom
32+
const require = createRequire(join(cwd, '_devframe_resolve.js'))
33+
try {
34+
return require.resolve(`${resolveFrom}/package.json`)
35+
}
36+
catch {}
37+
try {
38+
return require.resolve(resolveFrom)
39+
}
40+
catch {}
41+
return undefined
42+
}
43+
2144
/**
2245
* Import a service package's module, trying each `resolveFrom` candidate in
2346
* order (so a plugin-declared service resolves against the plugin's own

‎packages/devframe/src/types/services.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -220,10 +220,11 @@ export interface DevframeServicesHost {
220220
* already-installed package returns the existing API (a warning is
221221
* emitted when the late install carried options, since they're ignored).
222222
*
223-
* `resolveFrom` is the path or file URL (e.g. `import.meta.url`) a
224-
* descriptor's package resolves **from**, so a plugin-declared service
225-
* resolves against the plugin's own dependencies; it falls back to the
226-
* context's `workspaceRoot`.
223+
* `resolveFrom` is where a descriptor's package resolves **from**: a path
224+
* or file URL (e.g. `import.meta.url`), or an npm package name — typically
225+
* the declaring plugin's `packageName`, so its declared services resolve
226+
* against the plugin's own dependencies. Falls back to the context's
227+
* `workspaceRoot`.
227228
*/
228229
install: <API = unknown, Options = any>(
229230
input: DevframeServiceInput<API, Options>,

0 commit comments

Comments
 (0)