@lorion-org/surface-activation 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 +70 -0
- package/dist/index.cjs +73 -0
- package/dist/index.d.cts +36 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.js +45 -0
- package/package.json +65 -0
- package/src/index.ts +131 -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,70 @@
|
|
|
1
|
+
# @lorion-org/surface-activation
|
|
2
|
+
|
|
3
|
+
Framework-free **surface activation**: given a capability that lives as an on-disk
|
|
4
|
+
package, decide _which module and exported symbol_ provides a named "surface" (for
|
|
5
|
+
example `web` or `server`), and build the import specifier to reach it — by
|
|
6
|
+
file-layout convention, with no surface config in the descriptor.
|
|
7
|
+
|
|
8
|
+
It is pure: no filesystem access, no framework, no runtime. The same seam is shared
|
|
9
|
+
by build-time hosts (which code-generate static imports) and runtime hosts (which
|
|
10
|
+
dynamically `import()`), so the addressing rule lives in exactly one place instead
|
|
11
|
+
of being reinvented per host.
|
|
12
|
+
|
|
13
|
+
## Concepts
|
|
14
|
+
|
|
15
|
+
- **`SurfaceConvention`** — how a host detects a surface (`marker`), names its
|
|
16
|
+
export (`exportName(id)`), and where it is exported from (`exportSubpath`).
|
|
17
|
+
- **`conventionActivation(surfaces)`** — builds an `ActivationResolver` from
|
|
18
|
+
per-surface conventions.
|
|
19
|
+
- **`fileSurfaceConvention(options)`** — a ready-made `SurfaceConvention` for the
|
|
20
|
+
common file-layout case: the surface exists when one of `files` is present, and its
|
|
21
|
+
export is `camelCase(id) + exportSuffix`. It bakes the marker and naming so a host
|
|
22
|
+
stops repeating them per surface; existence is injected (`exists`) so this package
|
|
23
|
+
stays I/O-free.
|
|
24
|
+
- **`capabilitySpecifier(packageName, exportSubpath)`** — the one rule for a
|
|
25
|
+
capability's import specifier (`@acme/shops` + `./web` → `@acme/shops/web`).
|
|
26
|
+
- **`resolveSurfaceModules(active, surface, activation)`** — for each active
|
|
27
|
+
capability that provides the surface, the specifier and export name to import.
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
import { existsSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import {
|
|
35
|
+
conventionActivation,
|
|
36
|
+
fileSurfaceConvention,
|
|
37
|
+
resolveSurfaceModules,
|
|
38
|
+
} from '@lorion-org/surface-activation';
|
|
39
|
+
|
|
40
|
+
const activation = conventionActivation({
|
|
41
|
+
web: fileSurfaceConvention({
|
|
42
|
+
files: ['src/web.ts'], // surface present when any listed file exists
|
|
43
|
+
exportSuffix: 'WebPlugin', // exportName = camelCase(id) + suffix
|
|
44
|
+
exportSubpath: './web',
|
|
45
|
+
exists: existsSync, // injected — the package itself touches no filesystem
|
|
46
|
+
join, // optional; defaults to a POSIX join
|
|
47
|
+
}),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// `active` is any list of { id, directory, packageName } items.
|
|
51
|
+
const modules = resolveSurfaceModules(active, 'web', activation);
|
|
52
|
+
// -> [{ capability, specifier: '@acme/shops/web', exportName: 'shopsWebPlugin' }, ...]
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
The convention is the only surface-specific part: swap `exportSuffix`/`exportSubpath`
|
|
56
|
+
(and `files`) for a `server` surface, an `api` surface, or any other. The raw
|
|
57
|
+
`SurfaceConvention` object stays available for cases the preset does not cover.
|
|
58
|
+
|
|
59
|
+
A build-time host emits a static `import` per entry; a runtime host feeds each
|
|
60
|
+
`specifier` to a dynamic `import()`. Both use the identical list.
|
|
61
|
+
|
|
62
|
+
## Consumers in this repo
|
|
63
|
+
|
|
64
|
+
- `@lorion-org/capability-composition` — feeds the specifiers to its runtime
|
|
65
|
+
`composeCapabilities` loop, and re-exports the convention builders
|
|
66
|
+
(`conventionActivation`, `fileSurfaceConvention`) and their types for callers of
|
|
67
|
+
that loop. The build-time addressing tools (`resolveSurfaceModules`,
|
|
68
|
+
`capabilitySpecifier`) are imported from this package directly.
|
|
69
|
+
- `@lorion-org/react` — uses `capabilitySpecifier` when code-generating static
|
|
70
|
+
capability imports at build time.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
capabilitySpecifier: () => capabilitySpecifier,
|
|
24
|
+
conventionActivation: () => conventionActivation,
|
|
25
|
+
fileSurfaceConvention: () => fileSurfaceConvention,
|
|
26
|
+
resolveSurfaceModules: () => resolveSurfaceModules
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
function conventionActivation(surfaces) {
|
|
30
|
+
return (surface, capability) => {
|
|
31
|
+
const convention = surfaces[surface];
|
|
32
|
+
if (!convention || !convention.marker(capability.directory)) return void 0;
|
|
33
|
+
return {
|
|
34
|
+
exportSubpath: convention.exportSubpath,
|
|
35
|
+
exportName: convention.exportName(capability.id)
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function camelCaseId(id) {
|
|
40
|
+
return id.replace(/^-+|-+$/g, "").replace(/-+([a-z0-9])/gi, (_match, char) => char.toUpperCase());
|
|
41
|
+
}
|
|
42
|
+
function fileSurfaceConvention(options) {
|
|
43
|
+
const { files, exportSubpath, exportSuffix = "", exists } = options;
|
|
44
|
+
const join = options.join ?? ((directory, file) => `${directory}/${file}`);
|
|
45
|
+
return {
|
|
46
|
+
marker: (directory) => files.some((file) => exists(join(directory, file))),
|
|
47
|
+
exportName: (id) => `${camelCaseId(id)}${exportSuffix}`,
|
|
48
|
+
exportSubpath
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function capabilitySpecifier(packageName, exportSubpath) {
|
|
52
|
+
return `${packageName}${exportSubpath.replace(/^\./, "")}`;
|
|
53
|
+
}
|
|
54
|
+
function resolveSurfaceModules(active, surface, activation) {
|
|
55
|
+
return active.flatMap((capability) => {
|
|
56
|
+
const entry = activation(surface, { directory: capability.directory, id: capability.id });
|
|
57
|
+
if (!entry) return [];
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
capability,
|
|
61
|
+
specifier: capabilitySpecifier(capability.packageName, entry.exportSubpath),
|
|
62
|
+
exportName: entry.exportName
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
capabilitySpecifier,
|
|
70
|
+
conventionActivation,
|
|
71
|
+
fileSurfaceConvention,
|
|
72
|
+
resolveSurfaceModules
|
|
73
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface SurfaceActivation {
|
|
2
|
+
exportSubpath: string;
|
|
3
|
+
exportName: string;
|
|
4
|
+
}
|
|
5
|
+
interface SurfaceConvention {
|
|
6
|
+
marker: (directory: string) => boolean;
|
|
7
|
+
exportName: (id: string) => string;
|
|
8
|
+
exportSubpath: string;
|
|
9
|
+
}
|
|
10
|
+
type ActivationResolver = (surface: string, capability: {
|
|
11
|
+
directory: string;
|
|
12
|
+
id: string;
|
|
13
|
+
}) => SurfaceActivation | undefined;
|
|
14
|
+
declare function conventionActivation(surfaces: Record<string, SurfaceConvention>): ActivationResolver;
|
|
15
|
+
interface FileSurfaceConventionOptions {
|
|
16
|
+
files: readonly string[];
|
|
17
|
+
exportSubpath: string;
|
|
18
|
+
exportSuffix?: string;
|
|
19
|
+
exists: (path: string) => boolean;
|
|
20
|
+
join?: (directory: string, file: string) => string;
|
|
21
|
+
}
|
|
22
|
+
declare function fileSurfaceConvention(options: FileSurfaceConventionOptions): SurfaceConvention;
|
|
23
|
+
declare function capabilitySpecifier(packageName: string, exportSubpath: string): string;
|
|
24
|
+
interface SurfaceCapability {
|
|
25
|
+
id: string;
|
|
26
|
+
directory: string;
|
|
27
|
+
packageName: string;
|
|
28
|
+
}
|
|
29
|
+
interface CapabilitySurfaceModule<T extends SurfaceCapability = SurfaceCapability> {
|
|
30
|
+
capability: T;
|
|
31
|
+
specifier: string;
|
|
32
|
+
exportName: string;
|
|
33
|
+
}
|
|
34
|
+
declare function resolveSurfaceModules<T extends SurfaceCapability>(active: readonly T[], surface: string, activation: ActivationResolver): CapabilitySurfaceModule<T>[];
|
|
35
|
+
|
|
36
|
+
export { type ActivationResolver, type CapabilitySurfaceModule, type FileSurfaceConventionOptions, type SurfaceActivation, type SurfaceCapability, type SurfaceConvention, capabilitySpecifier, conventionActivation, fileSurfaceConvention, resolveSurfaceModules };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
interface SurfaceActivation {
|
|
2
|
+
exportSubpath: string;
|
|
3
|
+
exportName: string;
|
|
4
|
+
}
|
|
5
|
+
interface SurfaceConvention {
|
|
6
|
+
marker: (directory: string) => boolean;
|
|
7
|
+
exportName: (id: string) => string;
|
|
8
|
+
exportSubpath: string;
|
|
9
|
+
}
|
|
10
|
+
type ActivationResolver = (surface: string, capability: {
|
|
11
|
+
directory: string;
|
|
12
|
+
id: string;
|
|
13
|
+
}) => SurfaceActivation | undefined;
|
|
14
|
+
declare function conventionActivation(surfaces: Record<string, SurfaceConvention>): ActivationResolver;
|
|
15
|
+
interface FileSurfaceConventionOptions {
|
|
16
|
+
files: readonly string[];
|
|
17
|
+
exportSubpath: string;
|
|
18
|
+
exportSuffix?: string;
|
|
19
|
+
exists: (path: string) => boolean;
|
|
20
|
+
join?: (directory: string, file: string) => string;
|
|
21
|
+
}
|
|
22
|
+
declare function fileSurfaceConvention(options: FileSurfaceConventionOptions): SurfaceConvention;
|
|
23
|
+
declare function capabilitySpecifier(packageName: string, exportSubpath: string): string;
|
|
24
|
+
interface SurfaceCapability {
|
|
25
|
+
id: string;
|
|
26
|
+
directory: string;
|
|
27
|
+
packageName: string;
|
|
28
|
+
}
|
|
29
|
+
interface CapabilitySurfaceModule<T extends SurfaceCapability = SurfaceCapability> {
|
|
30
|
+
capability: T;
|
|
31
|
+
specifier: string;
|
|
32
|
+
exportName: string;
|
|
33
|
+
}
|
|
34
|
+
declare function resolveSurfaceModules<T extends SurfaceCapability>(active: readonly T[], surface: string, activation: ActivationResolver): CapabilitySurfaceModule<T>[];
|
|
35
|
+
|
|
36
|
+
export { type ActivationResolver, type CapabilitySurfaceModule, type FileSurfaceConventionOptions, type SurfaceActivation, type SurfaceCapability, type SurfaceConvention, capabilitySpecifier, conventionActivation, fileSurfaceConvention, resolveSurfaceModules };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
function conventionActivation(surfaces) {
|
|
3
|
+
return (surface, capability) => {
|
|
4
|
+
const convention = surfaces[surface];
|
|
5
|
+
if (!convention || !convention.marker(capability.directory)) return void 0;
|
|
6
|
+
return {
|
|
7
|
+
exportSubpath: convention.exportSubpath,
|
|
8
|
+
exportName: convention.exportName(capability.id)
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function camelCaseId(id) {
|
|
13
|
+
return id.replace(/^-+|-+$/g, "").replace(/-+([a-z0-9])/gi, (_match, char) => char.toUpperCase());
|
|
14
|
+
}
|
|
15
|
+
function fileSurfaceConvention(options) {
|
|
16
|
+
const { files, exportSubpath, exportSuffix = "", exists } = options;
|
|
17
|
+
const join = options.join ?? ((directory, file) => `${directory}/${file}`);
|
|
18
|
+
return {
|
|
19
|
+
marker: (directory) => files.some((file) => exists(join(directory, file))),
|
|
20
|
+
exportName: (id) => `${camelCaseId(id)}${exportSuffix}`,
|
|
21
|
+
exportSubpath
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
function capabilitySpecifier(packageName, exportSubpath) {
|
|
25
|
+
return `${packageName}${exportSubpath.replace(/^\./, "")}`;
|
|
26
|
+
}
|
|
27
|
+
function resolveSurfaceModules(active, surface, activation) {
|
|
28
|
+
return active.flatMap((capability) => {
|
|
29
|
+
const entry = activation(surface, { directory: capability.directory, id: capability.id });
|
|
30
|
+
if (!entry) return [];
|
|
31
|
+
return [
|
|
32
|
+
{
|
|
33
|
+
capability,
|
|
34
|
+
specifier: capabilitySpecifier(capability.packageName, entry.exportSubpath),
|
|
35
|
+
exportName: entry.exportName
|
|
36
|
+
}
|
|
37
|
+
];
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export {
|
|
41
|
+
capabilitySpecifier,
|
|
42
|
+
conventionActivation,
|
|
43
|
+
fileSurfaceConvention,
|
|
44
|
+
resolveSurfaceModules
|
|
45
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lorion-org/surface-activation",
|
|
3
|
+
"version": "1.0.0-beta.3",
|
|
4
|
+
"description": "Framework-free surface-activation convention: which module and export provides a capability's surface, and the import specifier to reach it.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/lorion-org/lorion.git",
|
|
9
|
+
"directory": "packages/surface-activation"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/lorion-org/lorion/tree/main/packages/surface-activation#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/surface-activation/src/index.spec.ts",
|
|
51
|
+
"coverage": "node -e \"require('node:fs').mkdirSync('../../coverage/.tmp', { recursive: true })\" && vitest run --coverage --coverage.include=packages/surface-activation/src/**/*.ts --root ../.. --config vitest.config.mts packages/surface-activation/src/index.spec.ts",
|
|
52
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
53
|
+
"package:check": "pnpm build && pnpm pack --dry-run && publint"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"surface",
|
|
57
|
+
"activation",
|
|
58
|
+
"capability",
|
|
59
|
+
"composition",
|
|
60
|
+
"typescript"
|
|
61
|
+
],
|
|
62
|
+
"engines": {
|
|
63
|
+
"node": "^20.19.0 || >=22.12.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// Surface activation: a framework-free, file-layout convention for deciding which
|
|
2
|
+
// module (and which exported symbol) provides a given "surface" of a capability,
|
|
3
|
+
// and the import specifier to reach it. Pure — no filesystem, no framework. The
|
|
4
|
+
// same seam is shared by build-time hosts (which code-generate static imports) and
|
|
5
|
+
// runtime hosts (which dynamically import), so the addressing rule lives in exactly
|
|
6
|
+
// one place.
|
|
7
|
+
|
|
8
|
+
export interface SurfaceActivation {
|
|
9
|
+
exportSubpath: string;
|
|
10
|
+
exportName: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SurfaceConvention {
|
|
14
|
+
// True when the capability provides this surface (a file-layout marker).
|
|
15
|
+
marker: (directory: string) => boolean;
|
|
16
|
+
// Derives the exported symbol name from the capability id.
|
|
17
|
+
exportName: (id: string) => string;
|
|
18
|
+
// The package export subpath the symbol is imported from (for example `./web`).
|
|
19
|
+
exportSubpath: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ActivationResolver = (
|
|
23
|
+
surface: string,
|
|
24
|
+
capability: { directory: string; id: string },
|
|
25
|
+
) => SurfaceActivation | undefined;
|
|
26
|
+
|
|
27
|
+
// Builds an activation resolver from per-surface conventions. A host declares how a
|
|
28
|
+
// surface is detected (marker) and named (exportName); the descriptor carries no
|
|
29
|
+
// surface config.
|
|
30
|
+
export function conventionActivation(
|
|
31
|
+
surfaces: Record<string, SurfaceConvention>,
|
|
32
|
+
): ActivationResolver {
|
|
33
|
+
return (surface, capability) => {
|
|
34
|
+
const convention = surfaces[surface];
|
|
35
|
+
if (!convention || !convention.marker(capability.directory)) return undefined;
|
|
36
|
+
return {
|
|
37
|
+
exportSubpath: convention.exportSubpath,
|
|
38
|
+
exportName: convention.exportName(capability.id),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface FileSurfaceConventionOptions {
|
|
44
|
+
// Relative marker files; the surface exists when the capability ships any of them
|
|
45
|
+
// (for example `['src/web.ts']`, or several accepted layouts). An empty list means
|
|
46
|
+
// the surface never activates — pass at least one marker.
|
|
47
|
+
files: readonly string[];
|
|
48
|
+
// Export subpath the symbol is imported from (for example `./web`).
|
|
49
|
+
exportSubpath: string;
|
|
50
|
+
// Suffix appended to the camelCased id to form the export name
|
|
51
|
+
// (`'WebPlugin'` turns `auth-oidc` into `authOidcWebPlugin`). Defaults to `''`.
|
|
52
|
+
exportSuffix?: string;
|
|
53
|
+
// Existence check for a marker path, injected by the host. Kept out of this module
|
|
54
|
+
// so the package stays I/O-free — a Node host passes `existsSync`.
|
|
55
|
+
exists: (path: string) => boolean;
|
|
56
|
+
// Joins a capability directory with a relative marker file. Defaults to a POSIX
|
|
57
|
+
// join (`${directory}/${file}`), which Node's fs accepts on every platform; pass
|
|
58
|
+
// `node:path`'s `join` for native separators.
|
|
59
|
+
join?: (directory: string, file: string) => string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Converts a kebab-case id to a camelCase identifier fragment (`auth-oidc` ->
|
|
63
|
+
// `authOidc`). The shared naming rule so every host derives export names the same
|
|
64
|
+
// way. Hyphen runs are collapsed and the following character is uppercased
|
|
65
|
+
// (letters and digits alike), and leading/trailing hyphens are dropped, so
|
|
66
|
+
// non-strict ids (`auth-2fa`, `foo--bar`, `foo-`) still yield a valid identifier
|
|
67
|
+
// fragment. An id that starts with a digit cannot be a valid identifier on its own —
|
|
68
|
+
// constrain ids at the descriptor level if that matters.
|
|
69
|
+
function camelCaseId(id: string): string {
|
|
70
|
+
return id
|
|
71
|
+
.replace(/^-+|-+$/g, '')
|
|
72
|
+
.replace(/-+([a-z0-9])/gi, (_match, char: string) => char.toUpperCase());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// A ready-made `SurfaceConvention` for the common file-layout case: the surface is
|
|
76
|
+
// present when one of `files` exists, and its export is `camelCase(id) + exportSuffix`
|
|
77
|
+
// from `exportSubpath`. Hand the result to `conventionActivation`. This is the whole
|
|
78
|
+
// marker/naming boilerplate a host would otherwise repeat per surface; the raw
|
|
79
|
+
// `SurfaceConvention` object stays available for anything this preset does not cover.
|
|
80
|
+
export function fileSurfaceConvention(options: FileSurfaceConventionOptions): SurfaceConvention {
|
|
81
|
+
const { files, exportSubpath, exportSuffix = '', exists } = options;
|
|
82
|
+
const join = options.join ?? ((directory, file) => `${directory}/${file}`);
|
|
83
|
+
return {
|
|
84
|
+
marker: (directory) => files.some((file) => exists(join(directory, file))),
|
|
85
|
+
exportName: (id) => `${camelCaseId(id)}${exportSuffix}`,
|
|
86
|
+
exportSubpath,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The import specifier for a capability's surface module: the package name joined
|
|
91
|
+
// with the export subpath, with a leading `.` dropped (`./web` becomes `/web`). One
|
|
92
|
+
// rule, shared by every host style.
|
|
93
|
+
export function capabilitySpecifier(packageName: string, exportSubpath: string): string {
|
|
94
|
+
return `${packageName}${exportSubpath.replace(/^\./, '')}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// The minimal capability shape surface addressing needs: an id, its on-disk
|
|
98
|
+
// directory, and the package name the specifier is built from.
|
|
99
|
+
export interface SurfaceCapability {
|
|
100
|
+
id: string;
|
|
101
|
+
directory: string;
|
|
102
|
+
packageName: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface CapabilitySurfaceModule<T extends SurfaceCapability = SurfaceCapability> {
|
|
106
|
+
capability: T;
|
|
107
|
+
specifier: string;
|
|
108
|
+
exportName: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// For each active capability that provides the surface, the module specifier and
|
|
112
|
+
// export name to import. The seam shared by both host styles: a runtime loop feeds
|
|
113
|
+
// each specifier to a dynamic import, while a build-time host code-generates static
|
|
114
|
+
// imports from the same list.
|
|
115
|
+
export function resolveSurfaceModules<T extends SurfaceCapability>(
|
|
116
|
+
active: readonly T[],
|
|
117
|
+
surface: string,
|
|
118
|
+
activation: ActivationResolver,
|
|
119
|
+
): CapabilitySurfaceModule<T>[] {
|
|
120
|
+
return active.flatMap((capability) => {
|
|
121
|
+
const entry = activation(surface, { directory: capability.directory, id: capability.id });
|
|
122
|
+
if (!entry) return [];
|
|
123
|
+
return [
|
|
124
|
+
{
|
|
125
|
+
capability,
|
|
126
|
+
specifier: capabilitySpecifier(capability.packageName, entry.exportSubpath),
|
|
127
|
+
exportName: entry.exportName,
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
});
|
|
131
|
+
}
|