@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 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
+ });
@@ -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 };
@@ -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
+ }