Kaynağa Gözat

fix: boot use internal loader and fix path resolve

imccyu 3 ay önce
ebeveyn
işleme
7fc11d685a

+ 3 - 3
packages/ui/acp-agent/tests/built-bin.e2e.ts

@@ -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 () => {

+ 1 - 1
packages/ui/app-boot/README.md

@@ -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.
 

+ 13 - 6
packages/ui/app-boot/src/index.ts

@@ -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()

+ 3 - 3
packages/ui/stdio-agent/tests/built-bin.e2e.ts

@@ -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 () => {