@telorun/kernel 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot-context-registry.d.ts.map +1 -1
- package/dist/boot-context-registry.js +6 -6
- package/dist/boot-context-registry.js.map +1 -1
- package/dist/capabilities/capabilities/component.yaml +2 -1
- package/dist/capabilities/capabilities/executable.yaml +2 -1
- package/dist/capabilities/capabilities/handler.yaml +2 -1
- package/dist/capabilities/capabilities/listener.yaml +2 -1
- package/dist/capabilities/capabilities/provider.yaml +2 -1
- package/dist/capabilities/capabilities/template.yaml +2 -1
- package/dist/capabilities/capabilities/type.yaml +2 -1
- package/dist/capabilities/component.yaml +1 -1
- package/dist/capabilities/executable.yaml +1 -1
- package/dist/capabilities/handler.yaml +1 -1
- package/dist/capabilities/listener.yaml +1 -1
- package/dist/capabilities/provider.yaml +1 -1
- package/dist/capabilities/template.yaml +1 -1
- package/dist/capabilities/type.yaml +1 -1
- package/dist/controller-loader.d.ts +1 -1
- package/dist/controller-loader.d.ts.map +1 -1
- package/dist/controller-loader.js +4 -2
- package/dist/controller-loader.js.map +1 -1
- package/dist/controller-registry.d.ts +1 -2
- package/dist/controller-registry.d.ts.map +1 -1
- package/dist/controller-registry.js.map +1 -1
- package/dist/controllers/module/import-controller.d.ts +38 -0
- package/dist/controllers/module/import-controller.d.ts.map +1 -0
- package/dist/controllers/module/import-controller.js +119 -0
- package/dist/controllers/module/import-controller.js.map +1 -0
- package/dist/controllers/module/module-controller.d.ts +57 -11
- package/dist/controllers/module/module-controller.d.ts.map +1 -1
- package/dist/controllers/module/module-controller.js +46 -82
- package/dist/controllers/module/module-controller.js.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.d.ts.map +1 -1
- package/dist/controllers/resource-definition/resource-definition-controller.js +12 -4
- package/dist/controllers/resource-definition/resource-definition-controller.js.map +1 -1
- package/dist/evaluation-context.d.ts +91 -0
- package/dist/evaluation-context.d.ts.map +1 -0
- package/dist/evaluation-context.js +220 -0
- package/dist/evaluation-context.js.map +1 -0
- package/dist/execution-context.d.ts +13 -0
- package/dist/execution-context.d.ts.map +1 -0
- package/dist/execution-context.js +14 -0
- package/dist/execution-context.js.map +1 -0
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +1 -1
- package/dist/kernel.d.ts +23 -31
- package/dist/kernel.d.ts.map +1 -1
- package/dist/kernel.js +212 -333
- package/dist/kernel.js.map +1 -1
- package/dist/loader.d.ts +2 -2
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +29 -9
- package/dist/loader.js.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.d.ts.map +1 -1
- package/dist/manifest-adapters/local-file-adapter.js +21 -12
- package/dist/manifest-adapters/local-file-adapter.js.map +1 -1
- package/dist/manifest-schemas.d.ts +1 -25
- package/dist/manifest-schemas.d.ts.map +1 -1
- package/dist/manifest-schemas.js +3 -22
- package/dist/manifest-schemas.js.map +1 -1
- package/dist/module-context-registry.d.ts +48 -0
- package/dist/module-context-registry.d.ts.map +1 -0
- package/dist/module-context-registry.js +91 -0
- package/dist/module-context-registry.js.map +1 -0
- package/dist/module-context.d.ts +31 -0
- package/dist/module-context.d.ts.map +1 -0
- package/dist/module-context.js +67 -0
- package/dist/module-context.js.map +1 -0
- package/dist/registry.d.ts +1 -2
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +3 -3
- package/dist/registry.js.map +1 -1
- package/dist/resource-context.d.ts +25 -5
- package/dist/resource-context.d.ts.map +1 -1
- package/dist/resource-context.js +74 -28
- package/dist/resource-context.js.map +1 -1
- package/dist/schema-valiator.d.ts.map +1 -1
- package/dist/schema-valiator.js +3 -1
- package/dist/schema-valiator.js.map +1 -1
- package/dist/snapshot-serializer.d.ts +1 -2
- package/dist/snapshot-serializer.d.ts.map +1 -1
- package/dist/snapshot-serializer.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +9 -6
- package/src/boot-context-registry.ts +169 -0
- package/src/capabilities/component.yaml +4 -0
- package/src/capabilities/executable.yaml +8 -0
- package/src/capabilities/handler.yaml +4 -0
- package/src/capabilities/listener.yaml +4 -0
- package/src/capabilities/provider.yaml +4 -0
- package/src/capabilities/template.yaml +4 -0
- package/src/capabilities/type.yaml +4 -0
- package/src/controller-loader.ts +298 -0
- package/src/controller-registry.ts +206 -0
- package/src/controllers/capability/capability-controller.ts +41 -0
- package/src/controllers/module/import-controller.ts +143 -0
- package/src/controllers/module/module-controller.ts +67 -0
- package/src/controllers/module/module.json +48 -0
- package/src/controllers/resource-definition/resource-definition-controller.ts +87 -0
- package/src/controllers/resource-definition/resource-definition.json +18 -0
- package/src/event-stream.ts +121 -0
- package/src/events.ts +99 -0
- package/src/index.ts +7 -0
- package/src/kernel.ts +558 -0
- package/src/loader.ts +245 -0
- package/src/manifest-adapters/http-adapter.ts +35 -0
- package/src/manifest-adapters/local-file-adapter.ts +69 -0
- package/src/manifest-adapters/manifest-adapter.ts +33 -0
- package/src/manifest-adapters/registry-adapter.ts +56 -0
- package/src/manifest-schemas.ts +49 -0
- package/src/registry.ts +137 -0
- package/src/resource-context.ts +266 -0
- package/src/resource-uri.ts +200 -0
- package/src/schema-valiator.ts +57 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -109
- package/dist/cli.js.map +0 -1
- package/dist/expressions.d.ts +0 -20
- package/dist/expressions.d.ts.map +0 -1
- package/dist/expressions.js +0 -253
- package/dist/expressions.js.map +0 -1
- package/dist/template-definition.d.ts +0 -38
- package/dist/template-definition.d.ts.map +0 -1
- package/dist/template-definition.js +0 -26
- package/dist/template-definition.js.map +0 -1
- package/dist/template-expander.d.ts +0 -19
- package/dist/template-expander.d.ts.map +0 -1
- package/dist/template-expander.js +0 -425
- package/dist/template-expander.js.map +0 -1
- /package/{dist/src → src}/controllers/module/module.yaml +0 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { RuntimeError } from "@telorun/sdk";
|
|
2
|
+
|
|
3
|
+
/** Stored entry for a single context provider */
|
|
4
|
+
interface ProviderEntry {
|
|
5
|
+
kind: string;
|
|
6
|
+
name: string;
|
|
7
|
+
module: string;
|
|
8
|
+
/**
|
|
9
|
+
* If defined, only resources whose `"${kind}/${name}"` is in this list may
|
|
10
|
+
* access this provider's context. `undefined` means unrestricted.
|
|
11
|
+
* An empty array means no consumer is allowed.
|
|
12
|
+
*/
|
|
13
|
+
grants: string[] | undefined;
|
|
14
|
+
context: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Accumulates ContextProvider contributions during the AOT boot initialization
|
|
19
|
+
* multi-pass loop and enforces RBAC visibility rules.
|
|
20
|
+
*
|
|
21
|
+
* Lifecycle:
|
|
22
|
+
* 1. A provider resource is created + init()'d → kernel calls register().
|
|
23
|
+
* 2. Before a consumer resource is created → kernel calls buildContext() to
|
|
24
|
+
* obtain a per-consumer CEL evaluation context.
|
|
25
|
+
* 3. buildContext() enforces grants and scans the raw manifest for unauthorized
|
|
26
|
+
* references, throwing ERR_VISIBILITY_DENIED on violations.
|
|
27
|
+
*/
|
|
28
|
+
export class BootContextRegistry {
|
|
29
|
+
private providers: ProviderEntry[] = [];
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Register a new context provider after its init() has completed.
|
|
33
|
+
* Called by the kernel for every resource instance that passes isContextProvider().
|
|
34
|
+
*/
|
|
35
|
+
register(
|
|
36
|
+
kind: string,
|
|
37
|
+
name: string,
|
|
38
|
+
module: string,
|
|
39
|
+
grants: string[] | undefined,
|
|
40
|
+
context: Record<string, unknown>,
|
|
41
|
+
): void {
|
|
42
|
+
this.providers.push({ kind, name, module, grants, context });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Returns true if at least one provider has been registered.
|
|
47
|
+
* Used by the kernel to skip context resolution when there are no providers yet.
|
|
48
|
+
*/
|
|
49
|
+
hasProviders(): boolean {
|
|
50
|
+
return this.providers.length > 0;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build a CEL evaluation context for a specific consumer resource, enforcing
|
|
55
|
+
* RBAC grants.
|
|
56
|
+
*
|
|
57
|
+
* For each registered provider:
|
|
58
|
+
* - `grants === undefined` → unrestricted, always included.
|
|
59
|
+
* - `grants` defined and consumer key in grants → included.
|
|
60
|
+
* - `grants` defined and consumer key NOT in grants:
|
|
61
|
+
* - Scan the raw manifest JSON for `${{ ... }}` expressions that reference
|
|
62
|
+
* the provider's namespace prefix (`"${kind}.${name}."`).
|
|
63
|
+
* - If a reference is found → throw ERR_VISIBILITY_DENIED immediately.
|
|
64
|
+
* - If no reference → silently exclude the provider from the context.
|
|
65
|
+
*
|
|
66
|
+
* CEL context structure mirrors the kind hierarchy:
|
|
67
|
+
* kind="Config", name="database" → { Config: { database: { host, port } } }
|
|
68
|
+
* kind="MyApp.Secrets", name="vault" → { MyApp: { Secrets: { vault: { ... } } } }
|
|
69
|
+
*
|
|
70
|
+
* @param consumerKind - kind of the resource being initialized (e.g. "MyApp.Api.Server")
|
|
71
|
+
* @param consumerName - name of the resource being initialized (e.g. "api")
|
|
72
|
+
* @param rawManifest - the unresolved manifest object (used for namespace reference scan)
|
|
73
|
+
*/
|
|
74
|
+
buildContext(
|
|
75
|
+
consumerKind: string,
|
|
76
|
+
consumerName: string,
|
|
77
|
+
rawManifest: Record<string, unknown>,
|
|
78
|
+
): Record<string, unknown> {
|
|
79
|
+
const consumerKey = `${consumerKind}/${consumerName}`;
|
|
80
|
+
const celContext: Record<string, unknown> = {};
|
|
81
|
+
const manifestJson = JSON.stringify(rawManifest);
|
|
82
|
+
|
|
83
|
+
for (const provider of this.providers) {
|
|
84
|
+
const isGranted = provider.grants === undefined || provider.grants.includes(consumerKey);
|
|
85
|
+
|
|
86
|
+
if (!isGranted) {
|
|
87
|
+
const namespacePrefix = buildProviderNamespacePrefix(provider.kind, provider.name);
|
|
88
|
+
if (manifestReferencesNamespace(manifestJson, namespacePrefix)) {
|
|
89
|
+
throw new RuntimeError(
|
|
90
|
+
"ERR_VISIBILITY_DENIED",
|
|
91
|
+
`Resource "${consumerKind}/${consumerName}" references context from ` +
|
|
92
|
+
`provider "${provider.kind}/${provider.name}" but is not listed in its grants. ` +
|
|
93
|
+
`Add "${consumerKey}" to the provider's grants field to allow access.`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
// Not referenced and not granted — silently exclude
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
mergeProviderIntoContext(celContext, provider.kind, provider.name, provider.context);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return celContext;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Build the namespace prefix string that appears inside a `${{ }}` expression
|
|
109
|
+
* when a consumer references this provider.
|
|
110
|
+
*
|
|
111
|
+
* The trailing dot ensures we match the start of a property access without
|
|
112
|
+
* false-matching a shared prefix substring:
|
|
113
|
+
* kind="Config", name="database" → "Config.database."
|
|
114
|
+
* kind="MyApp.Secrets", name="vault" → "MyApp.Secrets.vault."
|
|
115
|
+
*/
|
|
116
|
+
function buildProviderNamespacePrefix(kind: string, name: string): string {
|
|
117
|
+
return `${kind}.${name}.`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Scan serialized manifest JSON for any `${{ expression }}` block that contains
|
|
122
|
+
* the provider's namespace prefix.
|
|
123
|
+
*
|
|
124
|
+
* Using JSON.stringify() as the scan target covers all nested fields uniformly.
|
|
125
|
+
* The approach cannot produce false positives from field names or non-template
|
|
126
|
+
* values because `${{` is not valid JSON syntax outside a string value.
|
|
127
|
+
*/
|
|
128
|
+
function manifestReferencesNamespace(manifestJson: string, namespacePrefix: string): boolean {
|
|
129
|
+
let searchFrom = 0;
|
|
130
|
+
while (true) {
|
|
131
|
+
const start = manifestJson.indexOf("${{", searchFrom);
|
|
132
|
+
if (start === -1) break;
|
|
133
|
+
const end = manifestJson.indexOf("}}", start + 3);
|
|
134
|
+
if (end === -1) break;
|
|
135
|
+
const expression = manifestJson.slice(start + 3, end);
|
|
136
|
+
if (expression.includes(namespacePrefix)) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
searchFrom = end + 2;
|
|
140
|
+
}
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Place provider.context into the CEL context object under the path
|
|
146
|
+
* defined by the provider's kind segments and name.
|
|
147
|
+
*
|
|
148
|
+
* kind="Config", name="database":
|
|
149
|
+
* context.Config = { database: { host, port } }
|
|
150
|
+
*
|
|
151
|
+
* kind="MyApp.Api", name="server":
|
|
152
|
+
* context.MyApp = { Api: { server: { ... } } }
|
|
153
|
+
*/
|
|
154
|
+
function mergeProviderIntoContext(
|
|
155
|
+
celContext: Record<string, unknown>,
|
|
156
|
+
kind: string,
|
|
157
|
+
name: string,
|
|
158
|
+
providerData: Record<string, unknown>,
|
|
159
|
+
): void {
|
|
160
|
+
const kindSegments = kind.split(".");
|
|
161
|
+
let cursor = celContext;
|
|
162
|
+
for (const segment of kindSegments) {
|
|
163
|
+
if (typeof cursor[segment] !== "object" || cursor[segment] === null) {
|
|
164
|
+
cursor[segment] = {};
|
|
165
|
+
}
|
|
166
|
+
cursor = cursor[segment] as Record<string, unknown>;
|
|
167
|
+
}
|
|
168
|
+
cursor[name] = providerData;
|
|
169
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { ControllerInstance, RuntimeError } from "@telorun/sdk";
|
|
2
|
+
import { execFile } from "child_process";
|
|
3
|
+
import { createHash } from "crypto";
|
|
4
|
+
import * as fs from "fs/promises";
|
|
5
|
+
import * as os from "os";
|
|
6
|
+
import { PackageURL } from "packageurl-js";
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import { promisify } from "util";
|
|
9
|
+
|
|
10
|
+
const homedir = os.homedir();
|
|
11
|
+
const cacheRoot = path.join(homedir, ".cache", "telo");
|
|
12
|
+
const npmCacheRoot = path.join(cacheRoot, "npm");
|
|
13
|
+
const isBun = typeof (globalThis as any).Bun !== "undefined";
|
|
14
|
+
|
|
15
|
+
export class ControllerLoader {
|
|
16
|
+
/**
|
|
17
|
+
* Load controller instance from URI in format:
|
|
18
|
+
*
|
|
19
|
+
* <runtime>:<registry>:<path>@<version-spec>
|
|
20
|
+
*/
|
|
21
|
+
async load(purlCandidates: string[], baseUri: string): Promise<ControllerInstance> {
|
|
22
|
+
if (!purlCandidates || purlCandidates.length === 0) {
|
|
23
|
+
throw new RuntimeError("ERR_CONTROLLER_NOT_FOUND", "Missing controller PURL candidates");
|
|
24
|
+
}
|
|
25
|
+
const purl = purlCandidates.find((p) => p.startsWith("pkg:npm"));
|
|
26
|
+
if (!purl) {
|
|
27
|
+
throw new RuntimeError(
|
|
28
|
+
"ERR_CONTROLLER_NOT_FOUND",
|
|
29
|
+
"Controller PURL candidates not applicable",
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const [type, namespace, name, versionSpec, qualifiers, entry] = PackageURL.parseString(purl);
|
|
33
|
+
|
|
34
|
+
const localPath = (qualifiers as any)?.get("local_path");
|
|
35
|
+
const cacheKey = createHash("sha256").update(purlCandidates[0]).digest("hex").slice(0, 12);
|
|
36
|
+
const installDir = path.join(npmCacheRoot, cacheKey);
|
|
37
|
+
|
|
38
|
+
let packageRoot: string;
|
|
39
|
+
const isLocalManifest =
|
|
40
|
+
baseUri && !baseUri.startsWith("http://") && !baseUri.startsWith("https://");
|
|
41
|
+
if (localPath && isLocalManifest) {
|
|
42
|
+
const manifestDir = path.dirname(baseUri);
|
|
43
|
+
const resolvedLocalPath = path.resolve(manifestDir, localPath);
|
|
44
|
+
if (await this.pathExists(resolvedLocalPath)) {
|
|
45
|
+
packageRoot = resolvedLocalPath;
|
|
46
|
+
} else {
|
|
47
|
+
const nodeModulesPath = await this.findInNodeModules(`${namespace}/${name}`);
|
|
48
|
+
if (nodeModulesPath) {
|
|
49
|
+
packageRoot = nodeModulesPath;
|
|
50
|
+
} else {
|
|
51
|
+
await this.ensureNpmPackageInstalled(installDir, `${namespace}/${name}@${versionSpec}`);
|
|
52
|
+
packageRoot = this.getInstalledPackageRoot(installDir, `${namespace}/${name}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else {
|
|
56
|
+
const nodeModulesPath = await this.findInNodeModules(`${namespace}/${name}`);
|
|
57
|
+
if (nodeModulesPath) {
|
|
58
|
+
packageRoot = nodeModulesPath;
|
|
59
|
+
} else {
|
|
60
|
+
await this.ensureNpmPackageInstalled(installDir, `${namespace}/${name}@${versionSpec}`);
|
|
61
|
+
packageRoot = this.getInstalledPackageRoot(installDir, `${namespace}/${name}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const entryFile = await this.resolvePackageEntry(packageRoot, entry ? `./${entry}` : ".");
|
|
66
|
+
const instance = await import(entryFile);
|
|
67
|
+
if (!instance || (!instance.create && !instance.register)) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Invalid controller loaded from "${purlCandidates[0]}": missing create or register function`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return instance;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async ensureNpmPackageInstalled(installDir: string, packageSpec: string): Promise<void> {
|
|
76
|
+
const packageName = this.getPackageName(
|
|
77
|
+
packageSpec.startsWith(".") || path.isAbsolute(packageSpec)
|
|
78
|
+
? await this.getLocalPackageName(packageSpec)
|
|
79
|
+
: packageSpec,
|
|
80
|
+
);
|
|
81
|
+
const packageRoot = this.getInstalledPackageRoot(installDir, packageName);
|
|
82
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
83
|
+
if (await this.pathExists(packageJsonPath)) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await fs.mkdir(installDir, { recursive: true });
|
|
88
|
+
const rootPackageJson = path.join(installDir, "package.json");
|
|
89
|
+
if (!(await this.pathExists(rootPackageJson))) {
|
|
90
|
+
await fs.writeFile(
|
|
91
|
+
rootPackageJson,
|
|
92
|
+
JSON.stringify({ name: "telo-cache", private: true }, null, 2),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const execFileAsync = promisify(execFile);
|
|
97
|
+
const args = [
|
|
98
|
+
"install",
|
|
99
|
+
"--no-audit",
|
|
100
|
+
"--no-fund",
|
|
101
|
+
"--silent",
|
|
102
|
+
"--prefix",
|
|
103
|
+
installDir,
|
|
104
|
+
packageSpec,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
await execFileAsync("npm", args);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private getPackageName(packageSpec: string): string {
|
|
111
|
+
if (packageSpec.startsWith("@")) {
|
|
112
|
+
const lastAt = packageSpec.lastIndexOf("@");
|
|
113
|
+
return lastAt > 0 ? packageSpec.slice(0, lastAt) : packageSpec;
|
|
114
|
+
}
|
|
115
|
+
const [name] = packageSpec.split("@");
|
|
116
|
+
return name;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private getInstalledPackageRoot(installDir: string, packageName: string): string {
|
|
120
|
+
const nameParts = packageName.split("/");
|
|
121
|
+
return path.join(installDir, "node_modules", ...nameParts);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private async getLocalPackageName(packagePath: string): Promise<string> {
|
|
125
|
+
const packageJsonPath = path.join(packagePath, "package.json");
|
|
126
|
+
if (!(await this.pathExists(packageJsonPath))) {
|
|
127
|
+
throw new Error(`Local package missing package.json: ${packagePath}`);
|
|
128
|
+
}
|
|
129
|
+
const content = await fs.readFile(packageJsonPath, "utf8");
|
|
130
|
+
const parsed = JSON.parse(content);
|
|
131
|
+
if (!parsed?.name) {
|
|
132
|
+
throw new Error(`Local package missing name in package.json: ${packagePath}`);
|
|
133
|
+
}
|
|
134
|
+
return parsed.name;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private async resolvePackageEntry(
|
|
138
|
+
packageRoot: string,
|
|
139
|
+
entry: string,
|
|
140
|
+
packageName?: string,
|
|
141
|
+
): Promise<string> {
|
|
142
|
+
const packageJsonPath = path.join(packageRoot, "package.json");
|
|
143
|
+
let resolvedPackageName = packageName;
|
|
144
|
+
let packageJson: any = null;
|
|
145
|
+
if (!resolvedPackageName && (await this.pathExists(packageJsonPath))) {
|
|
146
|
+
const content = await fs.readFile(packageJsonPath, "utf8");
|
|
147
|
+
try {
|
|
148
|
+
packageJson = JSON.parse(content);
|
|
149
|
+
resolvedPackageName = packageJson?.name;
|
|
150
|
+
} catch {
|
|
151
|
+
resolvedPackageName = packageName;
|
|
152
|
+
}
|
|
153
|
+
} else if (await this.pathExists(packageJsonPath)) {
|
|
154
|
+
try {
|
|
155
|
+
packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
156
|
+
} catch {
|
|
157
|
+
packageJson = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const entryValue = entry.trim();
|
|
162
|
+
const exportTarget = this.resolvePackageExportTarget(packageJson?.exports, entryValue);
|
|
163
|
+
if (exportTarget) {
|
|
164
|
+
const resolved = path.resolve(packageRoot, exportTarget);
|
|
165
|
+
if (await this.pathExists(resolved)) {
|
|
166
|
+
return this.resolveForRuntime(resolved, packageRoot);
|
|
167
|
+
}
|
|
168
|
+
if (!path.extname(resolved)) {
|
|
169
|
+
const withJs = `${resolved}.js`;
|
|
170
|
+
if (await this.pathExists(withJs)) {
|
|
171
|
+
return withJs;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if ((entryValue === "." || entryValue === "./") && packageJson) {
|
|
176
|
+
const mainFields = ["module", "main"];
|
|
177
|
+
for (const field of mainFields) {
|
|
178
|
+
const target = packageJson[field];
|
|
179
|
+
if (typeof target === "string") {
|
|
180
|
+
const resolved = path.resolve(packageRoot, target);
|
|
181
|
+
if (await this.pathExists(resolved)) {
|
|
182
|
+
return this.resolveForRuntime(resolved, packageRoot);
|
|
183
|
+
}
|
|
184
|
+
if (!path.extname(resolved)) {
|
|
185
|
+
const withJs = `${resolved}.js`;
|
|
186
|
+
if (await this.pathExists(withJs)) {
|
|
187
|
+
return withJs;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const directPath = path.resolve(packageRoot, entryValue);
|
|
195
|
+
if (await this.pathExists(directPath)) {
|
|
196
|
+
return this.resolveForRuntime(directPath, packageRoot);
|
|
197
|
+
}
|
|
198
|
+
if (!path.extname(directPath)) {
|
|
199
|
+
const withJs = `${directPath}.js`;
|
|
200
|
+
if (await this.pathExists(withJs)) {
|
|
201
|
+
return withJs;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
throw new Error(`Controller entry "${entryValue}" could not be resolved in ${packageRoot}`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private resolvePackageExportTarget(exportsField: any, entry: string): string | null {
|
|
209
|
+
if (!exportsField) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const key = entry === "." || entry === "./" ? "." : entry;
|
|
214
|
+
const target = exportsField[key];
|
|
215
|
+
return this.resolveExportTargetValue(target);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private resolveExportTargetValue(target: any): string | null {
|
|
219
|
+
if (!target) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
if (typeof target === "string") {
|
|
223
|
+
return target;
|
|
224
|
+
}
|
|
225
|
+
if (Array.isArray(target)) {
|
|
226
|
+
for (const item of target) {
|
|
227
|
+
const resolved = this.resolveExportTargetValue(item);
|
|
228
|
+
if (resolved) {
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
if (typeof target === "object") {
|
|
235
|
+
const preferredKeys = isBun
|
|
236
|
+
? ["bun", "import", "default", "require"]
|
|
237
|
+
: ["import", "default", "require"];
|
|
238
|
+
for (const key of preferredKeys) {
|
|
239
|
+
if (target[key]) {
|
|
240
|
+
const resolved = this.resolveExportTargetValue(target[key]);
|
|
241
|
+
if (resolved) {
|
|
242
|
+
return resolved;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* For Node.js, resolve .ts paths to their compiled .js equivalents in dist/.
|
|
252
|
+
* Bun can load .ts directly, so it returns the path unchanged.
|
|
253
|
+
*/
|
|
254
|
+
private async resolveForRuntime(resolvedPath: string, packageRoot: string): Promise<string> {
|
|
255
|
+
if (isBun || !resolvedPath.endsWith(".ts")) {
|
|
256
|
+
return resolvedPath;
|
|
257
|
+
}
|
|
258
|
+
// Try dist/ equivalent: src/foo.ts -> dist/foo.js
|
|
259
|
+
const relative = path.relative(packageRoot, resolvedPath);
|
|
260
|
+
const distEquivalent = path.resolve(
|
|
261
|
+
packageRoot,
|
|
262
|
+
relative.replace(/^src\//, "dist/").replace(/\.ts$/, ".js"),
|
|
263
|
+
);
|
|
264
|
+
if (await this.pathExists(distEquivalent)) {
|
|
265
|
+
return distEquivalent;
|
|
266
|
+
}
|
|
267
|
+
// Fallback: same location but .js
|
|
268
|
+
const jsPath = resolvedPath.replace(/\.ts$/, ".js");
|
|
269
|
+
if (await this.pathExists(jsPath)) {
|
|
270
|
+
return jsPath;
|
|
271
|
+
}
|
|
272
|
+
return resolvedPath;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async findInNodeModules(packageName: string): Promise<string | null> {
|
|
276
|
+
const nameParts = packageName.split("/");
|
|
277
|
+
const candidates = [
|
|
278
|
+
path.join(process.cwd(), "node_modules", ...nameParts),
|
|
279
|
+
path.join(process.cwd(), "node_modules", ".pnpm", "node_modules", ...nameParts),
|
|
280
|
+
];
|
|
281
|
+
for (const candidate of candidates) {
|
|
282
|
+
const packageJsonPath = path.join(candidate, "package.json");
|
|
283
|
+
if (await this.pathExists(packageJsonPath)) {
|
|
284
|
+
return candidate;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private async pathExists(filePath: string): Promise<boolean> {
|
|
291
|
+
try {
|
|
292
|
+
await fs.access(filePath);
|
|
293
|
+
return true;
|
|
294
|
+
} catch {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|