fix: boot use internal loader and fix path resolve

This commit is contained in:
imccyu
2026-07-15 11:51:29 +08:00
parent 33474159e9
commit 7fc11d685a
4 changed files with 20 additions and 13 deletions

View File

@@ -147,11 +147,11 @@ describe.skipIf(!existsSync(acpBin))('dsh-acp-agent BUILT bin (node lib/bin.js,
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => {
// A nonexistent directory prevents even the include plugin import. Loader logs the failure and
// leaves no fiber; boot's settled-entry guard must convert that state into non-zero exit.
// boot() pre-resolves the bootstrap include to an absolute URL, so a nonexistent config
// directory cannot break its import; the include plugin's own read must fail loud instead.
const { code, stderr } = await runBinExpectingExit('/nonexistent/dir/cordis.yml')
expect(code).not.toBe(0)
expect(stderr).toContain('failed to load')
expect(stderr).toContain('config file not found')
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a missing config file in a real directory', async () => {

View File

@@ -8,7 +8,7 @@ Shared boot glue for the app bins ([`dsh-stdio-agent`](../stdio-agent/README.md)
| `loadEnv(binName, dir?, warn?)` | Load the gitignored `.env` (Node `process.loadEnvFile`); absent file is fine, an unloadable one warns a single labelled line (default: stderr) |
| `installFailLoud(binName, proc?)` | Turn a post-`boot()` unhandled Loader rejection into one labelled stderr line + `exit(1)`; returns the uninstaller (for tests) |
| `assertEntriesLoaded(ctx, binName)` | Throw when a settled tree holds an enabled entry with no fiber (a plugin module that failed to import) |
| `boot(binName, absoluteConfigPath)` | Mount the Loader, include the config by absolute `file://` URL, await the whole tree, assert entries loaded, return the root context |
| `boot(binName, absoluteConfigPath)` | Mount the Loader, mount the statically imported include plugin as the `cordis:include` builtin (so the config may live outside `node_modules` reach), include the config by absolute `file://` URL, await the whole tree, assert entries loaded, return the root context |
Two failure classes the guards handle: `loader.await()` swallows init rejections (`Promise.allSettled`) — Node still exits non-zero on the resulting unhandled rejection, and `installFailLoud` replaces the noisy dump with one labelled line and a guaranteed `exit(1)`; a failed plugin IMPORT is only logged by the Loader (the process would otherwise exit 0 on a usable config typo), leaving a fiber-less entry that `assertEntriesLoaded` turns into a `boot()` rejection.

View File

@@ -9,6 +9,7 @@ import { pathToFileURL } from 'node:url'
import { basename, dirname, resolve } from 'node:path'
import { Context } from 'cordis'
import Loader from '@cordisjs/plugin-loader'
import Include from '@cordisjs/plugin-include'
/**
* Resolve the config to boot. Replay swaps a `cordis.yml` basename for
@@ -95,11 +96,16 @@ export function assertEntriesLoaded(ctx: Context, binName: string): void {
/**
* Boot the Loader against `absoluteConfigPath` and return only after the whole
* tree settles. The include uses an absolute file URL while `baseUrl` stays at
* the config directory for its relative imports. A missing fiber rejects here;
* a later init rejection is handled by {@link installFailLoud}. Built bins need
* `--expose-internals` or the Loader's native fallback for bare plugin
* specifiers; relative specifiers do not.
* tree settles. Entry names load through the Loader's internal module loader
* against `baseUrl` (the config directory), which may live outside
* `node_modules` reach and, unbuilt, cannot load vendored source; the
* bootstrap include is therefore statically imported and mounted as the
* `cordis:include` builtin, loading through the ambient module pipeline
* (vite/tsx/plain ESM) while the included tree's own specifiers stay
* config-relative. A missing fiber rejects here; a later init rejection is
* handled by {@link installFailLoud}. Built bins need `--expose-internals` or
* the Loader's native fallback for bare plugin specifiers; relative specifiers
* do not.
* @param binName - the diagnostic prefix for load-failure errors.
* @param absoluteConfigPath - the config to include; must already be absolute
* (see {@link resolveConfigPath}).
@@ -109,8 +115,9 @@ export async function boot(binName: string, absoluteConfigPath: string): Promise
const ctx = new Context()
ctx.baseUrl = pathToFileURL(dirname(absoluteConfigPath)).href + '/'
await ctx.plugin(Loader)
ctx.loader.builtins.include = Include
await ctx.loader.create({
name: '@cordisjs/plugin-include',
name: 'cordis:include',
config: { path: pathToFileURL(absoluteConfigPath).href },
})
await ctx.loader.await()

View File

@@ -146,12 +146,12 @@ describe.skipIf(!existsSync(stdioBin))('dsh-stdio-agent BUILT bin (node lib/bin.
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a config whose directory does not exist', async () => {
// A nonexistent directory prevents even the include plugin import. Loader leaves no fiber, and
// boot's settled-entry guard must turn that state into a clear non-zero failure.
// boot() pre-resolves the bootstrap include to an absolute URL, so a nonexistent config
// directory cannot break its import; the include plugin's own read must fail loud instead.
consumer = await makeConsumer('unused')
const { code, stderr } = await runBuiltBin(consumer, '/nonexistent/dir/cordis.yml', '')
expect(code).not.toBe(0)
expect(stderr).toContain('failed to load')
expect(stderr).toContain('config file not found')
}, 30_000)
it('fails LOUD (non-zero exit + stderr) on a missing config file in a real directory', async () => {