@cueloop/extension-api 0.1.0-alpha.15 → 0.1.0-alpha.17
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/package.json +2 -2
- package/src/loader.ts +14 -14
- package/src/registry.test.ts +1 -1
- package/src/registry.ts +14 -14
- package/src/types.ts +3 -3
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/extension-api",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.17",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.ts"
|
|
8
8
|
},
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@cueloop/schema": "0.1.0-alpha.
|
|
10
|
+
"@cueloop/schema": "0.1.0-alpha.17"
|
|
11
11
|
},
|
|
12
12
|
"publishConfig": {
|
|
13
13
|
"access": "public"
|
package/src/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Extension discovery and loading
|
|
2
|
+
* Extension discovery and loading: zero-build TypeScript executed
|
|
3
3
|
* directly. Global user extensions always load; repo-local `.cueloop/
|
|
4
4
|
* extensions/` loads only when the repo is trusted (persisted decisions
|
|
5
5
|
* under the cueloop home).
|
|
@@ -51,29 +51,29 @@ export function grantTrust(home: string, repoRoot: string): void {
|
|
|
51
51
|
function extensionFiles(dir: string): string[] {
|
|
52
52
|
if (!existsSync(dir)) return [];
|
|
53
53
|
return readdirSync(dir)
|
|
54
|
-
.filter((
|
|
55
|
-
.map((
|
|
54
|
+
.filter((fileName) => fileName.endsWith(".ts") || fileName.endsWith(".js"))
|
|
55
|
+
.map((fileName) => join(dir, fileName))
|
|
56
56
|
.sort();
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export async function loadExtensions(
|
|
59
|
+
export async function loadExtensions(options: LoadOptions): Promise<{ loaded: string[]; skipped: string[] }> {
|
|
60
60
|
const loaded: string[] = [];
|
|
61
61
|
const skipped: string[] = [];
|
|
62
62
|
const files: { path: string; source: string }[] = [];
|
|
63
63
|
|
|
64
|
-
for (const path of extensionFiles(
|
|
64
|
+
for (const path of extensionFiles(options.userDir ?? userExtensionsDir())) {
|
|
65
65
|
files.push({ path, source: "user" });
|
|
66
66
|
}
|
|
67
|
-
if (
|
|
68
|
-
const repoDir = join(
|
|
67
|
+
if (options.repoRoot) {
|
|
68
|
+
const repoDir = join(options.repoRoot, ".cueloop", "extensions");
|
|
69
69
|
const repoFiles = extensionFiles(repoDir);
|
|
70
70
|
if (repoFiles.length) {
|
|
71
|
-
const home =
|
|
71
|
+
const home = options.home ?? join(homedir(), ".cueloop");
|
|
72
72
|
const trusted =
|
|
73
|
-
readTrust(home).trusted.includes(
|
|
74
|
-
(
|
|
73
|
+
readTrust(home).trusted.includes(options.repoRoot) ||
|
|
74
|
+
(options.confirmTrust ? await options.confirmTrust(options.repoRoot) : false);
|
|
75
75
|
if (trusted) {
|
|
76
|
-
grantTrust(home,
|
|
76
|
+
grantTrust(home, options.repoRoot);
|
|
77
77
|
for (const path of repoFiles) files.push({ path, source: "repo" });
|
|
78
78
|
} else {
|
|
79
79
|
skipped.push(...repoFiles);
|
|
@@ -83,12 +83,12 @@ export async function loadExtensions(opts: LoadOptions): Promise<{ loaded: strin
|
|
|
83
83
|
|
|
84
84
|
for (const file of files) {
|
|
85
85
|
try {
|
|
86
|
-
const
|
|
87
|
-
if (typeof
|
|
86
|
+
const extensionModule = (await import(file.path)) as { default?: ExtensionFactory };
|
|
87
|
+
if (typeof extensionModule.default !== "function") {
|
|
88
88
|
skipped.push(file.path);
|
|
89
89
|
continue;
|
|
90
90
|
}
|
|
91
|
-
await
|
|
91
|
+
await options.registry.load(file.path, extensionModule.default);
|
|
92
92
|
loaded.push(file.path);
|
|
93
93
|
} catch {
|
|
94
94
|
skipped.push(file.path);
|
package/src/registry.test.ts
CHANGED
|
@@ -48,7 +48,7 @@ describe("Registry", () => {
|
|
|
48
48
|
api.on("session.resolved", () => {
|
|
49
49
|
throw new Error("observer bug");
|
|
50
50
|
});
|
|
51
|
-
api.on("session.resolved", (
|
|
51
|
+
api.on("session.resolved", (session) => seen.push(session.id));
|
|
52
52
|
});
|
|
53
53
|
reg.emit("session.resolved", SESSION);
|
|
54
54
|
expect(seen).toEqual(["ses_x"]);
|
package/src/registry.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The contribution registry
|
|
2
|
+
* The contribution registry: registrations are plain data, attributed
|
|
3
3
|
* per extension. Reserved app keybindings are rejected at registration so a
|
|
4
4
|
* broken extension cannot shadow core grammar.
|
|
5
5
|
*/
|
|
@@ -44,19 +44,19 @@ export class Registry {
|
|
|
44
44
|
registerRenderer(type, renderer) {
|
|
45
45
|
record.renderers.set(type, renderer);
|
|
46
46
|
},
|
|
47
|
-
registerCommand(
|
|
48
|
-
record.commands.set(
|
|
47
|
+
registerCommand(commandName, registration) {
|
|
48
|
+
record.commands.set(commandName, registration);
|
|
49
49
|
},
|
|
50
50
|
registerKeybinding(registration) {
|
|
51
|
-
const clash = registration.defaultKeys.find((
|
|
51
|
+
const clash = registration.defaultKeys.find((key) => RESERVED_KEYS.has(key));
|
|
52
52
|
if (clash) {
|
|
53
53
|
record.errors.push(`keybinding "${registration.action}" rejected: "${clash}" is reserved`);
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
record.keybindings.push(registration);
|
|
57
57
|
},
|
|
58
|
-
registerExporter(
|
|
59
|
-
record.exporters.set(
|
|
58
|
+
registerExporter(exporterName, exporter) {
|
|
59
|
+
record.exporters.set(exporterName, exporter);
|
|
60
60
|
},
|
|
61
61
|
on(event, handler) {
|
|
62
62
|
const list = record.listeners.get(event) ?? [];
|
|
@@ -76,24 +76,24 @@ export class Registry {
|
|
|
76
76
|
|
|
77
77
|
/** First-registered wins for renderers/commands; built-ins load first. */
|
|
78
78
|
rendererFor(artifactType: string): ArtifactRenderer | undefined {
|
|
79
|
-
for (const
|
|
80
|
-
const
|
|
81
|
-
if (
|
|
79
|
+
for (const extension of this.extensions) {
|
|
80
|
+
const renderer = extension.renderers.get(artifactType);
|
|
81
|
+
if (renderer) return renderer;
|
|
82
82
|
}
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
commandFor(name: string): { extension: string; registration: CommandRegistration } | undefined {
|
|
87
|
-
for (const
|
|
88
|
-
const
|
|
89
|
-
if (
|
|
87
|
+
for (const extension of this.extensions) {
|
|
88
|
+
const command = extension.commands.get(name);
|
|
89
|
+
if (command) return { extension: extension.name, registration: command };
|
|
90
90
|
}
|
|
91
91
|
return undefined;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
emit(event: string, session: ReviewSession): void {
|
|
95
|
-
for (const
|
|
96
|
-
for (const handler of
|
|
95
|
+
for (const extension of this.extensions) {
|
|
96
|
+
for (const handler of extension.listeners.get(event) ?? []) {
|
|
97
97
|
try {
|
|
98
98
|
handler(session);
|
|
99
99
|
} catch {
|
package/src/types.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The single typed extension contract
|
|
2
|
+
* The single typed extension contract. This module is import-free
|
|
3
3
|
* beyond the schema: whatever a declaration file can reach gets published,
|
|
4
4
|
* so the contract stays self-contained.
|
|
5
5
|
*
|
|
@@ -32,14 +32,14 @@ export interface CommandContext {
|
|
|
32
32
|
|
|
33
33
|
export interface CommandRegistration {
|
|
34
34
|
description: string;
|
|
35
|
-
handler(
|
|
35
|
+
handler(context: CommandContext, args: string): void | Promise<void>;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface KeybindingRegistration {
|
|
39
39
|
/** Action name; binds through the same [keys] config as built-ins. */
|
|
40
40
|
action: string;
|
|
41
41
|
defaultKeys: string[];
|
|
42
|
-
handler(
|
|
42
|
+
handler(context: CommandContext): void | Promise<void>;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/** Exporters ship resolved sessions somewhere (notes vaults, forges). */
|