automate-electron-ipc
Version:
Node library for automating the generation of IPC components for Electron apps.
235 lines (234 loc) • 8.49 kB
JavaScript
/*
* Apache License 2.0
*
* Copyright (c) 2024, Mattias Aabmets
*
* The contents of this file are subject to the terms and conditions defined in the License.
* You may not use, modify, or distribute this file except in compliance with the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
import ts from "typescript";
import utils from "./utils.js";
import vld from "./validators.js";
export function isBuiltinType(typeName) {
return new Set([
"string",
"number",
"boolean",
"void",
"any",
"unknown",
"null",
"undefined",
"never",
"object",
"Function",
"Promise",
]).has(typeName);
}
export function collectCustomTypes(node, src, set) {
if (!node) {
return;
}
else if (ts.isTypeReferenceNode(node)) {
const typeName = node.typeName.getText(src);
if (!isBuiltinType(typeName)) {
set.add(typeName);
}
}
else if (ts.isTypeLiteralNode(node)) {
node.members.forEach((member) => {
if (ts.isPropertySignature(member) && member.type) {
collectCustomTypes(member.type, src, set);
}
});
}
else if (ts.isUnionTypeNode(node) || ts.isIntersectionTypeNode(node)) {
node.types.forEach((subType) => collectCustomTypes(subType, src, set));
}
else if (ts.isBindingElement(node)) {
const children = node.getChildren();
if (children.length === 3 && ts.isIdentifier(children[2])) {
const name = children[2].getText(src);
if (!isBuiltinType(name)) {
set.add(name);
}
}
}
else if (ts.isImportDeclaration(node)) {
const clause = node.importClause;
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings)) {
clause.namedBindings.elements.forEach((element) => {
const name = element.name.getText(src);
if ((clause?.isTypeOnly || element.isTypeOnly) && !isBuiltinType(name)) {
set.add(name);
}
});
}
}
node.forEachChild((child) => collectCustomTypes(child, src, set));
}
export const channelPattern = utils.concatRegex([
/^Channel\(['"](?<name>\w+)['"]\)/,
/.(?<direction>RendererToMain|MainToRenderer|RendererToRenderer)/,
/.(?<kind>Broadcast|Unicast|Port)$/,
]);
export function isSignatureAssignment(text) {
const regex = /^signature\s*:\s*type +as\s*\(/;
return regex.test(text);
}
export function isListenersAssignment(text) {
const regex = /^listeners\s*:\s*\[\s*['"\w\s,]{0,1000}]$/;
return regex.test(text);
}
export function isTriggerAssignment(text) {
const regex = /^trigger\s*:\s*['"][\w-]*['"]\s*,?/;
return regex.test(text);
}
export function parseChannelExpressions(node, src, spec) {
if (ts.isPropertyAccessExpression(node)) {
const text = node.getText(src).replaceAll(/\s*/gm, "");
const groups = text.match(channelPattern)?.groups;
Object.assign(spec, groups);
}
else if (ts.isPropertyAssignment(node)) {
const text = node.getText(src);
if (isSignatureAssignment(text)) {
const child1 = node.getChildAt(2);
const child2 = child1.getChildAt(2);
if (ts.isFunctionTypeNode(child2)) {
const set = new Set();
collectCustomTypes(child2, src, set);
const returnType = child2.type.getText(src) || "void";
const async = returnType.startsWith("Promise");
spec.signature = {
definition: child2.getText(src),
params: child2.parameters.map((param) => {
return {
name: param.name.getText(),
type: param?.type ? param.type.getText() : "any",
rest: !!param.dotDotDotToken,
optional: !!param.questionToken,
};
}),
customTypes: Array.from(set),
returnType,
async,
};
}
}
else if (isListenersAssignment(text)) {
const regex = /(['"])(\w*)\1/g;
const matches = [...text.matchAll(regex)];
if (matches.length > 0) {
spec.listeners = [];
matches.forEach((match) => {
if (spec.listeners && match[2].length > 0) {
spec.listeners.push(match[2]);
}
});
}
}
else if (isTriggerAssignment(text)) {
const regex = /['"](?<trigger>[\w-]*)['"]/;
const match = text.match(regex);
if (match) {
spec.trigger = match?.groups?.trigger;
}
}
}
node.forEachChild((child) => parseChannelExpressions(child, src, spec));
}
export function parseImportDeclarations(node, src, array) {
const customTypes = new Set();
const moduleSpecifier = node.moduleSpecifier;
const clause = node.importClause;
const importSpec = {
fromPath: moduleSpecifier.getText(src).slice(1, -1),
customTypes: [],
namespace: null,
};
if (clause?.namedBindings) {
if (ts.isNamespaceImport(clause.namedBindings)) {
importSpec.namespace = clause.namedBindings.name.getText(src);
array.push(importSpec);
}
else if (ts.isNamedImports(clause.namedBindings)) {
clause.namedBindings.elements.forEach((element) => {
const isTypeOnlyImport = clause.isTypeOnly || element.isTypeOnly;
const localName = element.name.getText(src);
const exportedName = element.propertyName ? element.propertyName.getText(src) : null;
if (isTypeOnlyImport && !isBuiltinType(exportedName || localName)) {
const typeName = exportedName ? `${exportedName} as ${localName}` : localName;
customTypes.add(typeName);
}
});
importSpec.customTypes = Array.from(customTypes);
array.push(importSpec);
}
}
}
export function parseTypeDefinitions(node, src, array) {
let isExported = false;
node.modifiers?.forEach((mod) => {
isExported = mod.kind === ts.SyntaxKind.ExportKeyword ? true : isExported;
});
const kind = new Map([
["InterfaceDeclaration", "interface"],
["TypeAliasDeclaration", "type"],
]).get(ts.SyntaxKind[node.kind]);
let generics = null;
if (node.typeParameters && node.typeParameters.length > 0) {
const typeParams = node.typeParameters.map((tp) => tp.getText(src)).join(", ");
generics = `<${typeParams}>`;
}
array.push({
name: node.name.getText(src),
kind: kind,
generics,
isExported,
});
}
export function parseSpecs(fileData) {
const channelSpecArray = [];
const importSpecArray = [];
const typeSpecArray = [];
const src = ts.createSourceFile("temp.ts", fileData.contents, ts.ScriptTarget.Latest, true);
ts.forEachChild(src, (node) => {
if (ts.isExpressionStatement(node)) {
if (node.getText(src).startsWith("Channel")) {
const spec = {};
parseChannelExpressions(node, src, spec);
channelSpecArray.push(spec);
}
}
else if (ts.isImportDeclaration(node)) {
parseImportDeclarations(node, src, importSpecArray);
}
else if (ts.isInterfaceDeclaration(node)) {
parseTypeDefinitions(node, src, typeSpecArray);
}
else if (ts.isTypeAliasDeclaration(node)) {
parseTypeDefinitions(node, src, typeSpecArray);
}
});
return {
typeSpecArray: vld.validateTypeSpecs(typeSpecArray),
channelSpecArray: vld.validateChannelSpecs(channelSpecArray),
importSpecArray: importSpecArray.filter((item) => {
return item.customTypes.length > 0 || item.namespace !== null;
}),
};
}
export default {
isBuiltinType,
collectCustomTypes,
channelPattern,
isSignatureAssignment,
isListenersAssignment,
parseChannelExpressions,
parseImportDeclarations,
parseTypeDefinitions,
parseSpecs,
};