@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 CHANGED
@@ -1,4 +1,4 @@
1
- import { WindowDefinition } from "@cordy/electro";
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
- windows: readonly WindowDefinition[];
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 window definitions.
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 + window definitions and emits generated files.
11
- * Uses PolicyEngine for deny-by-default per-window filtering.
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 window.
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(windowName, features, policy, preloadExtension) {
32
- const allowedFeatures = features.filter((f) => policy.canAccess(windowName, f.id));
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/${windowName}.gen.ts`,
58
+ path: `generated/preload/${viewName}.gen.ts`,
59
59
  content
60
60
  };
61
61
  }
62
62
  /**
63
- * Generate bridge type declarations for a specific window.
63
+ * Generate bridge type declarations for a specific view.
64
64
  * Makes `window.electro` type-safe in the renderer.
65
65
  */
66
- function generateBridgeTypes(windowName, features, policy) {
67
- const allowedFeatures = features.filter((f) => policy.canAccess(windowName, f.id));
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 }` : "Record<string, never>"}
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/windows/${windowName}.bridge.d.ts`,
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 feature type declarations for the main process.
117
- * Emits a single FeatureMap interface with per-feature entries for
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 generateWindowTypes(windows) {
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 electronType = (win.type ?? "base-window") === "browser-window" ? "import(\"electron\").BrowserWindow" : "import(\"electron\").BaseWindow";
125
- entries.push(` "${win.name}": ${electronType};`);
126
- }
127
- return `\n interface WindowMap {\n${entries.join("\n")}\n }\n`;
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
- ${generateWindowTypes(windows)}}
212
+ ${generateViewTypes(views)}${generateWindowApiTypes(windows, srcDir)}}
203
213
  `
204
214
  };
205
215
  }
206
216
  /**
207
- * Generate all output files from scan results and window definitions.
217
+ * Generate all output files from scan results and view definitions.
208
218
  */
209
219
  function generate(input) {
210
- const { scanResult, windows, outputDir, srcDir } = input;
211
- const policy = new PolicyEngine(windows);
220
+ const { scanResult, views, srcDir } = input;
221
+ const policy = new PolicyEngine(views);
212
222
  const files = [];
213
- for (const win of windows) {
223
+ for (const view of views) {
214
224
  const knownIds = new Set(scanResult.features.map((f) => f.id));
215
- for (const fId of win.features ?? []) if (!knownIds.has(fId)) console.warn(`[generator] Window "${win.name}" references unknown feature "${fId}"`);
216
- files.push(generatePreload(win.name, scanResult.features, policy, win.preload));
217
- files.push(generateBridgeTypes(win.name, scanResult.features, policy));
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 { features: allFeatures.map((raw) => {
574
- const services = [];
575
- for (const varName of raw.serviceVarNames) {
576
- const svc = serviceByVarName.get(varName);
577
- if (svc) services.push(svc);
578
- else console.warn(`[scanner] Feature "${raw.id}" references unknown service variable "${varName}" in ${raw.filePath}`);
579
- }
580
- const tasks = [];
581
- for (const varName of raw.taskVarNames) {
582
- const task = taskByVarName.get(varName);
583
- if (task) tasks.push(task);
584
- else console.warn(`[scanner] Feature "${raw.id}" references unknown task variable "${varName}" in ${raw.filePath}`);
585
- }
586
- const resolvedEvents = [];
587
- for (const varName of raw.eventVarNames) {
588
- const evt = eventByVarName.get(varName);
589
- if (evt) resolvedEvents.push(evt);
590
- else console.warn(`[scanner] Feature "${raw.id}" references unknown event variable "${varName}" in ${raw.filePath}`);
591
- }
592
- return {
593
- id: raw.id,
594
- filePath: raw.filePath,
595
- dependencies: raw.dependencies,
596
- services,
597
- tasks,
598
- events: resolvedEvents,
599
- publishedEvents: raw.publishedEvents
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.8",
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.8"
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.0.0",
54
+ "@cordy/electro": "1.1.0",
55
55
  "tsdown": "^0.20.3",
56
56
  "vitest": "v4.1.0-beta.3"
57
57
  }