@noego/wood 0.4.3 → 0.4.4
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/parser/operations_parser.cjs +23 -2
- package/dist/parser/operations_parser.cjs.map +1 -1
- package/dist/parser/operations_parser.js +23 -2
- package/dist/parser/operations_parser.js.map +1 -1
- package/dist/router/wood_router.cjs +28 -4
- package/dist/router/wood_router.cjs.map +1 -1
- package/dist/router/wood_router.d.cts +6 -0
- package/dist/router/wood_router.d.ts +6 -0
- package/dist/router/wood_router.js +28 -4
- package/dist/router/wood_router.js.map +1 -1
- package/dist/runtime/runtime.cjs +2 -0
- package/dist/runtime/runtime.cjs.map +1 -1
- package/dist/runtime/runtime.js +2 -0
- package/dist/runtime/runtime.js.map +1 -1
- package/dist/validation/schema_validator.cjs +13 -0
- package/dist/validation/schema_validator.cjs.map +1 -1
- package/dist/validation/schema_validator.d.cts +6 -0
- package/dist/validation/schema_validator.d.ts +6 -0
- package/dist/validation/schema_validator.js +13 -0
- package/dist/validation/schema_validator.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ContextMenuProvider.compile.test.ts +20 -0
- package/src/components/ContextMenuProvider.svelte +2 -2
|
@@ -37,9 +37,24 @@ var import_fs = require("fs");
|
|
|
37
37
|
function parseOperations(yamlContent) {
|
|
38
38
|
const doc = import_js_yaml.default.load(yamlContent);
|
|
39
39
|
const operations = /* @__PURE__ */ new Map();
|
|
40
|
+
if (doc === null || doc === void 0) {
|
|
41
|
+
throw new Error('Operations document is empty: expected a top-level "operations:" or "module:" mapping.');
|
|
42
|
+
}
|
|
43
|
+
if (typeof doc !== "object" || Array.isArray(doc)) {
|
|
44
|
+
throw new Error('Operations document must be a mapping with "operations:" and/or "module:" keys.');
|
|
45
|
+
}
|
|
46
|
+
if (!doc.operations && !doc.module) {
|
|
47
|
+
throw new Error('Operations document defines no operations: expected an "operations:" or "module:" key.');
|
|
48
|
+
}
|
|
49
|
+
const set = (channel, def) => {
|
|
50
|
+
if (operations.has(channel)) {
|
|
51
|
+
throw new Error(`Duplicate operation channel "${channel}": each channel may only be defined once.`);
|
|
52
|
+
}
|
|
53
|
+
operations.set(channel, def);
|
|
54
|
+
};
|
|
40
55
|
if (doc.operations) {
|
|
41
56
|
for (const [channel, entry] of Object.entries(doc.operations)) {
|
|
42
|
-
|
|
57
|
+
set(channel, {
|
|
43
58
|
channel,
|
|
44
59
|
controller: entry.controller,
|
|
45
60
|
action: entry.action,
|
|
@@ -53,6 +68,12 @@ function parseOperations(yamlContent) {
|
|
|
53
68
|
for (const [moduleName, moduleDef] of Object.entries(doc.module)) {
|
|
54
69
|
const moduleController = moduleDef.controller;
|
|
55
70
|
const moduleMiddleware = moduleDef.middleware ?? [];
|
|
71
|
+
if (moduleDef.operations === null || moduleDef.operations === void 0) {
|
|
72
|
+
throw new Error(`Module "${moduleName}" has no "operations:" block.`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof moduleDef.operations !== "object" || Array.isArray(moduleDef.operations)) {
|
|
75
|
+
throw new Error(`Module "${moduleName}" "operations:" must be a mapping of operation names.`);
|
|
76
|
+
}
|
|
56
77
|
for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {
|
|
57
78
|
const channel = `${moduleName}.${opName}`;
|
|
58
79
|
let middleware;
|
|
@@ -61,7 +82,7 @@ function parseOperations(yamlContent) {
|
|
|
61
82
|
} else {
|
|
62
83
|
middleware = [...moduleMiddleware, ...opEntry.middleware ?? []];
|
|
63
84
|
}
|
|
64
|
-
|
|
85
|
+
set(channel, {
|
|
65
86
|
channel,
|
|
66
87
|
controller: opEntry.controller ?? moduleController ?? "",
|
|
67
88
|
action: opEntry.action,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.cjs';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n
|
|
1
|
+
{"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.cjs';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n if (doc === null || doc === undefined) {\n throw new Error('Operations document is empty: expected a top-level \"operations:\" or \"module:\" mapping.');\n }\n if (typeof doc !== 'object' || Array.isArray(doc)) {\n throw new Error('Operations document must be a mapping with \"operations:\" and/or \"module:\" keys.');\n }\n if (!doc.operations && !doc.module) {\n throw new Error('Operations document defines no operations: expected an \"operations:\" or \"module:\" key.');\n }\n\n // A channel may only be defined once. Two operations sharing a channel would\n // silently overwrite each other (last-wins), dropping an RPC with no warning.\n const set = (channel: string, def: OperationDef): void => {\n if (operations.has(channel)) {\n throw new Error(`Duplicate operation channel \"${channel}\": each channel may only be defined once.`);\n }\n operations.set(channel, def);\n };\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n if (moduleDef.operations === null || moduleDef.operations === undefined) {\n throw new Error(`Module \"${moduleName}\" has no \"operations:\" block.`);\n }\n if (typeof moduleDef.operations !== 'object' || Array.isArray(moduleDef.operations)) {\n throw new Error(`Module \"${moduleName}\" \"operations:\" must be a mapping of operation names.`);\n }\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,qBAAiB;AACjB,gBAA6B;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,eAAAA,QAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAEjD,MAAI,QAAQ,QAAQ,QAAQ,QAAW;AACrC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AACA,MAAI,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjD,UAAM,IAAI,MAAM,iFAAiF;AAAA,EACnG;AACA,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,QAAQ;AAClC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AAIA,QAAM,MAAM,CAAC,SAAiB,QAA4B;AACxD,QAAI,WAAW,IAAI,OAAO,GAAG;AAC3B,YAAM,IAAI,MAAM,gCAAgC,OAAO,2CAA2C;AAAA,IACpG;AACA,eAAW,IAAI,SAAS,GAAG;AAAA,EAC7B;AAGA,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,UAAI,SAAS;AAAA,QACX;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,UAAI,UAAU,eAAe,QAAQ,UAAU,eAAe,QAAW;AACvE,cAAM,IAAI,MAAM,WAAW,UAAU,+BAA+B;AAAA,MACtE;AACA,UAAI,OAAO,UAAU,eAAe,YAAY,MAAM,QAAQ,UAAU,UAAU,GAAG;AACnF,cAAM,IAAI,MAAM,WAAW,UAAU,uDAAuD;AAAA,MAC9F;AAEA,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,YAAI,SAAS;AAAA,UACX;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,cAAU,wBAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":["yaml"]}
|
|
@@ -3,9 +3,24 @@ import { readFileSync } from "fs";
|
|
|
3
3
|
function parseOperations(yamlContent) {
|
|
4
4
|
const doc = yaml.load(yamlContent);
|
|
5
5
|
const operations = /* @__PURE__ */ new Map();
|
|
6
|
+
if (doc === null || doc === void 0) {
|
|
7
|
+
throw new Error('Operations document is empty: expected a top-level "operations:" or "module:" mapping.');
|
|
8
|
+
}
|
|
9
|
+
if (typeof doc !== "object" || Array.isArray(doc)) {
|
|
10
|
+
throw new Error('Operations document must be a mapping with "operations:" and/or "module:" keys.');
|
|
11
|
+
}
|
|
12
|
+
if (!doc.operations && !doc.module) {
|
|
13
|
+
throw new Error('Operations document defines no operations: expected an "operations:" or "module:" key.');
|
|
14
|
+
}
|
|
15
|
+
const set = (channel, def) => {
|
|
16
|
+
if (operations.has(channel)) {
|
|
17
|
+
throw new Error(`Duplicate operation channel "${channel}": each channel may only be defined once.`);
|
|
18
|
+
}
|
|
19
|
+
operations.set(channel, def);
|
|
20
|
+
};
|
|
6
21
|
if (doc.operations) {
|
|
7
22
|
for (const [channel, entry] of Object.entries(doc.operations)) {
|
|
8
|
-
|
|
23
|
+
set(channel, {
|
|
9
24
|
channel,
|
|
10
25
|
controller: entry.controller,
|
|
11
26
|
action: entry.action,
|
|
@@ -19,6 +34,12 @@ function parseOperations(yamlContent) {
|
|
|
19
34
|
for (const [moduleName, moduleDef] of Object.entries(doc.module)) {
|
|
20
35
|
const moduleController = moduleDef.controller;
|
|
21
36
|
const moduleMiddleware = moduleDef.middleware ?? [];
|
|
37
|
+
if (moduleDef.operations === null || moduleDef.operations === void 0) {
|
|
38
|
+
throw new Error(`Module "${moduleName}" has no "operations:" block.`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof moduleDef.operations !== "object" || Array.isArray(moduleDef.operations)) {
|
|
41
|
+
throw new Error(`Module "${moduleName}" "operations:" must be a mapping of operation names.`);
|
|
42
|
+
}
|
|
22
43
|
for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {
|
|
23
44
|
const channel = `${moduleName}.${opName}`;
|
|
24
45
|
let middleware;
|
|
@@ -27,7 +48,7 @@ function parseOperations(yamlContent) {
|
|
|
27
48
|
} else {
|
|
28
49
|
middleware = [...moduleMiddleware, ...opEntry.middleware ?? []];
|
|
29
50
|
}
|
|
30
|
-
|
|
51
|
+
set(channel, {
|
|
31
52
|
channel,
|
|
32
53
|
controller: opEntry.controller ?? moduleController ?? "",
|
|
33
54
|
action: opEntry.action,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.js';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n
|
|
1
|
+
{"version":3,"sources":["../../src/parser/operations_parser.ts"],"sourcesContent":["import yaml from 'js-yaml';\nimport { readFileSync } from 'fs';\nimport type { OperationDef, RawOperationsDocument, RawModuleDef, RawOperationEntry, RawModuleOperationEntry } from '../types/index.js';\n\nexport function parseOperations(yamlContent: string): Map<string, OperationDef> {\n const doc = yaml.load(yamlContent) as RawOperationsDocument;\n const operations = new Map<string, OperationDef>();\n\n if (doc === null || doc === undefined) {\n throw new Error('Operations document is empty: expected a top-level \"operations:\" or \"module:\" mapping.');\n }\n if (typeof doc !== 'object' || Array.isArray(doc)) {\n throw new Error('Operations document must be a mapping with \"operations:\" and/or \"module:\" keys.');\n }\n if (!doc.operations && !doc.module) {\n throw new Error('Operations document defines no operations: expected an \"operations:\" or \"module:\" key.');\n }\n\n // A channel may only be defined once. Two operations sharing a channel would\n // silently overwrite each other (last-wins), dropping an RPC with no warning.\n const set = (channel: string, def: OperationDef): void => {\n if (operations.has(channel)) {\n throw new Error(`Duplicate operation channel \"${channel}\": each channel may only be defined once.`);\n }\n operations.set(channel, def);\n };\n\n // 1. Parse flat operations (top-level `operations:` key)\n if (doc.operations) {\n for (const [channel, entry] of Object.entries(doc.operations)) {\n set(channel, {\n channel,\n controller: entry.controller,\n action: entry.action,\n middleware: entry.middleware ?? [],\n inputSchema: entry.input,\n outputSchema: entry.output,\n });\n }\n }\n\n // 2. Parse module operations (`module:` key)\n if (doc.module) {\n for (const [moduleName, moduleDef] of Object.entries(doc.module)) {\n const moduleController = moduleDef.controller;\n const moduleMiddleware = moduleDef.middleware ?? [];\n\n if (moduleDef.operations === null || moduleDef.operations === undefined) {\n throw new Error(`Module \"${moduleName}\" has no \"operations:\" block.`);\n }\n if (typeof moduleDef.operations !== 'object' || Array.isArray(moduleDef.operations)) {\n throw new Error(`Module \"${moduleName}\" \"operations:\" must be a mapping of operation names.`);\n }\n\n for (const [opName, opEntry] of Object.entries(moduleDef.operations)) {\n const channel = `${moduleName}.${opName}`;\n\n // Middleware: if operation explicitly sets middleware to [] or null, clear inherited\n // Otherwise, prepend module middleware to operation middleware\n let middleware: string[];\n if (opEntry.middleware === null || (Array.isArray(opEntry.middleware) && opEntry.middleware.length === 0)) {\n middleware = [];\n } else {\n middleware = [...moduleMiddleware, ...(opEntry.middleware ?? [])];\n }\n\n set(channel, {\n channel,\n controller: opEntry.controller ?? moduleController ?? '',\n action: opEntry.action,\n middleware,\n inputSchema: opEntry.input,\n outputSchema: opEntry.output,\n });\n }\n }\n }\n\n // 3. Validate: every operation must have a controller and action\n for (const [channel, op] of operations) {\n if (!op.controller) {\n throw new Error(`Operation \"${channel}\" has no controller defined`);\n }\n if (!op.action) {\n throw new Error(`Operation \"${channel}\" has no action defined`);\n }\n }\n\n return operations;\n}\n\n/** Parse from file path */\nexport function parseOperationsFile(filePath: string): Map<string, OperationDef> {\n const content = readFileSync(filePath, 'utf-8');\n return parseOperations(content);\n}\n"],"mappings":"AAAA,OAAO,UAAU;AACjB,SAAS,oBAAoB;AAGtB,SAAS,gBAAgB,aAAgD;AAC9E,QAAM,MAAM,KAAK,KAAK,WAAW;AACjC,QAAM,aAAa,oBAAI,IAA0B;AAEjD,MAAI,QAAQ,QAAQ,QAAQ,QAAW;AACrC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AACA,MAAI,OAAO,QAAQ,YAAY,MAAM,QAAQ,GAAG,GAAG;AACjD,UAAM,IAAI,MAAM,iFAAiF;AAAA,EACnG;AACA,MAAI,CAAC,IAAI,cAAc,CAAC,IAAI,QAAQ;AAClC,UAAM,IAAI,MAAM,wFAAwF;AAAA,EAC1G;AAIA,QAAM,MAAM,CAAC,SAAiB,QAA4B;AACxD,QAAI,WAAW,IAAI,OAAO,GAAG;AAC3B,YAAM,IAAI,MAAM,gCAAgC,OAAO,2CAA2C;AAAA,IACpG;AACA,eAAW,IAAI,SAAS,GAAG;AAAA,EAC7B;AAGA,MAAI,IAAI,YAAY;AAClB,eAAW,CAAC,SAAS,KAAK,KAAK,OAAO,QAAQ,IAAI,UAAU,GAAG;AAC7D,UAAI,SAAS;AAAA,QACX;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,QACd,YAAY,MAAM,cAAc,CAAC;AAAA,QACjC,aAAa,MAAM;AAAA,QACnB,cAAc,MAAM;AAAA,MACtB,CAAC;AAAA,IACH;AAAA,EACF;AAGA,MAAI,IAAI,QAAQ;AACd,eAAW,CAAC,YAAY,SAAS,KAAK,OAAO,QAAQ,IAAI,MAAM,GAAG;AAChE,YAAM,mBAAmB,UAAU;AACnC,YAAM,mBAAmB,UAAU,cAAc,CAAC;AAElD,UAAI,UAAU,eAAe,QAAQ,UAAU,eAAe,QAAW;AACvE,cAAM,IAAI,MAAM,WAAW,UAAU,+BAA+B;AAAA,MACtE;AACA,UAAI,OAAO,UAAU,eAAe,YAAY,MAAM,QAAQ,UAAU,UAAU,GAAG;AACnF,cAAM,IAAI,MAAM,WAAW,UAAU,uDAAuD;AAAA,MAC9F;AAEA,iBAAW,CAAC,QAAQ,OAAO,KAAK,OAAO,QAAQ,UAAU,UAAU,GAAG;AACpE,cAAM,UAAU,GAAG,UAAU,IAAI,MAAM;AAIvC,YAAI;AACJ,YAAI,QAAQ,eAAe,QAAS,MAAM,QAAQ,QAAQ,UAAU,KAAK,QAAQ,WAAW,WAAW,GAAI;AACzG,uBAAa,CAAC;AAAA,QAChB,OAAO;AACL,uBAAa,CAAC,GAAG,kBAAkB,GAAI,QAAQ,cAAc,CAAC,CAAE;AAAA,QAClE;AAEA,YAAI,SAAS;AAAA,UACX;AAAA,UACA,YAAY,QAAQ,cAAc,oBAAoB;AAAA,UACtD,QAAQ,QAAQ;AAAA,UAChB;AAAA,UACA,aAAa,QAAQ;AAAA,UACrB,cAAc,QAAQ;AAAA,QACxB,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AAGA,aAAW,CAAC,SAAS,EAAE,KAAK,YAAY;AACtC,QAAI,CAAC,GAAG,YAAY;AAClB,YAAM,IAAI,MAAM,cAAc,OAAO,6BAA6B;AAAA,IACpE;AACA,QAAI,CAAC,GAAG,QAAQ;AACd,YAAM,IAAI,MAAM,cAAc,OAAO,yBAAyB;AAAA,IAChE;AAAA,EACF;AAEA,SAAO;AACT;AAGO,SAAS,oBAAoB,UAA6C;AAC/E,QAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,SAAO,gBAAgB,OAAO;AAChC;","names":[]}
|
|
@@ -22,6 +22,7 @@ __export(wood_router_exports, {
|
|
|
22
22
|
WoodRouter: () => WoodRouter
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(wood_router_exports);
|
|
25
|
+
var import_types = require("../types/index.cjs");
|
|
25
26
|
var import_schema_validator = require("../validation/schema_validator.cjs");
|
|
26
27
|
var import_middleware_resolver = require("../middleware/middleware_resolver.cjs");
|
|
27
28
|
var import_middleware_pipeline = require("../middleware/middleware_pipeline.cjs");
|
|
@@ -142,11 +143,19 @@ class WoodRouter {
|
|
|
142
143
|
ipcContext.body
|
|
143
144
|
);
|
|
144
145
|
if (!result2.valid) {
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
(0, import_tracing.publishMainTrace)("error", {
|
|
147
|
+
source: "wood.router",
|
|
148
|
+
type: "dispatch.validation_error",
|
|
147
149
|
message: "Request validation failed",
|
|
148
|
-
|
|
149
|
-
|
|
150
|
+
...conversationId !== void 0 ? { conversationId } : {},
|
|
151
|
+
payload: {
|
|
152
|
+
channel,
|
|
153
|
+
controller: opDef.controller,
|
|
154
|
+
action: opDef.action,
|
|
155
|
+
errorCount: result2.errors?.length ?? 0
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
throw new import_types.WoodValidationError("Request validation failed", result2.errors ?? []);
|
|
150
159
|
}
|
|
151
160
|
}
|
|
152
161
|
const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);
|
|
@@ -254,6 +263,21 @@ ${details}`
|
|
|
254
263
|
}
|
|
255
264
|
logger.info(`All ${this.operations.size} operations have registered controllers`);
|
|
256
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* Eagerly compile every operation's inputSchema so a malformed schema fails
|
|
268
|
+
* at startup (with the offending channel named) instead of throwing an opaque
|
|
269
|
+
* ajv error on the first request to that channel. Call after loadOperations().
|
|
270
|
+
*/
|
|
271
|
+
precompileSchemas() {
|
|
272
|
+
let compiled = 0;
|
|
273
|
+
for (const [channel, opDef] of this.operations) {
|
|
274
|
+
if (opDef.inputSchema) {
|
|
275
|
+
this.validator.precompile(channel, opDef.inputSchema);
|
|
276
|
+
compiled += 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
logger.info(`Pre-compiled ${compiled} operation input schema(s)`);
|
|
280
|
+
}
|
|
257
281
|
/** Get all registered operation channels. */
|
|
258
282
|
getChannels() {
|
|
259
283
|
return Array.from(this.operations.keys());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.cjs';\nimport { SchemaValidator } from '../validation/schema_validator.cjs';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.cjs';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.cjs';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.cjs';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.cjs';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,8BAAgC;AAChC,iCAAmC;AACnC,iCAA0D;AAC1D,iCAAwD;AACxD,kCAA2C;AAC3C,oBAA0B;AAC1B,qBAAiC;AAEjC,MAAM,aAAS,yBAAU,aAAa;AAEtC,SAAS,2BAA2B,OAA6C;AAC/E,MAAI,CAAC,SAAS,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,GAAG;AAC/D,WAAO;AAAA,EACT;AAEA,QAAM,SAAS;AACf,MAAI,OAAO,OAAO,mBAAmB,YAAY,OAAO,OAAO,mBAAmB,UAAU;AAC1F,WAAO,OAAO;AAAA,EAChB;AACA,MAAI,OAAO,OAAO,aAAa,YAAY,OAAO,OAAO,aAAa,UAAU;AAC9E,WAAO,OAAO;AAAA,EAChB;AAEA,SAAO;AACT;AAGO,MAAM,oBAAoB,CAAC,UAAU,cAAc,YAAY,WAAW,iBAAiB,cAAc;AAMzG,MAAM,WAAW;AAAA;AAAA,EAQtB,YAAY,SAKT;AAZH,SAAQ,aAAa,oBAAI,IAA0B;AACnD,SAAQ,mBAAmB,oBAAI,IAA6C;AAY1E,SAAK,YAAY,QAAQ;AACzB,SAAK,YAAY,IAAI,wCAAgB;AACrC,SAAK,qBAAqB,IAAI,8CAAmB,QAAQ,aAAa;AACtE,SAAK,qBAAqB,IAAI,8CAAmB,QAAQ,gBAAgB;AAAA,MACvE,MAAM,QAAQ;AAAA,IAChB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,YAA6C;AAC1D,eAAW,CAAC,SAAS,KAAK,KAAK,YAAY;AACzC,WAAK,WAAW,IAAI,SAAS,KAAK;AAAA,IACpC;AACA,WAAO,MAAM,UAAU,WAAW,IAAI,aAAa;AAAA,EACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,wBAAwB,SAA0B,SAAuC;AACvF,SAAK,iBAAiB,IAAI,SAAS,OAAO;AAC1C,WAAO,MAAM,oCAAoC,OAAO,GAAG;AAAA,EAC7D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,oBAAoB,SAAoB;AACtC,eAAW,WAAW,KAAK,WAAW,KAAK,GAAG;AAC5C,cAAQ,OAAO,SAAS,OAAO,QAAa,SAAkB;AAC5D,eAAO,KAAK,SAAS,SAAS,MAAM,MAAM;AAAA,MAC5C,CAAC;AAAA,IACH;AAGA,eAAW,WAAW,mBAAmB;AACvC,cAAQ,OAAO,SAAS,OAAO,QAAa,SAAkB;AAC5D,cAAM,UAAU,KAAK,iBAAiB,IAAI,OAAO;AACjD,YAAI,CAAC,SAAS;AACZ,gBAAM,IAAI,MAAM,+CAA+C,OAAO,GAAG;AAAA,QAC3E;AACA,eAAO,QAAQ,MAAM,MAAM;AAAA,MAC7B,CAAC;AAAA,IACH;AAEA,WAAO;AAAA,MACL,cAAc,KAAK,WAAW,IAAI,2BAA2B,kBAAkB,MAAM;AAAA,IACvF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,SAAS,SAAiB,MAAe,OAA+B;AAC5E,UAAM,QAAQ,KAAK,WAAW,IAAI,OAAO;AACzC,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,uBAAuB,OAAO,GAAG;AAAA,IACnD;AAEA,UAAM,iBAAiB,2BAA2B,IAAI;AACtD,yCAAiB,SAAS;AAAA,MACxB,QAAQ;AAAA,MACR,MAAM;AAAA,MACN,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,MACzD,SAAS;AAAA,QACP;AAAA,QACA,YAAY,MAAM;AAAA,QAClB,QAAQ,MAAM;AAAA,MAChB;AAAA,IACF,CAAC;AAGD,UAAM,aAAyB;AAAA,MAC7B,MAAM,QAAQ,CAAC;AAAA,MACf,QAAS,QAAgB,CAAC;AAAA,MAC1B,OAAO,CAAC;AAAA,MACR,SAAS,CAAC;AAAA,MACV,QAAQ,OAAO;AAAA,MACf,UAAU;AAAA,IACZ;AAGA,QAAI,MAAM,WAAW,SAAS,GAAG;AAC/B,YAAM,gBAAY,mDAAuB,YAAY,OAAO;AAC5D,YAAM,gBAAgB,MAAM,QAAQ;AAAA,QAClC,MAAM,WAAW,IAAI,UAAQ,KAAK,mBAAmB,QAAQ,IAAI,CAAC;AAAA,MACpE;AACA,gBAAM,8CAAkB,eAAe,SAAS;AAAA,IAClD;AAGA,QAAI,MAAM,aAAa;AACrB,YAAMA,UAAS,KAAK,UAAU;AAAA,QAC5B;AAAA,QACA,MAAM;AAAA,QACN,WAAW;AAAA,MACb;AACA,UAAI,CAACA,QAAO,OAAO;AACjB,eAAO;AAAA,UACL,OAAO;AAAA,UACP,SAAS;AAAA,UACT,SAASA,QAAO;AAAA,QAClB;AAAA,MACF;AAAA,IACF;AAGA,UAAM,kBAAkB,MAAM,KAAK,mBAAmB,aAAa,MAAM,UAAU;AAGnF,UAAM,kBAAkB,KAAK,UAAU,OAAO;AAC9C,UAAM,WAAW,MAAM,gBAAgB,SAAS,eAAe;AAG/D,UAAM,WAAO,gDAAoB,YAAY,eAAe;AAG5D,UAAM,SAAS,SAAS,MAAM,MAAM;AACpC,QAAI;AACJ,QAAI;AAEJ,QAAI;AACF,UAAI,OAAO,WAAW,YAAY;AAChC,cAAM,IAAI;AAAA,UACR,eAAe,MAAM,UAAU,oBAAoB,MAAM,MAAM;AAAA,QACjE;AAAA,MACF;AAEA,eAAS,MAAM,OAAO,KAAK,UAAU,IAAI;AACzC,2CAAiB,SAAS;AAAA,QACxB,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,QACzD,SAAS;AAAA,UACP;AAAA,UACA,YAAY,MAAM;AAAA,UAClB,QAAQ,MAAM;AAAA,QAChB;AAAA,MACF,CAAC;AAAA,IACH,SAAS,OAAO;AACd,oBAAc;AACd,2CAAiB,SAAS;AAAA,QACxB,QAAQ;AAAA,QACR,MAAM;AAAA,QACN,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,QACzD,SAAS;AAAA,UACP;AAAA,UACA,YAAY,MAAM;AAAA,UAClB,QAAQ,MAAM;AAAA,QAChB;AAAA,MACF,CAAC;AAAA,IACH,UAAE;AACA,UAAI;AACF,kBAAM,wDAA2B,QAAQ;AAAA,MAC3C,SAAS,cAAc;AACrB,6CAAiB,SAAS;AAAA,UACxB,QAAQ;AAAA,UACR,MAAM;AAAA,UACN,SAAS,wBAAwB,QAAQ,aAAa,UAAU,OAAO,YAAY;AAAA,UACnF,GAAI,mBAAmB,SAAY,EAAE,eAAe,IAAI,CAAC;AAAA,UACzD,SAAS;AAAA,YACP;AAAA,YACA,YAAY,MAAM;AAAA,YAClB,QAAQ,MAAM;AAAA,UAChB;AAAA,QACF,CAAC;AAED,YAAI,CAAC,aAAa;AAChB,wBAAc;AAAA,QAChB;AAAA,MACF;AAAA,IACF;AAEA,QAAI,aAAa;AACf,YAAM;AAAA,IACR;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,mBAAmB,MAAc,iBAA4B;AAC3D,SAAK,mBAAmB,SAAS,MAAM,eAAe;AAAA,EACxD;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,uBAAuB,MAAc,YAA0B;AAC7D,SAAK,mBAAmB,aAAa,MAAM,UAAU;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,sBAA4B;AAC1B,UAAM,UAA0D,CAAC;AAEjE,eAAW,CAAC,SAAS,KAAK,KAAK,KAAK,YAAY;AAC9C,UAAI,CAAC,KAAK,mBAAmB,IAAI,MAAM,UAAU,GAAG;AAClD,gBAAQ,KAAK,EAAE,SAAS,YAAY,MAAM,WAAW,CAAC;AAAA,MACxD;AAAA,IACF;AAEA,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,UAAU,QACb,IAAI,OAAK,gBAAgB,EAAE,OAAO,0BAA0B,EAAE,UAAU,GAAG,EAC3E,KAAK,IAAI;AAEZ,YAAM,IAAI;AAAA,QACR,eAAe,QAAQ,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA,EAGJ,OAAO;AAAA,MAClC;AAAA,IACF;AAEA,WAAO,KAAK,OAAO,KAAK,WAAW,IAAI,yCAAyC;AAAA,EAClF;AAAA;AAAA,EAGA,cAAwB;AACtB,WAAO,MAAM,KAAK,KAAK,WAAW,KAAK,CAAC;AAAA,EAC1C;AAAA;AAAA,EAGA,aAAa,SAA2C;AACtD,WAAO,KAAK,WAAW,IAAI,OAAO;AAAA,EACpC;AACF;","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.cjs';\nimport { WoodValidationError } from '../types/index.cjs';\nimport { SchemaValidator } from '../validation/schema_validator.cjs';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.cjs';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.cjs';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.cjs';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.cjs';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.validation_error',\n message: 'Request validation failed',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n errorCount: result.errors?.length ?? 0,\n },\n });\n throw new WoodValidationError('Request validation failed', result.errors ?? []);\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /**\n * Eagerly compile every operation's inputSchema so a malformed schema fails\n * at startup (with the offending channel named) instead of throwing an opaque\n * ajv error on the first request to that channel. Call after loadOperations().\n */\n precompileSchemas(): void {\n let compiled = 0;\n for (const [channel, opDef] of this.operations) {\n if (opDef.inputSchema) {\n this.validator.precompile(channel, opDef.inputSchema as Record<string, unknown>);\n compiled += 1;\n }\n }\n logger.info(`Pre-compiled ${compiled} operation input schema(s)`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":["result"]}
|
|
@@ -61,6 +61,12 @@ declare class WoodRouter {
|
|
|
61
61
|
* operation references a controller that was not pre-registered.
|
|
62
62
|
*/
|
|
63
63
|
validateControllers(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Eagerly compile every operation's inputSchema so a malformed schema fails
|
|
66
|
+
* at startup (with the offending channel named) instead of throwing an opaque
|
|
67
|
+
* ajv error on the first request to that channel. Call after loadOperations().
|
|
68
|
+
*/
|
|
69
|
+
precompileSchemas(): void;
|
|
64
70
|
/** Get all registered operation channels. */
|
|
65
71
|
getChannels(): string[];
|
|
66
72
|
/** Get a specific operation definition. */
|
|
@@ -61,6 +61,12 @@ declare class WoodRouter {
|
|
|
61
61
|
* operation references a controller that was not pre-registered.
|
|
62
62
|
*/
|
|
63
63
|
validateControllers(): void;
|
|
64
|
+
/**
|
|
65
|
+
* Eagerly compile every operation's inputSchema so a malformed schema fails
|
|
66
|
+
* at startup (with the offending channel named) instead of throwing an opaque
|
|
67
|
+
* ajv error on the first request to that channel. Call after loadOperations().
|
|
68
|
+
*/
|
|
69
|
+
precompileSchemas(): void;
|
|
64
70
|
/** Get all registered operation channels. */
|
|
65
71
|
getChannels(): string[];
|
|
66
72
|
/** Get a specific operation definition. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { WoodValidationError } from "../types/index.js";
|
|
1
2
|
import { SchemaValidator } from "../validation/schema_validator.js";
|
|
2
3
|
import { MiddlewareResolver } from "../middleware/middleware_resolver.js";
|
|
3
4
|
import { buildMiddlewareContext, executeMiddleware } from "../middleware/middleware_pipeline.js";
|
|
@@ -118,11 +119,19 @@ class WoodRouter {
|
|
|
118
119
|
ipcContext.body
|
|
119
120
|
);
|
|
120
121
|
if (!result2.valid) {
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
publishMainTrace("error", {
|
|
123
|
+
source: "wood.router",
|
|
124
|
+
type: "dispatch.validation_error",
|
|
123
125
|
message: "Request validation failed",
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
...conversationId !== void 0 ? { conversationId } : {},
|
|
127
|
+
payload: {
|
|
128
|
+
channel,
|
|
129
|
+
controller: opDef.controller,
|
|
130
|
+
action: opDef.action,
|
|
131
|
+
errorCount: result2.errors?.length ?? 0
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
throw new WoodValidationError("Request validation failed", result2.errors ?? []);
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);
|
|
@@ -230,6 +239,21 @@ ${details}`
|
|
|
230
239
|
}
|
|
231
240
|
logger.info(`All ${this.operations.size} operations have registered controllers`);
|
|
232
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Eagerly compile every operation's inputSchema so a malformed schema fails
|
|
244
|
+
* at startup (with the offending channel named) instead of throwing an opaque
|
|
245
|
+
* ajv error on the first request to that channel. Call after loadOperations().
|
|
246
|
+
*/
|
|
247
|
+
precompileSchemas() {
|
|
248
|
+
let compiled = 0;
|
|
249
|
+
for (const [channel, opDef] of this.operations) {
|
|
250
|
+
if (opDef.inputSchema) {
|
|
251
|
+
this.validator.precompile(channel, opDef.inputSchema);
|
|
252
|
+
compiled += 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
logger.info(`Pre-compiled ${compiled} operation input schema(s)`);
|
|
256
|
+
}
|
|
233
257
|
/** Get all registered operation channels. */
|
|
234
258
|
getChannels() {
|
|
235
259
|
return Array.from(this.operations.keys());
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.js';\nimport { SchemaValidator } from '../validation/schema_validator.js';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.js';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.js';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.js';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.js';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n return {\n error: 'VALIDATION_ERROR',\n message: 'Request validation failed',\n details: result.errors,\n };\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../../src/router/wood_router.ts"],"sourcesContent":["import type { OperationDef, IpcContext, WoodMiddlewareContext } from '../types/index.js';\nimport { WoodValidationError } from '../types/index.js';\nimport { SchemaValidator } from '../validation/schema_validator.js';\nimport { MiddlewareResolver } from '../middleware/middleware_resolver.js';\nimport { buildMiddlewareContext, executeMiddleware } from '../middleware/middleware_pipeline.js';\nimport { ControllerResolver, buildControllerArgs } from '../controller/controller_resolver.js';\nimport { disposeControllerLifecycle } from '../controller/controller_lifecycle.js';\nimport { getLogger } from '@noego/logger';\nimport { publishMainTrace } from '../tracing/index.js';\n\nconst logger = getLogger('wood:router');\n\nfunction extractTraceConversationId(value: unknown): number | string | undefined {\n if (!value || typeof value !== 'object' || Array.isArray(value)) {\n return undefined;\n }\n\n const record = value as Record<string, unknown>;\n if (typeof record.conversationId === 'number' || typeof record.conversationId === 'string') {\n return record.conversationId;\n }\n if (typeof record.threadId === 'number' || typeof record.threadId === 'string') {\n return record.threadId;\n }\n\n return undefined;\n}\n\n/** Reserved internal channels prefixed with __ to avoid collisions with user operations. */\nexport const RESERVED_CHANNELS = ['__load', '__navigate', '__window', '__debug', '__contextMenu', '__woodConfig'] as const;\nexport type ReservedChannel = (typeof RESERVED_CHANNELS)[number];\n\n/** Handler signature for reserved channels. */\nexport type ReservedChannelHandler = (data: unknown, event?: any) => Promise<unknown>;\n\nexport class WoodRouter {\n private operations = new Map<string, OperationDef>();\n private reservedHandlers = new Map<ReservedChannel, ReservedChannelHandler>();\n private validator: SchemaValidator;\n private middlewareResolver: MiddlewareResolver;\n private controllerResolver: ControllerResolver;\n private container: any; // IoC Container\n\n constructor(options: {\n container: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n }) {\n this.container = options.container;\n this.validator = new SchemaValidator();\n this.middlewareResolver = new MiddlewareResolver(options.middlewareDir);\n this.controllerResolver = new ControllerResolver(options.controllersDir, {\n mode: options.mode,\n });\n }\n\n /**\n * Load operations from a parsed operations map.\n */\n loadOperations(operations: Map<string, OperationDef>): void {\n for (const [channel, opDef] of operations) {\n this.operations.set(channel, opDef);\n }\n logger.debug(`Loaded ${operations.size} operations`);\n }\n\n /**\n * Register a handler for a reserved channel (__load, __navigate, __window).\n * These channels are internal to Wood and serve as extension points for\n * the loader runner, navigation system, and window manager.\n */\n registerReservedHandler(channel: ReservedChannel, handler: ReservedChannelHandler): void {\n this.reservedHandlers.set(channel, handler);\n logger.debug(`Registered reserved handler for \"${channel}\"`);\n }\n\n /**\n * Register IPC handlers for all operations and reserved channels.\n * In production: registers with ipcMain.handle\n * In testing/bridge mode: handlers are callable via dispatch()\n */\n registerIpcHandlers(ipcMain: any): void {\n for (const channel of this.operations.keys()) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n return this.dispatch(channel, data, _event);\n });\n }\n\n // Register reserved channels\n for (const channel of RESERVED_CHANNELS) {\n ipcMain.handle(channel, async (_event: any, data: unknown) => {\n const handler = this.reservedHandlers.get(channel);\n if (!handler) {\n throw new Error(`No handler registered for reserved channel \"${channel}\"`);\n }\n return handler(data, _event);\n });\n }\n\n logger.debug(\n `Registered ${this.operations.size} operation handlers and ${RESERVED_CHANNELS.length} reserved channels`\n );\n }\n\n /**\n * The request lifecycle -- mirrors Dinner's pipeline.\n *\n * channel -> build context -> middleware -> validate -> resolve controller\n * -> create instance -> build args -> call action -> return result\n */\n async dispatch(channel: string, data: unknown, event?: any): Promise<unknown> {\n const opDef = this.operations.get(channel);\n if (!opDef) {\n throw new Error(`Unknown operation: \"${channel}\"`);\n }\n\n const conversationId = extractTraceConversationId(data);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.start',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n // 1. Build IPC context\n const ipcContext: IpcContext = {\n body: data ?? {},\n params: (data as any) ?? {},\n query: {},\n context: {},\n sender: event?.sender,\n windowId: undefined,\n };\n\n // 2. Execute middleware sequentially\n if (opDef.middleware.length > 0) {\n const mwContext = buildMiddlewareContext(ipcContext, channel);\n const middlewareFns = await Promise.all(\n opDef.middleware.map(spec => this.middlewareResolver.resolve(spec))\n );\n await executeMiddleware(middlewareFns, mwContext);\n }\n\n // 3. Validate input (if schema defined)\n if (opDef.inputSchema) {\n const result = this.validator.validate(\n channel,\n opDef.inputSchema as Record<string, unknown>,\n ipcContext.body\n );\n if (!result.valid) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.validation_error',\n message: 'Request validation failed',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n errorCount: result.errors?.length ?? 0,\n },\n });\n throw new WoodValidationError('Request validation failed', result.errors ?? []);\n }\n }\n\n // 4. Resolve controller class (cached)\n const ControllerClass = await this.controllerResolver.resolveClass(opDef.controller);\n\n // 5. Create instance via IoC (per-request scoped container)\n const scopedContainer = this.container.extend();\n const instance = await scopedContainer.instance(ControllerClass);\n\n // 6. Build controller args (fake req/res)\n const args = buildControllerArgs(ipcContext, scopedContainer);\n\n // 7. Call action, always dispose controller afterward\n const action = instance[opDef.action];\n let result: unknown;\n let actionError: unknown;\n\n try {\n if (typeof action !== 'function') {\n throw new Error(\n `Controller \"${opDef.controller}\" has no action \"${opDef.action}\"`\n );\n }\n\n result = await action.call(instance, args);\n publishMainTrace('debug', {\n source: 'wood.router',\n type: 'dispatch.success',\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } catch (error) {\n actionError = error;\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.error',\n message: error instanceof Error ? error.message : String(error),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n } finally {\n try {\n await disposeControllerLifecycle(instance);\n } catch (disposeError) {\n publishMainTrace('error', {\n source: 'wood.router',\n type: 'dispatch.dispose.error',\n message: disposeError instanceof Error ? disposeError.message : String(disposeError),\n ...(conversationId !== undefined ? { conversationId } : {}),\n payload: {\n channel,\n controller: opDef.controller,\n action: opDef.action,\n },\n });\n\n if (!actionError) {\n actionError = disposeError;\n }\n }\n }\n\n if (actionError) {\n throw actionError;\n }\n\n return result;\n }\n\n /**\n * Pre-register a controller class by name so it is resolved from\n * the in-memory cache instead of the filesystem.\n * This is the public API used by the generated registration file.\n */\n registerController(name: string, ControllerClass: any): void {\n this.controllerResolver.register(name, ControllerClass);\n }\n\n /**\n * Register an import path for a controller. The controller will be lazily\n * imported from this path when first needed, avoiding circular dependencies.\n */\n registerControllerPath(name: string, importPath: string): void {\n this.controllerResolver.registerPath(name, importPath);\n }\n\n /**\n * Validate that every controller referenced in loaded operations is\n * pre-registered in the controller resolver. Call this after\n * loadOperations() + registerControllers() but before registerIpcHandlers().\n *\n * Throws with a clear error listing all missing controllers if any\n * operation references a controller that was not pre-registered.\n */\n validateControllers(): void {\n const missing: Array<{ channel: string; controller: string }> = [];\n\n for (const [channel, opDef] of this.operations) {\n if (!this.controllerResolver.has(opDef.controller)) {\n missing.push({ channel, controller: opDef.controller });\n }\n }\n\n if (missing.length > 0) {\n const details = missing\n .map(m => ` - channel \"${m.channel}\" requires controller \"${m.controller}\"`)\n .join('\\n');\n\n throw new Error(\n `WoodRouter: ${missing.length} operation(s) reference controllers that are not registered.\\n` +\n `This usually means controllers.generated.ts is missing or out of date.\\n` +\n `Run \"npx wood build\" or \"npx wood dev\" to regenerate it.\\n\\n` +\n `Missing controllers:\\n${details}`\n );\n }\n\n logger.info(`All ${this.operations.size} operations have registered controllers`);\n }\n\n /**\n * Eagerly compile every operation's inputSchema so a malformed schema fails\n * at startup (with the offending channel named) instead of throwing an opaque\n * ajv error on the first request to that channel. Call after loadOperations().\n */\n precompileSchemas(): void {\n let compiled = 0;\n for (const [channel, opDef] of this.operations) {\n if (opDef.inputSchema) {\n this.validator.precompile(channel, opDef.inputSchema as Record<string, unknown>);\n compiled += 1;\n }\n }\n logger.info(`Pre-compiled ${compiled} operation input schema(s)`);\n }\n\n /** Get all registered operation channels. */\n getChannels(): string[] {\n return Array.from(this.operations.keys());\n }\n\n /** Get a specific operation definition. */\n getOperation(channel: string): OperationDef | undefined {\n return this.operations.get(channel);\n }\n}\n"],"mappings":"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","names":["result"]}
|
package/dist/runtime/runtime.cjs
CHANGED
|
@@ -92,6 +92,8 @@ async function runtime(options) {
|
|
|
92
92
|
logger.info("runtime registration complete");
|
|
93
93
|
router.validateControllers();
|
|
94
94
|
logger.info("runtime controller validation complete");
|
|
95
|
+
router.precompileSchemas();
|
|
96
|
+
logger.info("runtime schema pre-compilation complete");
|
|
95
97
|
router.registerReservedHandler("__load", async (data, event) => {
|
|
96
98
|
if (!options.onLoad) {
|
|
97
99
|
throw new Error('Reserved channel "__load" not configured. Provide runtime({ onLoad }).');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/runtime.ts"],"sourcesContent":["import { getLogger } from '@noego/logger';\nimport { WoodRouter } from '../router/wood_router.cjs';\nimport { getContainer } from './get_container.cjs';\nimport { TraceProviderConfiguration, dispatchRendererTracePayload } from '../tracing/index.js';\nimport {\n setMainTraceMinLevel,\n setRendererTraceMinLevel,\n type TraceLevel,\n type TraceRuntimeContext,\n} from '../tracing/index.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogDispatchResult,\n type RendererLogEntry,\n type RendererLogLevel,\n} from './renderer_log_dispatcher.js';\n\nconst logger = getLogger('wood:runtime');\n\ntype IpcMainLike = {\n handle: (channel: string, handler: (_event: unknown, data: unknown) => Promise<unknown> | unknown) => void;\n on: (channel: string, handler: (_event: unknown, ...args: unknown[]) => void) => void;\n removeHandler?: (channel: string) => void;\n removeAllListeners?: (channel: string) => void;\n};\n\nexport interface RegisterRuntimeRouter {\n registerController(name: string, cls: any): void;\n loadOperations(operations: Map<string, any>): void;\n}\n\nexport interface RuntimeOptions {\n ipcMain: IpcMainLike;\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n container?: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n enableRendererLogBridge?: boolean;\n enableRendererTraceBridge?: boolean;\n traceMinLevel?: TraceLevel;\n rendererTraceEnabled?: boolean;\n rendererTraceMinLevel?: TraceLevel;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onNavigate?: (data: unknown, event?: unknown) => Promise<unknown>;\n onWindow?: (data: unknown, event?: unknown) => Promise<unknown>;\n onDebug?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n resolveRendererTraceContext?: (event: unknown) => TraceRuntimeContext | undefined;\n}\n\nexport interface RuntimeResult {\n router: WoodRouter;\n}\n\ntype RendererLogPayload = {\n level: string;\n logger: string;\n message: string;\n context?: unknown;\n};\n\ntype IpcInvokeEventLike = {\n sender?: {\n id?: number;\n };\n};\n\nfunction normalizeRendererLogLevel(level: unknown): RendererLogLevel {\n if (level === 'debug' || level === 'warn' || level === 'error') {\n return level;\n }\n return 'info';\n}\n\nfunction coerceRendererLogPayload(\n rawPayload: unknown,\n event: unknown,\n): RendererLogEntry {\n const payload = (rawPayload ?? {}) as Partial<RendererLogPayload>;\n const ipcEvent = event as IpcInvokeEventLike;\n\n return {\n level: normalizeRendererLogLevel(payload.level),\n logger: typeof payload.logger === 'string' && payload.logger.trim().length > 0\n ? payload.logger\n : 'renderer',\n message: typeof payload.message === 'string' ? payload.message : '',\n context: payload.context,\n timestamp: new Date().toISOString(),\n senderId: typeof ipcEvent?.sender?.id === 'number' ? ipcEvent.sender.id : null,\n };\n}\n\n/**\n * Boot a bundled-safe Wood runtime using pre-generated static runtime artifacts.\n */\nexport async function runtime(options: RuntimeOptions): Promise<RuntimeResult> {\n const {\n ipcMain,\n registerRuntime,\n controllersDir,\n middlewareDir,\n mode = 'bundled',\n enableRendererLogBridge = true,\n enableRendererTraceBridge = true,\n traceMinLevel,\n rendererTraceEnabled = false,\n rendererTraceMinLevel = 'debug',\n } = options;\n\n const container = options.container ?? getContainer();\n\n logger.info('runtime start', {\n mode,\n hasProvidedContainer: !!options.container,\n controllersDir,\n middlewareDir,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n traceMinLevel: traceMinLevel ?? 'debug',\n rendererTraceEnabled,\n rendererTraceMinLevel,\n });\n\n if (traceMinLevel) {\n setMainTraceMinLevel(traceMinLevel);\n }\n setRendererTraceMinLevel(rendererTraceMinLevel);\n\n if (typeof container.instance === 'function') {\n const traceConfiguration = await container.instance(TraceProviderConfiguration);\n traceConfiguration.configureRuntime('main');\n if (traceMinLevel) {\n traceConfiguration.configureMinLevel(traceMinLevel);\n }\n }\n\n const router = new WoodRouter({\n container,\n controllersDir,\n middlewareDir,\n mode,\n });\n\n registerRuntime(router);\n logger.info('runtime registration complete');\n\n router.validateControllers();\n logger.info('runtime controller validation complete');\n\n router.registerReservedHandler('__load', async (data, event) => {\n if (!options.onLoad) {\n throw new Error('Reserved channel \"__load\" not configured. Provide runtime({ onLoad }).');\n }\n return options.onLoad(data, event);\n });\n\n router.registerReservedHandler('__navigate', async (data, event) => {\n if (!options.onNavigate) {\n throw new Error('Reserved channel \"__navigate\" not configured. Provide runtime({ onNavigate }).');\n }\n return options.onNavigate(data, event);\n });\n\n router.registerReservedHandler('__window', async (data, event) => {\n if (!options.onWindow) {\n throw new Error('Reserved channel \"__window\" not configured. Provide runtime({ onWindow }).');\n }\n return options.onWindow(data, event);\n });\n\n router.registerReservedHandler('__debug', async (data, event) => {\n if (!options.onDebug) {\n throw new Error('Reserved channel \"__debug\" not configured. Provide runtime({ onDebug }).');\n }\n return options.onDebug(data, event);\n });\n\n router.registerReservedHandler('__contextMenu', async (data, event) => {\n if (!options.onContextMenu) {\n throw new Error('Reserved channel \"__contextMenu\" not configured. Provide runtime({ onContextMenu }).');\n }\n return options.onContextMenu(data, event);\n });\n\n router.registerReservedHandler('__woodConfig', async () => {\n return {\n rendererTraceEnabled,\n rendererTraceMinLevel,\n };\n });\n\n if (enableRendererLogBridge) {\n registerRendererLogBridge(ipcMain);\n }\n\n ipcMain.removeAllListeners?.('__trace');\n\n if (enableRendererTraceBridge && rendererTraceEnabled) {\n registerRendererTraceBridge(ipcMain, options.resolveRendererTraceContext);\n }\n\n router.registerIpcHandlers(ipcMain);\n logger.info('runtime ipc handlers registered', {\n channelCount: router.getChannels().length,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n });\n\n return { router };\n}\n\nfunction registerRendererTraceBridge(\n ipcMain: IpcMainLike,\n resolveContext?: (event: unknown) => TraceRuntimeContext | undefined,\n): void {\n ipcMain.removeAllListeners?.('__trace');\n\n ipcMain.on('__trace', (event, rawPayload) => {\n dispatchRendererTracePayload(rawPayload, resolveContext?.(event));\n });\n\n logger.info('renderer trace bridge registered (fire-and-forget)');\n}\n\nfunction registerRendererLogBridge(ipcMain: IpcMainLike): void {\n const dispatcher = getRendererLogDispatcher();\n\n ipcMain.removeAllListeners?.('__log');\n\n ipcMain.on('__log', (_event, rawPayload) => {\n const entry = coerceRendererLogPayload(rawPayload, null);\n void dispatcher.dispatch(entry).then((result) => {\n const typedResult = result as RendererLogDispatchResult;\n if (!typedResult.ok) {\n logger.warn('renderer log dispatch failed', {\n logger: entry.logger,\n messageLength: entry.message.length,\n reason: typedResult.reason ?? null,\n errorMessage: typedResult.errorMessage ?? null,\n });\n }\n });\n });\n\n logger.info('renderer log bridge registered (fire-and-forget)');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,oBAA0B;AAC1B,yBAA2B;AAC3B,2BAA6B;AAC7B,qBAAyE;AACzE,IAAAA,kBAKO;AACP,qCAKO;AAEP,MAAM,aAAS,yBAAU,cAAc;AAmDvC,SAAS,0BAA0B,OAAkC;AACnE,MAAI,UAAU,WAAW,UAAU,UAAU,UAAU,SAAS;AAC9D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEA,SAAS,yBACP,YACA,OACkB;AAClB,QAAM,UAAW,cAAc,CAAC;AAChC,QAAM,WAAW;AAEjB,SAAO;AAAA,IACL,OAAO,0BAA0B,QAAQ,KAAK;AAAA,IAC9C,QAAQ,OAAO,QAAQ,WAAW,YAAY,QAAQ,OAAO,KAAK,EAAE,SAAS,IACzE,QAAQ,SACR;AAAA,IACJ,SAAS,OAAO,QAAQ,YAAY,WAAW,QAAQ,UAAU;AAAA,IACjE,SAAS,QAAQ;AAAA,IACjB,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,UAAU,OAAO,UAAU,QAAQ,OAAO,WAAW,SAAS,OAAO,KAAK;AAAA,EAC5E;AACF;AAKA,eAAsB,QAAQ,SAAiD;AAC7E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO;AAAA,IACP,0BAA0B;AAAA,IAC1B,4BAA4B;AAAA,IAC5B;AAAA,IACA,uBAAuB;AAAA,IACvB,wBAAwB;AAAA,EAC1B,IAAI;AAEJ,QAAM,YAAY,QAAQ,iBAAa,mCAAa;AAEpD,SAAO,KAAK,iBAAiB;AAAA,IAC3B;AAAA,IACA,sBAAsB,CAAC,CAAC,QAAQ;AAAA,IAChC;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,eAAe,iBAAiB;AAAA,IAChC;AAAA,IACA;AAAA,EACF,CAAC;AAED,MAAI,eAAe;AACjB,8CAAqB,aAAa;AAAA,EACpC;AACA,gDAAyB,qBAAqB;AAE9C,MAAI,OAAO,UAAU,aAAa,YAAY;AAC5C,UAAM,qBAAqB,MAAM,UAAU,SAAS,yCAA0B;AAC9E,uBAAmB,iBAAiB,MAAM;AAC1C,QAAI,eAAe;AACjB,yBAAmB,kBAAkB,aAAa;AAAA,IACpD;AAAA,EACF;AAEA,QAAM,SAAS,IAAI,8BAAW;AAAA,IAC5B;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,kBAAgB,MAAM;AACtB,SAAO,KAAK,+BAA+B;AAE3C,SAAO,oBAAoB;AAC3B,SAAO,KAAK,wCAAwC;AAEpD,SAAO,wBAAwB,UAAU,OAAO,MAAM,UAAU;AAC9D,QAAI,CAAC,QAAQ,QAAQ;AACnB,YAAM,IAAI,MAAM,wEAAwE;AAAA,IAC1F;AACA,WAAO,QAAQ,OAAO,MAAM,KAAK;AAAA,EACnC,CAAC;AAED,SAAO,wBAAwB,cAAc,OAAO,MAAM,UAAU;AAClE,QAAI,CAAC,QAAQ,YAAY;AACvB,YAAM,IAAI,MAAM,gFAAgF;AAAA,IAClG;AACA,WAAO,QAAQ,WAAW,MAAM,KAAK;AAAA,EACvC,CAAC;AAED,SAAO,wBAAwB,YAAY,OAAO,MAAM,UAAU;AAChE,QAAI,CAAC,QAAQ,UAAU;AACrB,YAAM,IAAI,MAAM,4EAA4E;AAAA,IAC9F;AACA,WAAO,QAAQ,SAAS,MAAM,KAAK;AAAA,EACrC,CAAC;AAED,SAAO,wBAAwB,WAAW,OAAO,MAAM,UAAU;AAC/D,QAAI,CAAC,QAAQ,SAAS;AACpB,YAAM,IAAI,MAAM,0EAA0E;AAAA,IAC5F;AACA,WAAO,QAAQ,QAAQ,MAAM,KAAK;AAAA,EACpC,CAAC;AAED,SAAO,wBAAwB,iBAAiB,OAAO,MAAM,UAAU;AACrE,QAAI,CAAC,QAAQ,eAAe;AAC1B,YAAM,IAAI,MAAM,sFAAsF;AAAA,IACxG;AACA,WAAO,QAAQ,cAAc,MAAM,KAAK;AAAA,EAC1C,CAAC;AAED,SAAO,wBAAwB,gBAAgB,YAAY;AACzD,WAAO;AAAA,MACL;AAAA,MACA;AAAA,IACF;AAAA,EACF,CAAC;AAED,MAAI,yBAAyB;AAC3B,8BAA0B,OAAO;AAAA,EACnC;AAEA,UAAQ,qBAAqB,SAAS;AAEtC,MAAI,6BAA6B,sBAAsB;AACrD,gCAA4B,SAAS,QAAQ,2BAA2B;AAAA,EAC1E;AAEA,SAAO,oBAAoB,OAAO;AAClC,SAAO,KAAK,mCAAmC;AAAA,IAC7C,cAAc,OAAO,YAAY,EAAE;AAAA,IACnC;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,EAAE,OAAO;AAClB;AAEA,SAAS,4BACP,SACA,gBACM;AACN,UAAQ,qBAAqB,SAAS;AAEtC,UAAQ,GAAG,WAAW,CAAC,OAAO,eAAe;AAC3C,qDAA6B,YAAY,iBAAiB,KAAK,CAAC;AAAA,EAClE,CAAC;AAED,SAAO,KAAK,oDAAoD;AAClE;AAEA,SAAS,0BAA0B,SAA4B;AAC7D,QAAM,iBAAa,yDAAyB;AAE5C,UAAQ,qBAAqB,OAAO;AAEpC,UAAQ,GAAG,SAAS,CAAC,QAAQ,eAAe;AAC1C,UAAM,QAAQ,yBAAyB,YAAY,IAAI;AACvD,SAAK,WAAW,SAAS,KAAK,EAAE,KAAK,CAAC,WAAW;AAC/C,YAAM,cAAc;AACpB,UAAI,CAAC,YAAY,IAAI;AACnB,eAAO,KAAK,gCAAgC;AAAA,UAC1C,QAAQ,MAAM;AAAA,UACd,eAAe,MAAM,QAAQ;AAAA,UAC7B,QAAQ,YAAY,UAAU;AAAA,UAC9B,cAAc,YAAY,gBAAgB;AAAA,QAC5C,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AAED,SAAO,KAAK,kDAAkD;AAChE;","names":["import_tracing"]}
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/runtime.ts"],"sourcesContent":["import { getLogger } from '@noego/logger';\nimport { WoodRouter } from '../router/wood_router.cjs';\nimport { getContainer } from './get_container.cjs';\nimport { TraceProviderConfiguration, dispatchRendererTracePayload } from '../tracing/index.js';\nimport {\n setMainTraceMinLevel,\n setRendererTraceMinLevel,\n type TraceLevel,\n type TraceRuntimeContext,\n} from '../tracing/index.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogDispatchResult,\n type RendererLogEntry,\n type RendererLogLevel,\n} from './renderer_log_dispatcher.js';\n\nconst logger = getLogger('wood:runtime');\n\ntype IpcMainLike = {\n handle: (channel: string, handler: (_event: unknown, data: unknown) => Promise<unknown> | unknown) => void;\n on: (channel: string, handler: (_event: unknown, ...args: unknown[]) => void) => void;\n removeHandler?: (channel: string) => void;\n removeAllListeners?: (channel: string) => void;\n};\n\nexport interface RegisterRuntimeRouter {\n registerController(name: string, cls: any): void;\n loadOperations(operations: Map<string, any>): void;\n}\n\nexport interface RuntimeOptions {\n ipcMain: IpcMainLike;\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n container?: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n enableRendererLogBridge?: boolean;\n enableRendererTraceBridge?: boolean;\n traceMinLevel?: TraceLevel;\n rendererTraceEnabled?: boolean;\n rendererTraceMinLevel?: TraceLevel;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onNavigate?: (data: unknown, event?: unknown) => Promise<unknown>;\n onWindow?: (data: unknown, event?: unknown) => Promise<unknown>;\n onDebug?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n resolveRendererTraceContext?: (event: unknown) => TraceRuntimeContext | undefined;\n}\n\nexport interface RuntimeResult {\n router: WoodRouter;\n}\n\ntype RendererLogPayload = {\n level: string;\n logger: string;\n message: string;\n context?: unknown;\n};\n\ntype IpcInvokeEventLike = {\n sender?: {\n id?: number;\n };\n};\n\nfunction normalizeRendererLogLevel(level: unknown): RendererLogLevel {\n if (level === 'debug' || level === 'warn' || level === 'error') {\n return level;\n }\n return 'info';\n}\n\nfunction coerceRendererLogPayload(\n rawPayload: unknown,\n event: unknown,\n): RendererLogEntry {\n const payload = (rawPayload ?? {}) as Partial<RendererLogPayload>;\n const ipcEvent = event as IpcInvokeEventLike;\n\n return {\n level: normalizeRendererLogLevel(payload.level),\n logger: typeof payload.logger === 'string' && payload.logger.trim().length > 0\n ? payload.logger\n : 'renderer',\n message: typeof payload.message === 'string' ? payload.message : '',\n context: payload.context,\n timestamp: new Date().toISOString(),\n senderId: typeof ipcEvent?.sender?.id === 'number' ? ipcEvent.sender.id : null,\n };\n}\n\n/**\n * Boot a bundled-safe Wood runtime using pre-generated static runtime artifacts.\n */\nexport async function runtime(options: RuntimeOptions): Promise<RuntimeResult> {\n const {\n ipcMain,\n registerRuntime,\n controllersDir,\n middlewareDir,\n mode = 'bundled',\n enableRendererLogBridge = true,\n enableRendererTraceBridge = true,\n traceMinLevel,\n rendererTraceEnabled = false,\n rendererTraceMinLevel = 'debug',\n } = options;\n\n const container = options.container ?? getContainer();\n\n logger.info('runtime start', {\n mode,\n hasProvidedContainer: !!options.container,\n controllersDir,\n middlewareDir,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n traceMinLevel: traceMinLevel ?? 'debug',\n rendererTraceEnabled,\n rendererTraceMinLevel,\n });\n\n if (traceMinLevel) {\n setMainTraceMinLevel(traceMinLevel);\n }\n setRendererTraceMinLevel(rendererTraceMinLevel);\n\n if (typeof container.instance === 'function') {\n const traceConfiguration = await container.instance(TraceProviderConfiguration);\n traceConfiguration.configureRuntime('main');\n if (traceMinLevel) {\n traceConfiguration.configureMinLevel(traceMinLevel);\n }\n }\n\n const router = new WoodRouter({\n container,\n controllersDir,\n middlewareDir,\n mode,\n });\n\n registerRuntime(router);\n logger.info('runtime registration complete');\n\n router.validateControllers();\n logger.info('runtime controller validation complete');\n\n router.precompileSchemas();\n logger.info('runtime schema pre-compilation complete');\n\n router.registerReservedHandler('__load', async (data, event) => {\n if (!options.onLoad) {\n throw new Error('Reserved channel \"__load\" not configured. Provide runtime({ onLoad }).');\n }\n return options.onLoad(data, event);\n });\n\n router.registerReservedHandler('__navigate', async (data, event) => {\n if (!options.onNavigate) {\n throw new Error('Reserved channel \"__navigate\" not configured. Provide runtime({ onNavigate }).');\n }\n return options.onNavigate(data, event);\n });\n\n router.registerReservedHandler('__window', async (data, event) => {\n if (!options.onWindow) {\n throw new Error('Reserved channel \"__window\" not configured. Provide runtime({ onWindow }).');\n }\n return options.onWindow(data, event);\n });\n\n router.registerReservedHandler('__debug', async (data, event) => {\n if (!options.onDebug) {\n throw new Error('Reserved channel \"__debug\" not configured. Provide runtime({ onDebug }).');\n }\n return options.onDebug(data, event);\n });\n\n router.registerReservedHandler('__contextMenu', async (data, event) => {\n if (!options.onContextMenu) {\n throw new Error('Reserved channel \"__contextMenu\" not configured. Provide runtime({ onContextMenu }).');\n }\n return options.onContextMenu(data, event);\n });\n\n router.registerReservedHandler('__woodConfig', async () => {\n return {\n rendererTraceEnabled,\n rendererTraceMinLevel,\n };\n });\n\n if (enableRendererLogBridge) {\n registerRendererLogBridge(ipcMain);\n }\n\n ipcMain.removeAllListeners?.('__trace');\n\n if (enableRendererTraceBridge && rendererTraceEnabled) {\n registerRendererTraceBridge(ipcMain, options.resolveRendererTraceContext);\n }\n\n router.registerIpcHandlers(ipcMain);\n logger.info('runtime ipc handlers registered', {\n channelCount: router.getChannels().length,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n });\n\n return { router };\n}\n\nfunction registerRendererTraceBridge(\n ipcMain: IpcMainLike,\n resolveContext?: (event: unknown) => TraceRuntimeContext | undefined,\n): void {\n ipcMain.removeAllListeners?.('__trace');\n\n ipcMain.on('__trace', (event, rawPayload) => {\n dispatchRendererTracePayload(rawPayload, resolveContext?.(event));\n });\n\n logger.info('renderer trace bridge registered (fire-and-forget)');\n}\n\nfunction registerRendererLogBridge(ipcMain: IpcMainLike): void {\n const dispatcher = getRendererLogDispatcher();\n\n ipcMain.removeAllListeners?.('__log');\n\n ipcMain.on('__log', (_event, rawPayload) => {\n const entry = coerceRendererLogPayload(rawPayload, null);\n void dispatcher.dispatch(entry).then((result) => {\n const typedResult = result as RendererLogDispatchResult;\n if (!typedResult.ok) {\n logger.warn('renderer log dispatch failed', {\n logger: entry.logger,\n messageLength: entry.message.length,\n reason: typedResult.reason ?? null,\n errorMessage: typedResult.errorMessage ?? null,\n });\n }\n });\n });\n\n logger.info('renderer log bridge registered (fire-and-forget)');\n}\n"],"mappings":"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","names":["import_tracing"]}
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -74,6 +74,8 @@ async function runtime(options) {
|
|
|
74
74
|
logger.info("runtime registration complete");
|
|
75
75
|
router.validateControllers();
|
|
76
76
|
logger.info("runtime controller validation complete");
|
|
77
|
+
router.precompileSchemas();
|
|
78
|
+
logger.info("runtime schema pre-compilation complete");
|
|
77
79
|
router.registerReservedHandler("__load", async (data, event) => {
|
|
78
80
|
if (!options.onLoad) {
|
|
79
81
|
throw new Error('Reserved channel "__load" not configured. Provide runtime({ onLoad }).');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/runtime/runtime.ts"],"sourcesContent":["import { getLogger } from '@noego/logger';\nimport { WoodRouter } from '../router/wood_router.js';\nimport { getContainer } from './get_container.js';\nimport { TraceProviderConfiguration, dispatchRendererTracePayload } from '../tracing/index.js';\nimport {\n setMainTraceMinLevel,\n setRendererTraceMinLevel,\n type TraceLevel,\n type TraceRuntimeContext,\n} from '../tracing/index.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogDispatchResult,\n type RendererLogEntry,\n type RendererLogLevel,\n} from './renderer_log_dispatcher.js';\n\nconst logger = getLogger('wood:runtime');\n\ntype IpcMainLike = {\n handle: (channel: string, handler: (_event: unknown, data: unknown) => Promise<unknown> | unknown) => void;\n on: (channel: string, handler: (_event: unknown, ...args: unknown[]) => void) => void;\n removeHandler?: (channel: string) => void;\n removeAllListeners?: (channel: string) => void;\n};\n\nexport interface RegisterRuntimeRouter {\n registerController(name: string, cls: any): void;\n loadOperations(operations: Map<string, any>): void;\n}\n\nexport interface RuntimeOptions {\n ipcMain: IpcMainLike;\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n container?: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n enableRendererLogBridge?: boolean;\n enableRendererTraceBridge?: boolean;\n traceMinLevel?: TraceLevel;\n rendererTraceEnabled?: boolean;\n rendererTraceMinLevel?: TraceLevel;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onNavigate?: (data: unknown, event?: unknown) => Promise<unknown>;\n onWindow?: (data: unknown, event?: unknown) => Promise<unknown>;\n onDebug?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n resolveRendererTraceContext?: (event: unknown) => TraceRuntimeContext | undefined;\n}\n\nexport interface RuntimeResult {\n router: WoodRouter;\n}\n\ntype RendererLogPayload = {\n level: string;\n logger: string;\n message: string;\n context?: unknown;\n};\n\ntype IpcInvokeEventLike = {\n sender?: {\n id?: number;\n };\n};\n\nfunction normalizeRendererLogLevel(level: unknown): RendererLogLevel {\n if (level === 'debug' || level === 'warn' || level === 'error') {\n return level;\n }\n return 'info';\n}\n\nfunction coerceRendererLogPayload(\n rawPayload: unknown,\n event: unknown,\n): RendererLogEntry {\n const payload = (rawPayload ?? {}) as Partial<RendererLogPayload>;\n const ipcEvent = event as IpcInvokeEventLike;\n\n return {\n level: normalizeRendererLogLevel(payload.level),\n logger: typeof payload.logger === 'string' && payload.logger.trim().length > 0\n ? payload.logger\n : 'renderer',\n message: typeof payload.message === 'string' ? payload.message : '',\n context: payload.context,\n timestamp: new Date().toISOString(),\n senderId: typeof ipcEvent?.sender?.id === 'number' ? ipcEvent.sender.id : null,\n };\n}\n\n/**\n * Boot a bundled-safe Wood runtime using pre-generated static runtime artifacts.\n */\nexport async function runtime(options: RuntimeOptions): Promise<RuntimeResult> {\n const {\n ipcMain,\n registerRuntime,\n controllersDir,\n middlewareDir,\n mode = 'bundled',\n enableRendererLogBridge = true,\n enableRendererTraceBridge = true,\n traceMinLevel,\n rendererTraceEnabled = false,\n rendererTraceMinLevel = 'debug',\n } = options;\n\n const container = options.container ?? getContainer();\n\n logger.info('runtime start', {\n mode,\n hasProvidedContainer: !!options.container,\n controllersDir,\n middlewareDir,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n traceMinLevel: traceMinLevel ?? 'debug',\n rendererTraceEnabled,\n rendererTraceMinLevel,\n });\n\n if (traceMinLevel) {\n setMainTraceMinLevel(traceMinLevel);\n }\n setRendererTraceMinLevel(rendererTraceMinLevel);\n\n if (typeof container.instance === 'function') {\n const traceConfiguration = await container.instance(TraceProviderConfiguration);\n traceConfiguration.configureRuntime('main');\n if (traceMinLevel) {\n traceConfiguration.configureMinLevel(traceMinLevel);\n }\n }\n\n const router = new WoodRouter({\n container,\n controllersDir,\n middlewareDir,\n mode,\n });\n\n registerRuntime(router);\n logger.info('runtime registration complete');\n\n router.validateControllers();\n logger.info('runtime controller validation complete');\n\n router.registerReservedHandler('__load', async (data, event) => {\n if (!options.onLoad) {\n throw new Error('Reserved channel \"__load\" not configured. Provide runtime({ onLoad }).');\n }\n return options.onLoad(data, event);\n });\n\n router.registerReservedHandler('__navigate', async (data, event) => {\n if (!options.onNavigate) {\n throw new Error('Reserved channel \"__navigate\" not configured. Provide runtime({ onNavigate }).');\n }\n return options.onNavigate(data, event);\n });\n\n router.registerReservedHandler('__window', async (data, event) => {\n if (!options.onWindow) {\n throw new Error('Reserved channel \"__window\" not configured. Provide runtime({ onWindow }).');\n }\n return options.onWindow(data, event);\n });\n\n router.registerReservedHandler('__debug', async (data, event) => {\n if (!options.onDebug) {\n throw new Error('Reserved channel \"__debug\" not configured. Provide runtime({ onDebug }).');\n }\n return options.onDebug(data, event);\n });\n\n router.registerReservedHandler('__contextMenu', async (data, event) => {\n if (!options.onContextMenu) {\n throw new Error('Reserved channel \"__contextMenu\" not configured. Provide runtime({ onContextMenu }).');\n }\n return options.onContextMenu(data, event);\n });\n\n router.registerReservedHandler('__woodConfig', async () => {\n return {\n rendererTraceEnabled,\n rendererTraceMinLevel,\n };\n });\n\n if (enableRendererLogBridge) {\n registerRendererLogBridge(ipcMain);\n }\n\n ipcMain.removeAllListeners?.('__trace');\n\n if (enableRendererTraceBridge && rendererTraceEnabled) {\n registerRendererTraceBridge(ipcMain, options.resolveRendererTraceContext);\n }\n\n router.registerIpcHandlers(ipcMain);\n logger.info('runtime ipc handlers registered', {\n channelCount: router.getChannels().length,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n });\n\n return { router };\n}\n\nfunction registerRendererTraceBridge(\n ipcMain: IpcMainLike,\n resolveContext?: (event: unknown) => TraceRuntimeContext | undefined,\n): void {\n ipcMain.removeAllListeners?.('__trace');\n\n ipcMain.on('__trace', (event, rawPayload) => {\n dispatchRendererTracePayload(rawPayload, resolveContext?.(event));\n });\n\n logger.info('renderer trace bridge registered (fire-and-forget)');\n}\n\nfunction registerRendererLogBridge(ipcMain: IpcMainLike): void {\n const dispatcher = getRendererLogDispatcher();\n\n ipcMain.removeAllListeners?.('__log');\n\n ipcMain.on('__log', (_event, rawPayload) => {\n const entry = coerceRendererLogPayload(rawPayload, null);\n void dispatcher.dispatch(entry).then((result) => {\n const typedResult = result as RendererLogDispatchResult;\n if (!typedResult.ok) {\n logger.warn('renderer log dispatch failed', {\n logger: entry.logger,\n messageLength: entry.message.length,\n reason: typedResult.reason ?? null,\n errorMessage: typedResult.errorMessage ?? null,\n });\n }\n });\n });\n\n logger.info('renderer log bridge registered (fire-and-forget)');\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/runtime/runtime.ts"],"sourcesContent":["import { getLogger } from '@noego/logger';\nimport { WoodRouter } from '../router/wood_router.js';\nimport { getContainer } from './get_container.js';\nimport { TraceProviderConfiguration, dispatchRendererTracePayload } from '../tracing/index.js';\nimport {\n setMainTraceMinLevel,\n setRendererTraceMinLevel,\n type TraceLevel,\n type TraceRuntimeContext,\n} from '../tracing/index.js';\nimport {\n getRendererLogDispatcher,\n type RendererLogDispatchResult,\n type RendererLogEntry,\n type RendererLogLevel,\n} from './renderer_log_dispatcher.js';\n\nconst logger = getLogger('wood:runtime');\n\ntype IpcMainLike = {\n handle: (channel: string, handler: (_event: unknown, data: unknown) => Promise<unknown> | unknown) => void;\n on: (channel: string, handler: (_event: unknown, ...args: unknown[]) => void) => void;\n removeHandler?: (channel: string) => void;\n removeAllListeners?: (channel: string) => void;\n};\n\nexport interface RegisterRuntimeRouter {\n registerController(name: string, cls: any): void;\n loadOperations(operations: Map<string, any>): void;\n}\n\nexport interface RuntimeOptions {\n ipcMain: IpcMainLike;\n registerRuntime: (router: RegisterRuntimeRouter) => void;\n container?: any;\n controllersDir: string;\n middlewareDir: string;\n mode?: 'bundled' | 'dev';\n enableRendererLogBridge?: boolean;\n enableRendererTraceBridge?: boolean;\n traceMinLevel?: TraceLevel;\n rendererTraceEnabled?: boolean;\n rendererTraceMinLevel?: TraceLevel;\n onLoad?: (data: unknown, event?: unknown) => Promise<unknown>;\n onNavigate?: (data: unknown, event?: unknown) => Promise<unknown>;\n onWindow?: (data: unknown, event?: unknown) => Promise<unknown>;\n onDebug?: (data: unknown, event?: unknown) => Promise<unknown>;\n onContextMenu?: (data: unknown, event?: unknown) => Promise<unknown>;\n resolveRendererTraceContext?: (event: unknown) => TraceRuntimeContext | undefined;\n}\n\nexport interface RuntimeResult {\n router: WoodRouter;\n}\n\ntype RendererLogPayload = {\n level: string;\n logger: string;\n message: string;\n context?: unknown;\n};\n\ntype IpcInvokeEventLike = {\n sender?: {\n id?: number;\n };\n};\n\nfunction normalizeRendererLogLevel(level: unknown): RendererLogLevel {\n if (level === 'debug' || level === 'warn' || level === 'error') {\n return level;\n }\n return 'info';\n}\n\nfunction coerceRendererLogPayload(\n rawPayload: unknown,\n event: unknown,\n): RendererLogEntry {\n const payload = (rawPayload ?? {}) as Partial<RendererLogPayload>;\n const ipcEvent = event as IpcInvokeEventLike;\n\n return {\n level: normalizeRendererLogLevel(payload.level),\n logger: typeof payload.logger === 'string' && payload.logger.trim().length > 0\n ? payload.logger\n : 'renderer',\n message: typeof payload.message === 'string' ? payload.message : '',\n context: payload.context,\n timestamp: new Date().toISOString(),\n senderId: typeof ipcEvent?.sender?.id === 'number' ? ipcEvent.sender.id : null,\n };\n}\n\n/**\n * Boot a bundled-safe Wood runtime using pre-generated static runtime artifacts.\n */\nexport async function runtime(options: RuntimeOptions): Promise<RuntimeResult> {\n const {\n ipcMain,\n registerRuntime,\n controllersDir,\n middlewareDir,\n mode = 'bundled',\n enableRendererLogBridge = true,\n enableRendererTraceBridge = true,\n traceMinLevel,\n rendererTraceEnabled = false,\n rendererTraceMinLevel = 'debug',\n } = options;\n\n const container = options.container ?? getContainer();\n\n logger.info('runtime start', {\n mode,\n hasProvidedContainer: !!options.container,\n controllersDir,\n middlewareDir,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n traceMinLevel: traceMinLevel ?? 'debug',\n rendererTraceEnabled,\n rendererTraceMinLevel,\n });\n\n if (traceMinLevel) {\n setMainTraceMinLevel(traceMinLevel);\n }\n setRendererTraceMinLevel(rendererTraceMinLevel);\n\n if (typeof container.instance === 'function') {\n const traceConfiguration = await container.instance(TraceProviderConfiguration);\n traceConfiguration.configureRuntime('main');\n if (traceMinLevel) {\n traceConfiguration.configureMinLevel(traceMinLevel);\n }\n }\n\n const router = new WoodRouter({\n container,\n controllersDir,\n middlewareDir,\n mode,\n });\n\n registerRuntime(router);\n logger.info('runtime registration complete');\n\n router.validateControllers();\n logger.info('runtime controller validation complete');\n\n router.precompileSchemas();\n logger.info('runtime schema pre-compilation complete');\n\n router.registerReservedHandler('__load', async (data, event) => {\n if (!options.onLoad) {\n throw new Error('Reserved channel \"__load\" not configured. Provide runtime({ onLoad }).');\n }\n return options.onLoad(data, event);\n });\n\n router.registerReservedHandler('__navigate', async (data, event) => {\n if (!options.onNavigate) {\n throw new Error('Reserved channel \"__navigate\" not configured. Provide runtime({ onNavigate }).');\n }\n return options.onNavigate(data, event);\n });\n\n router.registerReservedHandler('__window', async (data, event) => {\n if (!options.onWindow) {\n throw new Error('Reserved channel \"__window\" not configured. Provide runtime({ onWindow }).');\n }\n return options.onWindow(data, event);\n });\n\n router.registerReservedHandler('__debug', async (data, event) => {\n if (!options.onDebug) {\n throw new Error('Reserved channel \"__debug\" not configured. Provide runtime({ onDebug }).');\n }\n return options.onDebug(data, event);\n });\n\n router.registerReservedHandler('__contextMenu', async (data, event) => {\n if (!options.onContextMenu) {\n throw new Error('Reserved channel \"__contextMenu\" not configured. Provide runtime({ onContextMenu }).');\n }\n return options.onContextMenu(data, event);\n });\n\n router.registerReservedHandler('__woodConfig', async () => {\n return {\n rendererTraceEnabled,\n rendererTraceMinLevel,\n };\n });\n\n if (enableRendererLogBridge) {\n registerRendererLogBridge(ipcMain);\n }\n\n ipcMain.removeAllListeners?.('__trace');\n\n if (enableRendererTraceBridge && rendererTraceEnabled) {\n registerRendererTraceBridge(ipcMain, options.resolveRendererTraceContext);\n }\n\n router.registerIpcHandlers(ipcMain);\n logger.info('runtime ipc handlers registered', {\n channelCount: router.getChannels().length,\n enableRendererLogBridge,\n enableRendererTraceBridge,\n });\n\n return { router };\n}\n\nfunction registerRendererTraceBridge(\n ipcMain: IpcMainLike,\n resolveContext?: (event: unknown) => TraceRuntimeContext | undefined,\n): void {\n ipcMain.removeAllListeners?.('__trace');\n\n ipcMain.on('__trace', (event, rawPayload) => {\n dispatchRendererTracePayload(rawPayload, resolveContext?.(event));\n });\n\n logger.info('renderer trace bridge registered (fire-and-forget)');\n}\n\nfunction registerRendererLogBridge(ipcMain: IpcMainLike): void {\n const dispatcher = getRendererLogDispatcher();\n\n ipcMain.removeAllListeners?.('__log');\n\n ipcMain.on('__log', (_event, rawPayload) => {\n const entry = coerceRendererLogPayload(rawPayload, null);\n void dispatcher.dispatch(entry).then((result) => {\n const typedResult = result as RendererLogDispatchResult;\n if (!typedResult.ok) {\n logger.warn('renderer log dispatch failed', {\n logger: entry.logger,\n messageLength: entry.message.length,\n reason: typedResult.reason ?? null,\n errorMessage: typedResult.errorMessage ?? null,\n });\n }\n });\n });\n\n logger.info('renderer log bridge registered (fire-and-forget)');\n}\n"],"mappings":"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","names":[]}
|
|
@@ -57,6 +57,19 @@ class SchemaValidator {
|
|
|
57
57
|
this.cache.set(channel, validator);
|
|
58
58
|
return validator;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Eagerly compile and cache a channel's schema so malformed schemas fail
|
|
62
|
+
* fast at startup rather than throwing an opaque ajv error on the first
|
|
63
|
+
* request to that channel. Re-throws ajv compile errors with channel context.
|
|
64
|
+
*/
|
|
65
|
+
precompile(channel, schema) {
|
|
66
|
+
try {
|
|
67
|
+
this.getValidator(channel, schema);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
throw new Error(`Invalid inputSchema for channel "${channel}": ${message}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
60
73
|
/**
|
|
61
74
|
* Validate data against a schema for a given channel.
|
|
62
75
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/validation/schema_validator.ts"],"sourcesContent":["import Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport type { ValidateFunction } from 'ajv';\nimport { getLogger } from '@noego/logger';\n\nconst logger = getLogger('wood:validation');\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: unknown[];\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n private cache = new Map<string, ValidateFunction>();\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n coerceTypes: false,\n strict: false,\n });\n addFormats(this.ajv);\n }\n\n /**\n * Compile and cache a schema for an operation channel.\n * Lazy compilation — only compiles on first use.\n */\n private getValidator(channel: string, schema: Record<string, unknown>): ValidateFunction {\n const cached = this.cache.get(channel);\n if (cached) return cached;\n\n logger.debug(`Compiling schema for channel: ${channel}`);\n const validator = this.ajv.compile(schema);\n this.cache.set(channel, validator);\n return validator;\n }\n\n /**\n * Validate data against a schema for a given channel.\n */\n validate(channel: string, schema: Record<string, unknown>, data: unknown): ValidationResult {\n const validator = this.getValidator(channel, schema);\n const valid = validator(data);\n\n if (valid) {\n return { valid: true };\n }\n\n logger.debug(`Validation failed for channel \"${channel}\": ${JSON.stringify(validator.errors)}`);\n return {\n valid: false,\n errors: validator.errors ?? [],\n };\n }\n\n /**\n * Clear the cache (useful for testing or hot reload).\n */\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAgB;AAChB,yBAAuB;AAEvB,oBAA0B;AAE1B,MAAM,aAAS,yBAAU,iBAAiB;AAOnC,MAAM,gBAAgB;AAAA,EAI3B,cAAc;AAFd,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,MAAM,IAAI,WAAAA,QAAI;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AACD,2BAAAC,SAAW,KAAK,GAAG;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aAAa,SAAiB,QAAmD;AACvF,UAAM,SAAS,KAAK,MAAM,IAAI,OAAO;AACrC,QAAI,OAAQ,QAAO;AAEnB,WAAO,MAAM,iCAAiC,OAAO,EAAE;AACvD,UAAM,YAAY,KAAK,IAAI,QAAQ,MAAM;AACzC,SAAK,MAAM,IAAI,SAAS,SAAS;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAiB,QAAiC,MAAiC;AAC1F,UAAM,YAAY,KAAK,aAAa,SAAS,MAAM;AACnD,UAAM,QAAQ,UAAU,IAAI;AAE5B,QAAI,OAAO;AACT,aAAO,EAAE,OAAO,KAAK;AAAA,IACvB;AAEA,WAAO,MAAM,kCAAkC,OAAO,MAAM,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE;AAC9F,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,UAAU,UAAU,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":["Ajv","addFormats"]}
|
|
1
|
+
{"version":3,"sources":["../../src/validation/schema_validator.ts"],"sourcesContent":["import Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport type { ValidateFunction } from 'ajv';\nimport { getLogger } from '@noego/logger';\n\nconst logger = getLogger('wood:validation');\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: unknown[];\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n private cache = new Map<string, ValidateFunction>();\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n coerceTypes: false,\n strict: false,\n });\n addFormats(this.ajv);\n }\n\n /**\n * Compile and cache a schema for an operation channel.\n * Lazy compilation — only compiles on first use.\n */\n private getValidator(channel: string, schema: Record<string, unknown>): ValidateFunction {\n const cached = this.cache.get(channel);\n if (cached) return cached;\n\n logger.debug(`Compiling schema for channel: ${channel}`);\n const validator = this.ajv.compile(schema);\n this.cache.set(channel, validator);\n return validator;\n }\n\n /**\n * Eagerly compile and cache a channel's schema so malformed schemas fail\n * fast at startup rather than throwing an opaque ajv error on the first\n * request to that channel. Re-throws ajv compile errors with channel context.\n */\n precompile(channel: string, schema: Record<string, unknown>): void {\n try {\n this.getValidator(channel, schema);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid inputSchema for channel \"${channel}\": ${message}`);\n }\n }\n\n /**\n * Validate data against a schema for a given channel.\n */\n validate(channel: string, schema: Record<string, unknown>, data: unknown): ValidationResult {\n const validator = this.getValidator(channel, schema);\n const valid = validator(data);\n\n if (valid) {\n return { valid: true };\n }\n\n logger.debug(`Validation failed for channel \"${channel}\": ${JSON.stringify(validator.errors)}`);\n return {\n valid: false,\n errors: validator.errors ?? [],\n };\n }\n\n /**\n * Clear the cache (useful for testing or hot reload).\n */\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,iBAAgB;AAChB,yBAAuB;AAEvB,oBAA0B;AAE1B,MAAM,aAAS,yBAAU,iBAAiB;AAOnC,MAAM,gBAAgB;AAAA,EAI3B,cAAc;AAFd,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,MAAM,IAAI,WAAAA,QAAI;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AACD,2BAAAC,SAAW,KAAK,GAAG;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aAAa,SAAiB,QAAmD;AACvF,UAAM,SAAS,KAAK,MAAM,IAAI,OAAO;AACrC,QAAI,OAAQ,QAAO;AAEnB,WAAO,MAAM,iCAAiC,OAAO,EAAE;AACvD,UAAM,YAAY,KAAK,IAAI,QAAQ,MAAM;AACzC,SAAK,MAAM,IAAI,SAAS,SAAS;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,SAAiB,QAAuC;AACjE,QAAI;AACF,WAAK,aAAa,SAAS,MAAM;AAAA,IACnC,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,YAAM,IAAI,MAAM,oCAAoC,OAAO,MAAM,OAAO,EAAE;AAAA,IAC5E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAiB,QAAiC,MAAiC;AAC1F,UAAM,YAAY,KAAK,aAAa,SAAS,MAAM;AACnD,UAAM,QAAQ,UAAU,IAAI;AAE5B,QAAI,OAAO;AACT,aAAO,EAAE,OAAO,KAAK;AAAA,IACvB;AAEA,WAAO,MAAM,kCAAkC,OAAO,MAAM,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE;AAC9F,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,UAAU,UAAU,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":["Ajv","addFormats"]}
|
|
@@ -11,6 +11,12 @@ declare class SchemaValidator {
|
|
|
11
11
|
* Lazy compilation — only compiles on first use.
|
|
12
12
|
*/
|
|
13
13
|
private getValidator;
|
|
14
|
+
/**
|
|
15
|
+
* Eagerly compile and cache a channel's schema so malformed schemas fail
|
|
16
|
+
* fast at startup rather than throwing an opaque ajv error on the first
|
|
17
|
+
* request to that channel. Re-throws ajv compile errors with channel context.
|
|
18
|
+
*/
|
|
19
|
+
precompile(channel: string, schema: Record<string, unknown>): void;
|
|
14
20
|
/**
|
|
15
21
|
* Validate data against a schema for a given channel.
|
|
16
22
|
*/
|
|
@@ -11,6 +11,12 @@ declare class SchemaValidator {
|
|
|
11
11
|
* Lazy compilation — only compiles on first use.
|
|
12
12
|
*/
|
|
13
13
|
private getValidator;
|
|
14
|
+
/**
|
|
15
|
+
* Eagerly compile and cache a channel's schema so malformed schemas fail
|
|
16
|
+
* fast at startup rather than throwing an opaque ajv error on the first
|
|
17
|
+
* request to that channel. Re-throws ajv compile errors with channel context.
|
|
18
|
+
*/
|
|
19
|
+
precompile(channel: string, schema: Record<string, unknown>): void;
|
|
14
20
|
/**
|
|
15
21
|
* Validate data against a schema for a given channel.
|
|
16
22
|
*/
|
|
@@ -24,6 +24,19 @@ class SchemaValidator {
|
|
|
24
24
|
this.cache.set(channel, validator);
|
|
25
25
|
return validator;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Eagerly compile and cache a channel's schema so malformed schemas fail
|
|
29
|
+
* fast at startup rather than throwing an opaque ajv error on the first
|
|
30
|
+
* request to that channel. Re-throws ajv compile errors with channel context.
|
|
31
|
+
*/
|
|
32
|
+
precompile(channel, schema) {
|
|
33
|
+
try {
|
|
34
|
+
this.getValidator(channel, schema);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
throw new Error(`Invalid inputSchema for channel "${channel}": ${message}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
27
40
|
/**
|
|
28
41
|
* Validate data against a schema for a given channel.
|
|
29
42
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/validation/schema_validator.ts"],"sourcesContent":["import Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport type { ValidateFunction } from 'ajv';\nimport { getLogger } from '@noego/logger';\n\nconst logger = getLogger('wood:validation');\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: unknown[];\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n private cache = new Map<string, ValidateFunction>();\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n coerceTypes: false,\n strict: false,\n });\n addFormats(this.ajv);\n }\n\n /**\n * Compile and cache a schema for an operation channel.\n * Lazy compilation — only compiles on first use.\n */\n private getValidator(channel: string, schema: Record<string, unknown>): ValidateFunction {\n const cached = this.cache.get(channel);\n if (cached) return cached;\n\n logger.debug(`Compiling schema for channel: ${channel}`);\n const validator = this.ajv.compile(schema);\n this.cache.set(channel, validator);\n return validator;\n }\n\n /**\n * Validate data against a schema for a given channel.\n */\n validate(channel: string, schema: Record<string, unknown>, data: unknown): ValidationResult {\n const validator = this.getValidator(channel, schema);\n const valid = validator(data);\n\n if (valid) {\n return { valid: true };\n }\n\n logger.debug(`Validation failed for channel \"${channel}\": ${JSON.stringify(validator.errors)}`);\n return {\n valid: false,\n errors: validator.errors ?? [],\n };\n }\n\n /**\n * Clear the cache (useful for testing or hot reload).\n */\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":"AAAA,OAAO,SAAS;AAChB,OAAO,gBAAgB;AAEvB,SAAS,iBAAiB;AAE1B,MAAM,SAAS,UAAU,iBAAiB;AAOnC,MAAM,gBAAgB;AAAA,EAI3B,cAAc;AAFd,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,MAAM,IAAI,IAAI;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AACD,eAAW,KAAK,GAAG;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aAAa,SAAiB,QAAmD;AACvF,UAAM,SAAS,KAAK,MAAM,IAAI,OAAO;AACrC,QAAI,OAAQ,QAAO;AAEnB,WAAO,MAAM,iCAAiC,OAAO,EAAE;AACvD,UAAM,YAAY,KAAK,IAAI,QAAQ,MAAM;AACzC,SAAK,MAAM,IAAI,SAAS,SAAS;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAiB,QAAiC,MAAiC;AAC1F,UAAM,YAAY,KAAK,aAAa,SAAS,MAAM;AACnD,UAAM,QAAQ,UAAU,IAAI;AAE5B,QAAI,OAAO;AACT,aAAO,EAAE,OAAO,KAAK;AAAA,IACvB;AAEA,WAAO,MAAM,kCAAkC,OAAO,MAAM,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE;AAC9F,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,UAAU,UAAU,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/validation/schema_validator.ts"],"sourcesContent":["import Ajv from 'ajv';\nimport addFormats from 'ajv-formats';\nimport type { ValidateFunction } from 'ajv';\nimport { getLogger } from '@noego/logger';\n\nconst logger = getLogger('wood:validation');\n\nexport interface ValidationResult {\n valid: boolean;\n errors?: unknown[];\n}\n\nexport class SchemaValidator {\n private ajv: Ajv;\n private cache = new Map<string, ValidateFunction>();\n\n constructor() {\n this.ajv = new Ajv({\n allErrors: true,\n coerceTypes: false,\n strict: false,\n });\n addFormats(this.ajv);\n }\n\n /**\n * Compile and cache a schema for an operation channel.\n * Lazy compilation — only compiles on first use.\n */\n private getValidator(channel: string, schema: Record<string, unknown>): ValidateFunction {\n const cached = this.cache.get(channel);\n if (cached) return cached;\n\n logger.debug(`Compiling schema for channel: ${channel}`);\n const validator = this.ajv.compile(schema);\n this.cache.set(channel, validator);\n return validator;\n }\n\n /**\n * Eagerly compile and cache a channel's schema so malformed schemas fail\n * fast at startup rather than throwing an opaque ajv error on the first\n * request to that channel. Re-throws ajv compile errors with channel context.\n */\n precompile(channel: string, schema: Record<string, unknown>): void {\n try {\n this.getValidator(channel, schema);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`Invalid inputSchema for channel \"${channel}\": ${message}`);\n }\n }\n\n /**\n * Validate data against a schema for a given channel.\n */\n validate(channel: string, schema: Record<string, unknown>, data: unknown): ValidationResult {\n const validator = this.getValidator(channel, schema);\n const valid = validator(data);\n\n if (valid) {\n return { valid: true };\n }\n\n logger.debug(`Validation failed for channel \"${channel}\": ${JSON.stringify(validator.errors)}`);\n return {\n valid: false,\n errors: validator.errors ?? [],\n };\n }\n\n /**\n * Clear the cache (useful for testing or hot reload).\n */\n clearCache(): void {\n this.cache.clear();\n }\n}\n"],"mappings":"AAAA,OAAO,SAAS;AAChB,OAAO,gBAAgB;AAEvB,SAAS,iBAAiB;AAE1B,MAAM,SAAS,UAAU,iBAAiB;AAOnC,MAAM,gBAAgB;AAAA,EAI3B,cAAc;AAFd,SAAQ,QAAQ,oBAAI,IAA8B;AAGhD,SAAK,MAAM,IAAI,IAAI;AAAA,MACjB,WAAW;AAAA,MACX,aAAa;AAAA,MACb,QAAQ;AAAA,IACV,CAAC;AACD,eAAW,KAAK,GAAG;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,aAAa,SAAiB,QAAmD;AACvF,UAAM,SAAS,KAAK,MAAM,IAAI,OAAO;AACrC,QAAI,OAAQ,QAAO;AAEnB,WAAO,MAAM,iCAAiC,OAAO,EAAE;AACvD,UAAM,YAAY,KAAK,IAAI,QAAQ,MAAM;AACzC,SAAK,MAAM,IAAI,SAAS,SAAS;AACjC,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,WAAW,SAAiB,QAAuC;AACjE,QAAI;AACF,WAAK,aAAa,SAAS,MAAM;AAAA,IACnC,SAAS,OAAO;AACd,YAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AACrE,YAAM,IAAI,MAAM,oCAAoC,OAAO,MAAM,OAAO,EAAE;AAAA,IAC5E;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,SAAS,SAAiB,QAAiC,MAAiC;AAC1F,UAAM,YAAY,KAAK,aAAa,SAAS,MAAM;AACnD,UAAM,QAAQ,UAAU,IAAI;AAE5B,QAAI,OAAO;AACT,aAAO,EAAE,OAAO,KAAK;AAAA,IACvB;AAEA,WAAO,MAAM,kCAAkC,OAAO,MAAM,KAAK,UAAU,UAAU,MAAM,CAAC,EAAE;AAC9F,WAAO;AAAA,MACL,OAAO;AAAA,MACP,QAAQ,UAAU,UAAU,CAAC;AAAA,IAC/B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAmB;AACjB,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;","names":[]}
|
package/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, test, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { compile } from 'svelte/compiler';
|
|
5
|
+
|
|
6
|
+
// Regression guard: ContextMenuProvider must compile under Svelte 5. It uses
|
|
7
|
+
// runes ($props/$state), so the legacy `onclick|modifier` event syntax is a
|
|
8
|
+
// hard compile error ("'onclick|stopPropagation' is not a valid attribute name").
|
|
9
|
+
describe('ContextMenuProvider.svelte', () => {
|
|
10
|
+
test('compiles cleanly under Svelte 5 (no legacy event modifiers)', () => {
|
|
11
|
+
const source = readFileSync(
|
|
12
|
+
path.join(__dirname, 'ContextMenuProvider.svelte'),
|
|
13
|
+
'utf-8',
|
|
14
|
+
);
|
|
15
|
+
expect(() =>
|
|
16
|
+
compile(source, { name: 'ContextMenuProvider', generate: 'client' }),
|
|
17
|
+
).not.toThrow();
|
|
18
|
+
expect(source).not.toMatch(/on\w+\|/); // no `onclick|modifier` style attributes
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -90,13 +90,13 @@
|
|
|
90
90
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
91
91
|
<div
|
|
92
92
|
class="wood-context-menu-overlay"
|
|
93
|
-
onclick
|
|
93
|
+
onclick={(e) => { e.stopPropagation(); close(); }}
|
|
94
94
|
onkeydown={() => {}}
|
|
95
95
|
style="position:fixed;inset:0;z-index:999998;"
|
|
96
96
|
>
|
|
97
97
|
<div
|
|
98
98
|
style="position:fixed;left:{position.x}px;top:{position.y}px;z-index:999999;"
|
|
99
|
-
onclick
|
|
99
|
+
onclick={(e) => e.stopPropagation()}
|
|
100
100
|
onkeydown={() => {}}
|
|
101
101
|
>
|
|
102
102
|
{#if menu}
|