返回 CodeWhale
specifier.ts
1 /**
2 * How one composition row's `name` reaches a module.
3 *
4 * A preset composition is read by `Include`, which rewrites its context's
5 * `baseUrl` to the composition's own directory. That is right for a row
6 * naming a file the preset ships and wrong for a row naming a package: a
7 * locally authored preset lives under the user's home, where Node's upward
8 * `node_modules` walk never reaches the harness's own dependencies. Both the
9 * mount's import override and discovery's health check therefore have to
10 * classify a row's name before they can act on it, and they must classify it
11 * the same way — a row discovery resolves from one base and the mount imports
12 * from another would be reported healthy and then fail to load.
13 * @module @deepseek-ai/dsh-agent-presets/specifier
14 */
15
16 import { isAbsolute } from 'node:path'
17 import { pathToFileURL } from 'node:url'
18
19 /** One composition row's module specifier, classified by where it resolves. */
20 export type RowSpecifier =
21 /** A `cordis:` builtin the Loader supplies; nothing is resolved. */
22 | { readonly kind: 'builtin'; readonly specifier: string }
23 /** A path relative to the preset's own directory; the preset ships the file. */
24 | { readonly kind: 'preset'; readonly specifier: string }
25 /** An absolute path or `file:` URL; it names one file and no base. */
26 | { readonly kind: 'file'; readonly specifier: string }
27 /** A package name resolved from the installed harness. */
28 | { readonly kind: 'package'; readonly specifier: string }
29
30 /**
31 * Classify one row's `name`.
32 *
33 * An absolute filesystem path becomes a file URL here rather than at each
34 * call site, because Node's ESM resolver rejects a bare drive-letter path on
35 * Windows. A `file:` URL is already one and joins it: the Loader accepts both
36 * spellings for the same thing, and treating the URL as a package name would
37 * hand it to a resolver that only normalizes it, reporting a file that is not
38 * there as present. The `specifier` a caller receives is always the string to
39 * hand a resolver; only `kind` decides which base it goes with.
40 * @param name - the module specifier exactly as the row wrote it.
41 * @returns the classification, carrying the specifier to resolve.
42 */
43 export function classifyRowSpecifier(name: string): RowSpecifier {
44 if (name.startsWith('cordis:')) return { kind: 'builtin', specifier: name }
45 if (name.startsWith('.')) return { kind: 'preset', specifier: name }
46 if (name.startsWith('file:')) return { kind: 'file', specifier: name }
47 if (isAbsolute(name)) return { kind: 'file', specifier: pathToFileURL(name).href }
48 return { kind: 'package', specifier: name }
49 }
50
50 lines TYPESCRIPT