UNPKG

automate-electron-ipc

Version:

Node library for automating the generation of IPC components for Electron apps.

235 lines (234 loc) 8.49 kB
/* * 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, };