@lorion-org/capability-composition 1.0.0-beta.3
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.cjs +86 -0
- package/dist/index.d.cts +35 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +64 -0
- package/package.json +70 -0
- package/src/index.ts +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the 'Software'), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @lorion-org/capability-composition
|
|
2
|
+
|
|
3
|
+
Framework-free capability composition over the LORION core.
|
|
4
|
+
|
|
5
|
+
It resolves a descriptor-defined capability set (discovery, dependency-graph selection, provider selection, and seeding), detects surfaces by a host-defined convention, and composes the active capabilities into any runtime. One composition path is shared by build-time hosts (for example a Vite adapter) and runtime hosts (for example a Bun server); each host supplies only its activation convention and its registration.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
pnpm add @lorion-org/capability-composition
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## API
|
|
14
|
+
|
|
15
|
+
- `resolveSelectedCapabilities({ workspaceRoot, capabilitiesDir, seed })` resolves the active capabilities: base descriptors, the selection seed, transitive dependencies, and exactly one provider per capability.
|
|
16
|
+
- `conventionActivation(surfaces)` builds an activation resolver from per-surface conventions (a file-layout marker plus an export-name derivation), so descriptors carry no surface config. Re-exported from [`@lorion-org/surface-activation`](../surface-activation), which owns the addressing convention.
|
|
17
|
+
- `composeCapabilities({ workspaceRoot, capabilitiesDir, seed, surface, activation, load, register })` resolves the active set and, for each capability that provides the surface, loads its module and hands the exported value to the host's registration. Registry- and framework-agnostic.
|
|
18
|
+
- Build-time hosts that code-generate static imports use `resolveSurfaceModules` from [`@lorion-org/surface-activation`](../surface-activation) directly — the same seam `composeCapabilities` uses internally. It is intentionally not re-exported here, so a build-time host depends only on the light addressing package, not this runtime host.
|
|
19
|
+
|
|
20
|
+
## What It Is Not
|
|
21
|
+
|
|
22
|
+
- not a framework runtime or plugin registry
|
|
23
|
+
- not a bundler or a router
|
|
24
|
+
- not an application naming convention
|
|
25
|
+
|
|
26
|
+
## Composition timing: runtime vs build-time
|
|
27
|
+
|
|
28
|
+
The same descriptor selection drives two host styles, differing only in _when_
|
|
29
|
+
composition runs and _how_ modules are loaded:
|
|
30
|
+
|
|
31
|
+
- **Runtime** — call `composeCapabilities` at boot with a dynamic
|
|
32
|
+
`load: (specifier) => import(specifier)`. Simple and fine for a source-run
|
|
33
|
+
server that starts once; resolution is a one-time boot cost.
|
|
34
|
+
- **Build-time** — run `resolveSelectedCapabilities` (here) + `resolveSurfaceModules`
|
|
35
|
+
(from `@lorion-org/surface-activation`) in a build step and code-generate _static_
|
|
36
|
+
imports. The injected set is fixed and
|
|
37
|
+
auditable at build time, with no runtime discovery or dynamic `import()` —
|
|
38
|
+
suited to bundled or air-gapped artifacts.
|
|
39
|
+
|
|
40
|
+
Both compose the identical set from one seam — `resolveSurfaceModules` in
|
|
41
|
+
`@lorion-org/surface-activation`. See `snippets/buildtime-composition.ts` for the
|
|
42
|
+
build-time manifest.
|
|
43
|
+
|
|
44
|
+
## Local Commands
|
|
45
|
+
|
|
46
|
+
```shell
|
|
47
|
+
cd packages/capability-composition
|
|
48
|
+
pnpm build
|
|
49
|
+
pnpm test
|
|
50
|
+
pnpm typecheck
|
|
51
|
+
pnpm package:check
|
|
52
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
composeCapabilities: () => composeCapabilities,
|
|
24
|
+
conventionActivation: () => import_surface_activation2.conventionActivation,
|
|
25
|
+
fileSurfaceConvention: () => import_surface_activation2.fileSurfaceConvention,
|
|
26
|
+
resolveSelectedCapabilities: () => resolveSelectedCapabilities
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
var import_node_fs = require("fs");
|
|
30
|
+
var import_node_path = require("path");
|
|
31
|
+
var import_descriptor_discovery = require("@lorion-org/descriptor-discovery");
|
|
32
|
+
var import_descriptor_selection = require("@lorion-org/descriptor-selection");
|
|
33
|
+
var import_surface_activation = require("@lorion-org/surface-activation");
|
|
34
|
+
var import_surface_activation2 = require("@lorion-org/surface-activation");
|
|
35
|
+
function readPackageName(directory) {
|
|
36
|
+
const path = (0, import_node_path.resolve)(directory, "package.json");
|
|
37
|
+
const json = JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
38
|
+
return (0, import_descriptor_discovery.requirePackageName)(json, path);
|
|
39
|
+
}
|
|
40
|
+
function resolveSelectedCapabilities(options) {
|
|
41
|
+
const capabilitiesDir = options.capabilitiesDir ?? "capabilities";
|
|
42
|
+
const items = (0, import_descriptor_discovery.discoverDescriptors)({
|
|
43
|
+
cwd: options.workspaceRoot,
|
|
44
|
+
descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
|
|
45
|
+
validation: { schema: import_descriptor_discovery.descriptorSchema }
|
|
46
|
+
}).map((entry) => ({
|
|
47
|
+
id: entry.descriptor.id,
|
|
48
|
+
directory: entry.cwd,
|
|
49
|
+
descriptor: entry.descriptor
|
|
50
|
+
}));
|
|
51
|
+
const selected = (0, import_descriptor_selection.selectDescriptors)({
|
|
52
|
+
items,
|
|
53
|
+
getDescriptor: (item) => item.descriptor,
|
|
54
|
+
withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
|
|
55
|
+
seed: options.seed,
|
|
56
|
+
...options.policy ? { policy: options.policy } : {}
|
|
57
|
+
});
|
|
58
|
+
return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
|
|
59
|
+
}
|
|
60
|
+
async function composeCapabilities(options) {
|
|
61
|
+
const active = resolveSelectedCapabilities({
|
|
62
|
+
workspaceRoot: options.workspaceRoot,
|
|
63
|
+
capabilitiesDir: options.capabilitiesDir ?? "capabilities",
|
|
64
|
+
seed: options.seed
|
|
65
|
+
});
|
|
66
|
+
const activated = [];
|
|
67
|
+
for (const { capability, specifier, exportName } of (0, import_surface_activation.resolveSurfaceModules)(
|
|
68
|
+
active,
|
|
69
|
+
options.surface,
|
|
70
|
+
options.activation
|
|
71
|
+
)) {
|
|
72
|
+
const module2 = await options.load(specifier);
|
|
73
|
+
const exportValue = module2[exportName];
|
|
74
|
+
if (exportValue === void 0) continue;
|
|
75
|
+
await options.register(exportValue, capability);
|
|
76
|
+
activated.push(capability);
|
|
77
|
+
}
|
|
78
|
+
return activated;
|
|
79
|
+
}
|
|
80
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
81
|
+
0 && (module.exports = {
|
|
82
|
+
composeCapabilities,
|
|
83
|
+
conventionActivation,
|
|
84
|
+
fileSurfaceConvention,
|
|
85
|
+
resolveSelectedCapabilities
|
|
86
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DescriptorId, Descriptor, CompositionPolicy } from '@lorion-org/composition-graph';
|
|
2
|
+
import { SurfaceCapability, ActivationResolver } from '@lorion-org/surface-activation';
|
|
3
|
+
export { ActivationResolver, FileSurfaceConventionOptions, SurfaceActivation, SurfaceConvention, conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
|
|
4
|
+
|
|
5
|
+
interface CapabilitySelectionSeed {
|
|
6
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
7
|
+
defaultSelection?: readonly DescriptorId[];
|
|
8
|
+
selected?: readonly DescriptorId[];
|
|
9
|
+
selectionSeed?: false | {
|
|
10
|
+
argv?: string[];
|
|
11
|
+
cliKeys?: string[];
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
envKeys?: string[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface ResolvedCapability extends SurfaceCapability {
|
|
17
|
+
descriptor: Descriptor;
|
|
18
|
+
}
|
|
19
|
+
declare function resolveSelectedCapabilities(options: {
|
|
20
|
+
workspaceRoot: string;
|
|
21
|
+
capabilitiesDir?: string;
|
|
22
|
+
seed: CapabilitySelectionSeed;
|
|
23
|
+
policy?: Partial<CompositionPolicy>;
|
|
24
|
+
}): ResolvedCapability[];
|
|
25
|
+
declare function composeCapabilities(options: {
|
|
26
|
+
workspaceRoot: string;
|
|
27
|
+
capabilitiesDir?: string;
|
|
28
|
+
seed: CapabilitySelectionSeed;
|
|
29
|
+
surface: string;
|
|
30
|
+
activation: ActivationResolver;
|
|
31
|
+
load: (specifier: string) => Promise<Record<string, unknown>>;
|
|
32
|
+
register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
|
|
33
|
+
}): Promise<ResolvedCapability[]>;
|
|
34
|
+
|
|
35
|
+
export { type CapabilitySelectionSeed, type ResolvedCapability, composeCapabilities, resolveSelectedCapabilities };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DescriptorId, Descriptor, CompositionPolicy } from '@lorion-org/composition-graph';
|
|
2
|
+
import { SurfaceCapability, ActivationResolver } from '@lorion-org/surface-activation';
|
|
3
|
+
export { ActivationResolver, FileSurfaceConventionOptions, SurfaceActivation, SurfaceConvention, conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
|
|
4
|
+
|
|
5
|
+
interface CapabilitySelectionSeed {
|
|
6
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
7
|
+
defaultSelection?: readonly DescriptorId[];
|
|
8
|
+
selected?: readonly DescriptorId[];
|
|
9
|
+
selectionSeed?: false | {
|
|
10
|
+
argv?: string[];
|
|
11
|
+
cliKeys?: string[];
|
|
12
|
+
env?: Record<string, string | undefined>;
|
|
13
|
+
envKeys?: string[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
interface ResolvedCapability extends SurfaceCapability {
|
|
17
|
+
descriptor: Descriptor;
|
|
18
|
+
}
|
|
19
|
+
declare function resolveSelectedCapabilities(options: {
|
|
20
|
+
workspaceRoot: string;
|
|
21
|
+
capabilitiesDir?: string;
|
|
22
|
+
seed: CapabilitySelectionSeed;
|
|
23
|
+
policy?: Partial<CompositionPolicy>;
|
|
24
|
+
}): ResolvedCapability[];
|
|
25
|
+
declare function composeCapabilities(options: {
|
|
26
|
+
workspaceRoot: string;
|
|
27
|
+
capabilitiesDir?: string;
|
|
28
|
+
seed: CapabilitySelectionSeed;
|
|
29
|
+
surface: string;
|
|
30
|
+
activation: ActivationResolver;
|
|
31
|
+
load: (specifier: string) => Promise<Record<string, unknown>>;
|
|
32
|
+
register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
|
|
33
|
+
}): Promise<ResolvedCapability[]>;
|
|
34
|
+
|
|
35
|
+
export { type CapabilitySelectionSeed, type ResolvedCapability, composeCapabilities, resolveSelectedCapabilities };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { readFileSync } from "fs";
|
|
3
|
+
import { resolve } from "path";
|
|
4
|
+
import {
|
|
5
|
+
descriptorSchema,
|
|
6
|
+
discoverDescriptors,
|
|
7
|
+
requirePackageName
|
|
8
|
+
} from "@lorion-org/descriptor-discovery";
|
|
9
|
+
import { selectDescriptors } from "@lorion-org/descriptor-selection";
|
|
10
|
+
import {
|
|
11
|
+
resolveSurfaceModules
|
|
12
|
+
} from "@lorion-org/surface-activation";
|
|
13
|
+
import { conventionActivation, fileSurfaceConvention } from "@lorion-org/surface-activation";
|
|
14
|
+
function readPackageName(directory) {
|
|
15
|
+
const path = resolve(directory, "package.json");
|
|
16
|
+
const json = JSON.parse(readFileSync(path, "utf8"));
|
|
17
|
+
return requirePackageName(json, path);
|
|
18
|
+
}
|
|
19
|
+
function resolveSelectedCapabilities(options) {
|
|
20
|
+
const capabilitiesDir = options.capabilitiesDir ?? "capabilities";
|
|
21
|
+
const items = discoverDescriptors({
|
|
22
|
+
cwd: options.workspaceRoot,
|
|
23
|
+
descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
|
|
24
|
+
validation: { schema: descriptorSchema }
|
|
25
|
+
}).map((entry) => ({
|
|
26
|
+
id: entry.descriptor.id,
|
|
27
|
+
directory: entry.cwd,
|
|
28
|
+
descriptor: entry.descriptor
|
|
29
|
+
}));
|
|
30
|
+
const selected = selectDescriptors({
|
|
31
|
+
items,
|
|
32
|
+
getDescriptor: (item) => item.descriptor,
|
|
33
|
+
withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
|
|
34
|
+
seed: options.seed,
|
|
35
|
+
...options.policy ? { policy: options.policy } : {}
|
|
36
|
+
});
|
|
37
|
+
return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
|
|
38
|
+
}
|
|
39
|
+
async function composeCapabilities(options) {
|
|
40
|
+
const active = resolveSelectedCapabilities({
|
|
41
|
+
workspaceRoot: options.workspaceRoot,
|
|
42
|
+
capabilitiesDir: options.capabilitiesDir ?? "capabilities",
|
|
43
|
+
seed: options.seed
|
|
44
|
+
});
|
|
45
|
+
const activated = [];
|
|
46
|
+
for (const { capability, specifier, exportName } of resolveSurfaceModules(
|
|
47
|
+
active,
|
|
48
|
+
options.surface,
|
|
49
|
+
options.activation
|
|
50
|
+
)) {
|
|
51
|
+
const module = await options.load(specifier);
|
|
52
|
+
const exportValue = module[exportName];
|
|
53
|
+
if (exportValue === void 0) continue;
|
|
54
|
+
await options.register(exportValue, capability);
|
|
55
|
+
activated.push(capability);
|
|
56
|
+
}
|
|
57
|
+
return activated;
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
composeCapabilities,
|
|
61
|
+
conventionActivation,
|
|
62
|
+
fileSurfaceConvention,
|
|
63
|
+
resolveSelectedCapabilities
|
|
64
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lorion-org/capability-composition",
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
|
+
"description": "Framework-free capability composition: on-disk descriptor selection and runtime/build-time composition, built on the surface-activation convention.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lorion-org/lorion.git",
|
|
9
|
+
"directory": "packages/capability-composition"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/lorion-org/lorion/tree/main/packages/capability-composition#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/lorion-org/lorion/issues"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"main": "./dist/index.cjs",
|
|
21
|
+
"module": "./dist/index.js",
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"lorion-source": {
|
|
26
|
+
"types": "./src/index.ts",
|
|
27
|
+
"default": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"default": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/index.d.cts",
|
|
35
|
+
"default": "./dist/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"src/**/*.ts",
|
|
43
|
+
"!src/**/*.spec.ts"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm,cjs --dts",
|
|
47
|
+
"clean": "rimraf coverage dist tsconfig.tsbuildinfo",
|
|
48
|
+
"lint": "eslint src --ext .ts",
|
|
49
|
+
"prepack": "pnpm build",
|
|
50
|
+
"test": "vitest run --root ../.. --config vitest.config.mts packages/capability-composition/src/index.spec.ts",
|
|
51
|
+
"coverage": "node -e \"require('node:fs').mkdirSync('../../coverage/.tmp', { recursive: true })\" && vitest run --coverage --coverage.include=packages/capability-composition/src/**/*.ts --root ../.. --config vitest.config.mts packages/capability-composition/src/index.spec.ts",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"package:check": "pnpm build && pnpm pack --dry-run && publint"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"@lorion-org/composition-graph": "workspace:^",
|
|
57
|
+
"@lorion-org/descriptor-discovery": "workspace:^",
|
|
58
|
+
"@lorion-org/descriptor-selection": "workspace:^",
|
|
59
|
+
"@lorion-org/surface-activation": "workspace:^"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"lorion",
|
|
63
|
+
"capability",
|
|
64
|
+
"composition",
|
|
65
|
+
"typescript"
|
|
66
|
+
],
|
|
67
|
+
"engines": {
|
|
68
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import type { CompositionPolicy, Descriptor, DescriptorId } from '@lorion-org/composition-graph';
|
|
4
|
+
import {
|
|
5
|
+
descriptorSchema,
|
|
6
|
+
discoverDescriptors,
|
|
7
|
+
requirePackageName,
|
|
8
|
+
} from '@lorion-org/descriptor-discovery';
|
|
9
|
+
import { selectDescriptors } from '@lorion-org/descriptor-selection';
|
|
10
|
+
import {
|
|
11
|
+
type ActivationResolver,
|
|
12
|
+
resolveSurfaceModules,
|
|
13
|
+
type SurfaceCapability,
|
|
14
|
+
} from '@lorion-org/surface-activation';
|
|
15
|
+
|
|
16
|
+
// Capability composition: descriptor-defined capabilities that live as filesystem
|
|
17
|
+
// packages, composed into a host. This package owns disk discovery and the
|
|
18
|
+
// runtime/build-time compose loop; resolving the active set (seed, dependencies,
|
|
19
|
+
// one provider per capability) is delegated to @lorion-org/descriptor-selection and
|
|
20
|
+
// the surface-addressing convention to @lorion-org/surface-activation, so no logic
|
|
21
|
+
// is duplicated here.
|
|
22
|
+
|
|
23
|
+
// Re-export the convention-building companions a `composeCapabilities` caller needs
|
|
24
|
+
// to build the activation it passes in: `conventionActivation`, the
|
|
25
|
+
// `fileSurfaceConvention` preset, and the describing types. The build-time
|
|
26
|
+
// addressing tools (`resolveSurfaceModules`, `capabilitySpecifier`) stay owned
|
|
27
|
+
// solely by @lorion-org/surface-activation, so a build-time host depends on that
|
|
28
|
+
// light package directly instead of pulling in this runtime host.
|
|
29
|
+
export { conventionActivation, fileSurfaceConvention } from '@lorion-org/surface-activation';
|
|
30
|
+
export type {
|
|
31
|
+
ActivationResolver,
|
|
32
|
+
FileSurfaceConventionOptions,
|
|
33
|
+
SurfaceActivation,
|
|
34
|
+
SurfaceConvention,
|
|
35
|
+
} from '@lorion-org/surface-activation';
|
|
36
|
+
|
|
37
|
+
export interface CapabilitySelectionSeed {
|
|
38
|
+
baseDescriptors?: readonly DescriptorId[];
|
|
39
|
+
defaultSelection?: readonly DescriptorId[];
|
|
40
|
+
selected?: readonly DescriptorId[];
|
|
41
|
+
selectionSeed?:
|
|
42
|
+
| false
|
|
43
|
+
| {
|
|
44
|
+
argv?: string[];
|
|
45
|
+
cliKeys?: string[];
|
|
46
|
+
env?: Record<string, string | undefined>;
|
|
47
|
+
envKeys?: string[];
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResolvedCapability extends SurfaceCapability {
|
|
52
|
+
descriptor: Descriptor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readPackageName(directory: string): string {
|
|
56
|
+
const path = resolve(directory, 'package.json');
|
|
57
|
+
const json = JSON.parse(readFileSync(path, 'utf8')) as { name?: unknown };
|
|
58
|
+
return requirePackageName(json, path);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Resolves the active capability set: base + seed + transitive dependencies +
|
|
62
|
+
// exactly one provider per capability, over capabilities discovered on disk.
|
|
63
|
+
// The selection itself is owned by @lorion-org/descriptor-selection.
|
|
64
|
+
export function resolveSelectedCapabilities(options: {
|
|
65
|
+
workspaceRoot: string;
|
|
66
|
+
capabilitiesDir?: string;
|
|
67
|
+
seed: CapabilitySelectionSeed;
|
|
68
|
+
policy?: Partial<CompositionPolicy>;
|
|
69
|
+
}): ResolvedCapability[] {
|
|
70
|
+
const capabilitiesDir = options.capabilitiesDir ?? 'capabilities';
|
|
71
|
+
const items = discoverDescriptors({
|
|
72
|
+
cwd: options.workspaceRoot,
|
|
73
|
+
descriptorPaths: [`${capabilitiesDir}/*/capability.json`],
|
|
74
|
+
validation: { schema: descriptorSchema },
|
|
75
|
+
}).map((entry) => ({
|
|
76
|
+
id: entry.descriptor.id,
|
|
77
|
+
directory: entry.cwd,
|
|
78
|
+
descriptor: entry.descriptor,
|
|
79
|
+
}));
|
|
80
|
+
|
|
81
|
+
const selected = selectDescriptors({
|
|
82
|
+
items,
|
|
83
|
+
getDescriptor: (item) => item.descriptor,
|
|
84
|
+
withDescriptor: (item, descriptor) => ({ ...item, descriptor }),
|
|
85
|
+
seed: options.seed,
|
|
86
|
+
...(options.policy ? { policy: options.policy } : {}),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Read package.json only for the resolved set: an unrelated broken or nameless
|
|
90
|
+
// package.json must not abort a composition that never imports that capability.
|
|
91
|
+
return selected.map((item) => ({ ...item, packageName: readPackageName(item.directory) }));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Runtime composition: resolve the active set, and for each capability that
|
|
95
|
+
// provides the requested surface, load its module and hand the exported value to
|
|
96
|
+
// the host's registration. Registry- and framework-agnostic. A build-time host
|
|
97
|
+
// uses `resolveSurfaceModules` directly to emit static imports instead.
|
|
98
|
+
export async function composeCapabilities(options: {
|
|
99
|
+
workspaceRoot: string;
|
|
100
|
+
capabilitiesDir?: string;
|
|
101
|
+
seed: CapabilitySelectionSeed;
|
|
102
|
+
surface: string;
|
|
103
|
+
activation: ActivationResolver;
|
|
104
|
+
load: (specifier: string) => Promise<Record<string, unknown>>;
|
|
105
|
+
register: (exportValue: unknown, capability: ResolvedCapability) => void | Promise<void>;
|
|
106
|
+
}): Promise<ResolvedCapability[]> {
|
|
107
|
+
const active = resolveSelectedCapabilities({
|
|
108
|
+
workspaceRoot: options.workspaceRoot,
|
|
109
|
+
capabilitiesDir: options.capabilitiesDir ?? 'capabilities',
|
|
110
|
+
seed: options.seed,
|
|
111
|
+
});
|
|
112
|
+
const activated: ResolvedCapability[] = [];
|
|
113
|
+
|
|
114
|
+
for (const { capability, specifier, exportName } of resolveSurfaceModules(
|
|
115
|
+
active,
|
|
116
|
+
options.surface,
|
|
117
|
+
options.activation,
|
|
118
|
+
)) {
|
|
119
|
+
const module = await options.load(specifier);
|
|
120
|
+
const exportValue = module[exportName];
|
|
121
|
+
if (exportValue === undefined) continue;
|
|
122
|
+
await options.register(exportValue, capability);
|
|
123
|
+
activated.push(capability);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return activated;
|
|
127
|
+
}
|