@cordy/electro-generator 1.0.8 → 1.1.0
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/index.d.mts +11 -4
- package/dist/index.mjs +108 -59
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ViewDefinition } from "@cordy/electro";
|
|
2
2
|
|
|
3
3
|
//#region src/types.d.ts
|
|
4
4
|
/**
|
|
@@ -27,6 +27,12 @@ interface ScannedEvent {
|
|
|
27
27
|
filePath: string;
|
|
28
28
|
exported: boolean;
|
|
29
29
|
}
|
|
30
|
+
interface ScannedWindow {
|
|
31
|
+
id: string;
|
|
32
|
+
varName: string;
|
|
33
|
+
filePath: string;
|
|
34
|
+
exported: boolean;
|
|
35
|
+
}
|
|
30
36
|
interface ScannedFeature {
|
|
31
37
|
id: string;
|
|
32
38
|
filePath: string;
|
|
@@ -38,6 +44,7 @@ interface ScannedFeature {
|
|
|
38
44
|
}
|
|
39
45
|
interface ScanResult {
|
|
40
46
|
features: ScannedFeature[];
|
|
47
|
+
windows: ScannedWindow[];
|
|
41
48
|
}
|
|
42
49
|
interface GeneratedFile {
|
|
43
50
|
path: string;
|
|
@@ -47,7 +54,7 @@ interface GeneratedFile {
|
|
|
47
54
|
//#region src/generator.d.ts
|
|
48
55
|
interface GeneratorInput {
|
|
49
56
|
scanResult: ScanResult;
|
|
50
|
-
|
|
57
|
+
views: readonly ViewDefinition[];
|
|
51
58
|
/** Root output directory (e.g. `.electro/`). Generated files go into `generated/` subdirectory. */
|
|
52
59
|
outputDir: string;
|
|
53
60
|
/** Source directory where `electro-env.d.ts` will be written (e.g. `src/`). */
|
|
@@ -60,7 +67,7 @@ interface GeneratorOutput {
|
|
|
60
67
|
envTypes: GeneratedFile;
|
|
61
68
|
}
|
|
62
69
|
/**
|
|
63
|
-
* Generate all output files from scan results and
|
|
70
|
+
* Generate all output files from scan results and view definitions.
|
|
64
71
|
*/
|
|
65
72
|
declare function generate(input: GeneratorInput): GeneratorOutput;
|
|
66
73
|
//#endregion
|
|
@@ -73,4 +80,4 @@ declare function generate(input: GeneratorInput): GeneratorOutput;
|
|
|
73
80
|
*/
|
|
74
81
|
declare function scan(basePath: string): Promise<ScanResult>;
|
|
75
82
|
//#endregion
|
|
76
|
-
export { type GeneratedFile, type GeneratorInput, type GeneratorOutput, type ScanResult, type ScannedFeature, type ScannedService, type ScannedTask, generate, scan };
|
|
83
|
+
export { type GeneratedFile, type GeneratorInput, type GeneratorOutput, type ScanResult, type ScannedFeature, type ScannedService, type ScannedTask, type ScannedWindow, generate, scan };
|
package/dist/index.mjs
CHANGED
|
@@ -7,8 +7,8 @@ import { Visitor, parseSync } from "oxc-parser";
|
|
|
7
7
|
/**
|
|
8
8
|
* Code Generator — produces preload scripts, bridge types, and context types.
|
|
9
9
|
*
|
|
10
|
-
* Takes a ScanResult +
|
|
11
|
-
* Uses PolicyEngine for deny-by-default per-
|
|
10
|
+
* Takes a ScanResult + view definitions and emits generated files.
|
|
11
|
+
* Uses PolicyEngine for deny-by-default per-view filtering.
|
|
12
12
|
*/
|
|
13
13
|
const HEADER = "// Auto-generated by Electro codegen. Do not edit.\n";
|
|
14
14
|
/** Quote a property key if it's not a valid JS identifier. */
|
|
@@ -23,13 +23,13 @@ function methodStub(featureId, serviceId, method) {
|
|
|
23
23
|
return `(...args: unknown[]) => ipcRenderer.invoke("${`${featureId}:${serviceId}:${method}`}", ...args)`;
|
|
24
24
|
}
|
|
25
25
|
/**
|
|
26
|
-
* Generate a preload script for a specific
|
|
26
|
+
* Generate a preload script for a specific view.
|
|
27
27
|
*
|
|
28
28
|
* Structure: `window.electro.{featureId}.{serviceId}.{method}()`
|
|
29
29
|
* Only EXPOSED scope services are included.
|
|
30
30
|
*/
|
|
31
|
-
function generatePreload(
|
|
32
|
-
const allowedFeatures = features.filter((f) => policy.canAccess(
|
|
31
|
+
function generatePreload(viewName, features, policy, preloadExtension) {
|
|
32
|
+
const allowedFeatures = features.filter((f) => policy.canAccess(viewName, f.id));
|
|
33
33
|
const featureEntries = [];
|
|
34
34
|
for (const feature of allowedFeatures) {
|
|
35
35
|
const exposedServices = feature.services.filter((s) => s.scope === "exposed");
|
|
@@ -55,16 +55,16 @@ contextBridge.exposeInMainWorld("electro", ${featureEntries.length > 0 ? `{\n${f
|
|
|
55
55
|
`;
|
|
56
56
|
if (preloadExtension) content += `\n// User extension\nimport "${preloadExtension}";\n`;
|
|
57
57
|
return {
|
|
58
|
-
path: `generated/preload/${
|
|
58
|
+
path: `generated/preload/${viewName}.gen.ts`,
|
|
59
59
|
content
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
/**
|
|
63
|
-
* Generate bridge type declarations for a specific
|
|
63
|
+
* Generate bridge type declarations for a specific view.
|
|
64
64
|
* Makes `window.electro` type-safe in the renderer.
|
|
65
65
|
*/
|
|
66
|
-
function generateBridgeTypes(
|
|
67
|
-
const allowedFeatures = features.filter((f) => policy.canAccess(
|
|
66
|
+
function generateBridgeTypes(viewName, features, policy) {
|
|
67
|
+
const allowedFeatures = features.filter((f) => policy.canAccess(viewName, f.id));
|
|
68
68
|
const featureTypes = [];
|
|
69
69
|
for (const feature of allowedFeatures) {
|
|
70
70
|
const exposedServices = feature.services.filter((s) => s.scope === "exposed");
|
|
@@ -84,7 +84,7 @@ function generateBridgeTypes(windowName, features, policy) {
|
|
|
84
84
|
featureTypes.push(` ${q(feature.id)}: {\n${serviceTypes.join("\n")}\n };`);
|
|
85
85
|
}
|
|
86
86
|
const content = `${HEADER}
|
|
87
|
-
export interface ElectroBridge ${featureTypes.length > 0 ? `{\n${featureTypes.join("\n")}\n }` : "
|
|
87
|
+
export interface ElectroBridge ${featureTypes.length > 0 ? `{\n${featureTypes.join("\n")}\n }` : "{}"}
|
|
88
88
|
|
|
89
89
|
declare global {
|
|
90
90
|
interface Window {
|
|
@@ -93,7 +93,7 @@ declare global {
|
|
|
93
93
|
}
|
|
94
94
|
`;
|
|
95
95
|
return {
|
|
96
|
-
path: `generated/
|
|
96
|
+
path: `generated/views/${viewName}.bridge.d.ts`,
|
|
97
97
|
content
|
|
98
98
|
};
|
|
99
99
|
}
|
|
@@ -111,22 +111,32 @@ export {};
|
|
|
111
111
|
type _SvcApi<T> = T extends { api(): infer R } ? NonNullable<R> : never;
|
|
112
112
|
type _TaskPayload<T> = T extends { start(payload?: infer P): any } ? P : void;
|
|
113
113
|
type _EventPayload<T> = T extends { payload(): infer P } ? P : unknown;
|
|
114
|
+
type _WinApi<T> = T extends import("@cordy/electro").CreatedWindow<infer A> ? A : void;
|
|
114
115
|
`;
|
|
115
116
|
/**
|
|
116
|
-
* Generate
|
|
117
|
-
*
|
|
118
|
-
* services, tasks, and dependencies. Uses typeof import() for full type inference.
|
|
117
|
+
* Generate ViewMap entries for all defined views.
|
|
118
|
+
* All views use WebContentsView as their type.
|
|
119
119
|
*/
|
|
120
|
-
function
|
|
120
|
+
function generateViewTypes(views) {
|
|
121
|
+
if (views.length === 0) return "";
|
|
122
|
+
const entries = [];
|
|
123
|
+
for (const view of views) entries.push(` "${view.name}": import("electron").WebContentsView;`);
|
|
124
|
+
return `\n interface ViewMap {\n${entries.join("\n")}\n }\n`;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Generate WindowApiMap entries for all scanned createWindow() calls.
|
|
128
|
+
* Uses _WinApi utility type to extract the typed API from CreatedWindow<TApi>.
|
|
129
|
+
*/
|
|
130
|
+
function generateWindowApiTypes(windows, srcDir) {
|
|
121
131
|
if (windows.length === 0) return "";
|
|
122
132
|
const entries = [];
|
|
123
|
-
for (const win of windows) {
|
|
124
|
-
const
|
|
125
|
-
entries.push(` "${win.
|
|
126
|
-
}
|
|
127
|
-
return `\n interface
|
|
133
|
+
for (const win of windows) if (win.exported) {
|
|
134
|
+
const importPath = toImportPath(srcDir, win.filePath);
|
|
135
|
+
entries.push(` "${win.id}": _WinApi<typeof import("${importPath}").${win.varName}>;`);
|
|
136
|
+
} else entries.push(` "${win.id}": unknown;`);
|
|
137
|
+
return `\n interface WindowApiMap {\n${entries.join("\n")}\n }\n`;
|
|
128
138
|
}
|
|
129
|
-
function generateFeatureTypes(features, srcDir, windows) {
|
|
139
|
+
function generateFeatureTypes(features, srcDir, views, windows) {
|
|
130
140
|
const seenFeatures = /* @__PURE__ */ new Set();
|
|
131
141
|
const featureBlocks = [];
|
|
132
142
|
const svcOwnerEntries = [];
|
|
@@ -199,26 +209,26 @@ declare module "@cordy/electro" {
|
|
|
199
209
|
interface ServiceOwnerMap ${svcOwnerEntries.length > 0 ? `{\n${svcOwnerEntries.join("\n")}\n }` : "{}"}
|
|
200
210
|
|
|
201
211
|
interface TaskOwnerMap ${taskOwnerEntries.length > 0 ? `{\n${taskOwnerEntries.join("\n")}\n }` : "{}"}
|
|
202
|
-
${
|
|
212
|
+
${generateViewTypes(views)}${generateWindowApiTypes(windows, srcDir)}}
|
|
203
213
|
`
|
|
204
214
|
};
|
|
205
215
|
}
|
|
206
216
|
/**
|
|
207
|
-
* Generate all output files from scan results and
|
|
217
|
+
* Generate all output files from scan results and view definitions.
|
|
208
218
|
*/
|
|
209
219
|
function generate(input) {
|
|
210
|
-
const { scanResult,
|
|
211
|
-
const policy = new PolicyEngine(
|
|
220
|
+
const { scanResult, views, srcDir } = input;
|
|
221
|
+
const policy = new PolicyEngine(views);
|
|
212
222
|
const files = [];
|
|
213
|
-
for (const
|
|
223
|
+
for (const view of views) {
|
|
214
224
|
const knownIds = new Set(scanResult.features.map((f) => f.id));
|
|
215
|
-
for (const fId of
|
|
216
|
-
files.push(generatePreload(
|
|
217
|
-
files.push(generateBridgeTypes(
|
|
225
|
+
for (const fId of view.features ?? []) if (!knownIds.has(fId)) console.warn(`[generator] View "${view.name}" references unknown feature "${fId}"`);
|
|
226
|
+
files.push(generatePreload(view.name, scanResult.features, policy, view.preload));
|
|
227
|
+
files.push(generateBridgeTypes(view.name, scanResult.features, policy));
|
|
218
228
|
}
|
|
219
229
|
return {
|
|
220
230
|
files,
|
|
221
|
-
envTypes: generateFeatureTypes(scanResult.features, srcDir, windows)
|
|
231
|
+
envTypes: generateFeatureTypes(scanResult.features, srcDir, views, scanResult.windows ?? [])
|
|
222
232
|
};
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -484,6 +494,39 @@ function extractEvents(program, filePath, exportedNames) {
|
|
|
484
494
|
} }).visit(program);
|
|
485
495
|
return events;
|
|
486
496
|
}
|
|
497
|
+
/**
|
|
498
|
+
* Extract createWindow() calls from a file.
|
|
499
|
+
* Uses VariableDeclarator to capture `const x = createWindow({...})` patterns.
|
|
500
|
+
*/
|
|
501
|
+
function extractWindows(program, filePath, exportedNames) {
|
|
502
|
+
const windows = [];
|
|
503
|
+
new Visitor({ VariableDeclarator(node) {
|
|
504
|
+
const init = node.init;
|
|
505
|
+
if (!init || init.type !== "CallExpression") return;
|
|
506
|
+
const callee = init.callee;
|
|
507
|
+
if (callee.type !== "Identifier" || callee.name !== "createWindow") return;
|
|
508
|
+
const args = init.arguments;
|
|
509
|
+
if (args.length < 1 || args[0].type !== "ObjectExpression") return;
|
|
510
|
+
const config = args[0];
|
|
511
|
+
const id = getStringLiteral(getObjectProperty(config, "id"));
|
|
512
|
+
if (!id) {
|
|
513
|
+
console.warn(`[scanner] Skipping createWindow() with non-literal id in ${filePath}`);
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const idNode = node.id;
|
|
517
|
+
const varName = idNode.type === "Identifier" && typeof idNode.name === "string" ? idNode.name : id;
|
|
518
|
+
windows.push({
|
|
519
|
+
varName,
|
|
520
|
+
window: {
|
|
521
|
+
id,
|
|
522
|
+
varName,
|
|
523
|
+
filePath,
|
|
524
|
+
exported: exportedNames.has(varName)
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
} }).visit(program);
|
|
528
|
+
return windows;
|
|
529
|
+
}
|
|
487
530
|
/** Extract createFeature() calls from a file. */
|
|
488
531
|
function extractFeatures(program, filePath) {
|
|
489
532
|
const features = [];
|
|
@@ -549,6 +592,7 @@ async function scan(basePath) {
|
|
|
549
592
|
const allServices = [];
|
|
550
593
|
const allTasks = [];
|
|
551
594
|
const allEvents = [];
|
|
595
|
+
const allWindows = [];
|
|
552
596
|
const allFeatures = [];
|
|
553
597
|
for (const filePath of files) {
|
|
554
598
|
const result = parseSync(filePath, readFileSync(filePath, "utf-8"), { sourceType: "module" });
|
|
@@ -558,10 +602,12 @@ async function scan(basePath) {
|
|
|
558
602
|
const services = extractServices(program, filePath, exportedNames);
|
|
559
603
|
const tasks = extractTasks(program, filePath, exportedNames);
|
|
560
604
|
const events = extractEvents(program, filePath, exportedNames);
|
|
605
|
+
const windows = extractWindows(program, filePath, exportedNames);
|
|
561
606
|
const features = extractFeatures(program, filePath);
|
|
562
607
|
allServices.push(...services);
|
|
563
608
|
allTasks.push(...tasks);
|
|
564
609
|
allEvents.push(...events);
|
|
610
|
+
allWindows.push(...windows);
|
|
565
611
|
allFeatures.push(...features);
|
|
566
612
|
}
|
|
567
613
|
const serviceByVarName = /* @__PURE__ */ new Map();
|
|
@@ -570,35 +616,38 @@ async function scan(basePath) {
|
|
|
570
616
|
for (const { varName, task } of allTasks) taskByVarName.set(varName, task);
|
|
571
617
|
const eventByVarName = /* @__PURE__ */ new Map();
|
|
572
618
|
for (const { varName, event } of allEvents) eventByVarName.set(varName, event);
|
|
573
|
-
return {
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
619
|
+
return {
|
|
620
|
+
features: allFeatures.map((raw) => {
|
|
621
|
+
const services = [];
|
|
622
|
+
for (const varName of raw.serviceVarNames) {
|
|
623
|
+
const svc = serviceByVarName.get(varName);
|
|
624
|
+
if (svc) services.push(svc);
|
|
625
|
+
else console.warn(`[scanner] Feature "${raw.id}" references unknown service variable "${varName}" in ${raw.filePath}`);
|
|
626
|
+
}
|
|
627
|
+
const tasks = [];
|
|
628
|
+
for (const varName of raw.taskVarNames) {
|
|
629
|
+
const task = taskByVarName.get(varName);
|
|
630
|
+
if (task) tasks.push(task);
|
|
631
|
+
else console.warn(`[scanner] Feature "${raw.id}" references unknown task variable "${varName}" in ${raw.filePath}`);
|
|
632
|
+
}
|
|
633
|
+
const resolvedEvents = [];
|
|
634
|
+
for (const varName of raw.eventVarNames) {
|
|
635
|
+
const evt = eventByVarName.get(varName);
|
|
636
|
+
if (evt) resolvedEvents.push(evt);
|
|
637
|
+
else console.warn(`[scanner] Feature "${raw.id}" references unknown event variable "${varName}" in ${raw.filePath}`);
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
id: raw.id,
|
|
641
|
+
filePath: raw.filePath,
|
|
642
|
+
dependencies: raw.dependencies,
|
|
643
|
+
services,
|
|
644
|
+
tasks,
|
|
645
|
+
events: resolvedEvents,
|
|
646
|
+
publishedEvents: raw.publishedEvents
|
|
647
|
+
};
|
|
648
|
+
}),
|
|
649
|
+
windows: allWindows.map((pw) => pw.window)
|
|
650
|
+
};
|
|
602
651
|
}
|
|
603
652
|
|
|
604
653
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cordy/electro-generator",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Code generator for @cordy/electro — scans features and emits preload scripts and bridge types",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,14 +44,14 @@
|
|
|
44
44
|
"prepublishOnly": "bun run build"
|
|
45
45
|
},
|
|
46
46
|
"peerDependencies": {
|
|
47
|
-
"@cordy/electro": "1.0
|
|
47
|
+
"@cordy/electro": "1.1.0"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"oxc-parser": "^0.114.0",
|
|
51
51
|
"tinyglobby": "^0.2.15"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@cordy/electro": "1.
|
|
54
|
+
"@cordy/electro": "1.1.0",
|
|
55
55
|
"tsdown": "^0.20.3",
|
|
56
56
|
"vitest": "v4.1.0-beta.3"
|
|
57
57
|
}
|