UNPKG

@pact-toolbox/unplugin

Version:
389 lines (378 loc) 12.8 kB
import { camelCase, pascalCase } from "scule"; import Parser, { Query } from "tree-sitter"; import Pact from "tree-sitter-pact"; //#region src/transformer/parameter.ts /** * Represents a parameter (used in functions and capabilities). */ var PactParameter = class { name; type; constructor(node, module) { this.node = node; this.module = module; this.name = node.childForFieldName("name")?.text || ""; this.type = node.childForFieldName("type")?.descendantsOfType("type_identifier")?.[0]?.text || "unknown"; } }; //#endregion //#region src/transformer/utils.ts const TYPE_MAP = { integer: "number", decimal: "number", time: "Date", bool: "boolean", string: "string", list: "unknown[]", keyset: "object", guard: "object", object: "Record<string, unknown>", table: "Record<string, unknown>" }; /** * Maps Pact types to TypeScript types. * @param pactType The Pact type as a string. * @param module The current module context. * @returns The corresponding TypeScript type as a string. */ function pactTypeToTypescriptType(pactType, module) { if (pactType.startsWith("{") || pactType.startsWith("object{")) { const schemaName = pactType.startsWith("{") ? pactType.slice(1, -1) : pactType.slice(7, -1); const schema = module.getSchema(schemaName); if (schema) return pascalCase(schema.name); return "Record<string, unknown>"; } if (pactType.startsWith("[")) { const innerType = pactType.slice(1, -1); if (innerType) return `${pactTypeToTypescriptType(innerType, module)}[]`; return "unknown[]"; } return TYPE_MAP[pactType] || "unknown"; } function getReturnTypeOf(node) { return node.childForFieldName("return_type")?.descendantsOfType("type_identifier")?.[0]?.text || "void"; } function getNamespaceOf(node) { let namespace = node?.descendantsOfType("namespace")?.[0]?.childForFieldName("namespace")?.text; if (!namespace) return void 0; if (namespace.startsWith("'")) return namespace.slice(1); return namespace.slice(1, -1); } /** * Converts a multi-line string with backslashes into a JSDoc comment. * * @param {string} inputStr - The original multi-line string with backslashes. * @returns {string} - The formatted JSDoc comment. * * @example * const originalString = " Checks ACCOUNT for reserved name and returns type if \\ * \\ found or empty string. Reserved names start with a \\ * \\ single char and colon, e.g. 'c:foo', which would return 'c' as type."; * * console.log(convertToJsDoc(originalString)); * * // Output: * /** * * Checks ACCOUNT for reserved name and returns type if * * found or empty string. Reserved names start with a * * single char and colon, e.g. 'c:foo', which would return 'c' as type. * *\/ */ function convertToJsDoc(inputStr) { if (!inputStr) return ""; let trimmedStr = inputStr.trim(); if (trimmedStr.startsWith("\"") && trimmedStr.endsWith("\"") || trimmedStr.startsWith("'") && trimmedStr.endsWith("'")) trimmedStr = trimmedStr.slice(1, -1); trimmedStr = trimmedStr.replace(/\\\s*\\/g, " "); trimmedStr = trimmedStr.replace(/\\\s*\n\s*/g, " "); const lines = trimmedStr.split(/(?<=\.)\s+/); const jsDocLines = lines.map((line) => ` * ${line.trim()}`); const jsDocComment = [ "/**", ...jsDocLines, " */" ].join("\n"); return `${jsDocComment}\n`; } //#endregion //#region src/transformer/capability.ts /** * Represents a Pact capability. */ var PactCapability = class { name; path; parameters; returnType; doc = ""; constructor(node, module) { this.node = node; this.module = module; this.name = node.childForFieldName("name")?.text || ""; this.returnType = getReturnTypeOf(node); this.doc = node.childForFieldName("doc")?.descendantsOfType("doc_string")[0]?.text || ""; this.path = `${module.path}.${this.name}`; this.parameters = []; const parameterNodes = node.childForFieldName("parameters")?.children || []; for (const pNode of parameterNodes) if (pNode.type === "parameter") this.parameters.push(new PactParameter(pNode, module)); } }; //#endregion //#region src/transformer/codeGenerator.ts /** * Generates JavaScript/TypeScript code for a Pact module. * @param module The PactModule to generate code for. * @returns The generated module code as a string. */ function generateModuleCode(module, debug = false) { let code = `// This file was generated by the Pact Toolbox\nimport { execution } from "@pact-toolbox/client";\n`; let types = `// This file was generated by the Pact Toolbox\nimport { PactTransactionBuilder, PactExecPayload } from "@pact-toolbox/client";\n`; for (const schema of module.schemas) { const schemaTypes = generateSchemaTypes(schema); types += `${schemaTypes}\n`; } for (const func of module.functions) { const functionCode = generateFunctionCode(func, debug); code += `${functionCode}\n`; const functionTypes = generateFunctionTypes(func); types += `${functionTypes}\n`; } return { code, types }; } function insertDebugLog(pactCall, debug = false) { if (debug) return ` console.log("%c[pact-toolbox] executing pact code",'font-weight: bold; font-style: italic', ${pactCall});`; return void 0; } /** * Generates JavaScript/TypeScript code for a Pact function. * @param func The PactFunction to generate code for. * @returns The generated function code as a string. */ function generateFunctionCode(func, debug = false) { const paramList = func.parameters.map((p) => camelCase(p.name)).join(", "); const pactCall = func.parameters.length > 0 ? `\`(${func.path} ${func.parameters.map((p) => `\${JSON.stringify(${camelCase(p.name)})}`).join(" ")})\`` : `"(${func.path})"`; return [ `export function ${camelCase(func.name)}(${paramList}) {`, insertDebugLog(pactCall, debug), ` return execution(${pactCall});`, "}" ].filter(Boolean).join("\n"); } /** * Generates TypeScript type declarations for a Pact function. * @param func The PactFunction to generate type declarations for. * @returns The generated function types as a string. */ function generateFunctionTypes(func) { const paramList = func.parameters.map((p) => `${camelCase(p.name)}: ${pactTypeToTypescriptType(p.type, func.module)}`).join(", "); return `${convertToJsDoc(func.doc)}export function ${camelCase(func.name)}(${paramList}): PactTransactionBuilder<PactExecPayload, ${pactTypeToTypescriptType(func.returnType, func.module)}>;`; } /** * Generates TypeScript interface definitions for a Pact schema. * @param schema The PactSchema to generate interface definitions for. * @returns The generated schema types as a string. */ function generateSchemaTypes(schema) { const fieldList = schema.fields.map((f) => ` ${f.name}: ${pactTypeToTypescriptType(f.type, schema.module)};`).join("\n"); return `${convertToJsDoc(schema.doc)}export interface ${pascalCase(schema.name)} {\n${fieldList}\n}`; } //#endregion //#region src/transformer/errors.ts /** * Custom error class for transformation errors. */ var TransformationError = class extends Error { constructor(message) { super(message); this.name = "TransformationError"; } }; /** * Custom error class for parsing errors. */ var ParsingError = class extends TransformationError { errors; constructor(message, errors) { super(message); this.name = "ParsingError"; this.errors = errors; } }; //#endregion //#region src/transformer/function.ts function getRequiredCapabilities(node) { const queryStr = `(s_expression head: (s_expression_head) @caller (#any-of? @caller "with-capability" "require-capability" "compose-capability" "install-capability" ) tail: (s_expression (s_expression_head) @cap)* )`; const query = new Query(Pact, queryStr); query.matches(node); return []; } /** * Represents a Pact function. */ var PactFunction = class { name; path; parameters = []; returnType; requiredCapabilities = []; doc = ""; constructor(node, module) { this.node = node; this.module = module; this.name = node.childForFieldName("name")?.text || ""; this.returnType = getReturnTypeOf(node); this.doc = node.childForFieldName("doc")?.descendantsOfType("doc_string")[0]?.text || ""; this.path = `${module.path}.${this.name}`; const parameterNodes = node.childForFieldName("parameters")?.children || []; for (const pNode of parameterNodes) if (pNode.type === "parameter") this.parameters.push(new PactParameter(pNode, module)); } }; //#endregion //#region src/transformer/schema.ts /** * Represents a field within a schema. */ var PactSchemaField = class { name; type; constructor(node, module) { this.node = node; this.module = module; this.name = node.childForFieldName("name")?.text || ""; this.type = node.descendantsOfType("type_identifier")?.[0]?.text || "unknown"; } }; /** * Represents a Pact schema. */ var PactSchema = class { name; fields = []; doc = ""; constructor(node, module) { this.node = node; this.module = module; this.name = node.childForFieldName("name")?.text || ""; this.doc = node.childForFieldName("doc")?.descendantsOfType("doc_string")[0]?.text || ""; const fieldNodes = node.childForFieldName("fields")?.descendantsOfType("schema_field") || []; for (const child of fieldNodes) this.fields.push(new PactSchemaField(child, module)); } }; //#endregion //#region src/transformer/module.ts /** * Represents a Pact module. */ var PactModule = class { name; governance = ""; functions = []; schemas = []; capabilities = []; doc = ""; path; constructor(node, namespace) { this.node = node; this.namespace = namespace; this.name = node.childForFieldName("name")?.text || ""; this.doc = node.childForFieldName("doc")?.descendantsOfType("doc_string")[0]?.text || ""; this.governance = node.childForFieldName("governance")?.text || ""; this.path = this.namespace ? `${this.namespace}.${this.name}` : this.name; for (const child of node.children) if (child.type === "defschema") this.schemas.push(new PactSchema(child, this)); else if (child.type === "defcap") this.capabilities.push(new PactCapability(child, this)); else if (child.type === "defun") this.functions.push(new PactFunction(child, this)); } getSchema(name) { return this.schemas.find((s) => s.name.toLowerCase() === name.toLowerCase()); } getFunction(name) { return this.functions.find((f) => f.name === name); } getCapability(name) { return this.capabilities.find((c) => c.name === name); } }; //#endregion //#region src/transformer/transformer.ts /** * Class responsible for transforming Pact code into a custom AST and applying visitors. */ var PactTransformer = class { parser; constructor() { this.parser = new Parser(); this.parser.setLanguage(Pact); } /** * Transforms the given Pact code into a custom AST and applies the provided visitors. * @param pactCode The Pact code as a string. * @param visitors Array of Visitor instances to apply during traversal. * @returns The transformation result containing the AST. */ transform(pactCode) { const tree = this.parser.parse(pactCode); const root = tree.rootNode; if (root.hasError) { const errors = this.collectErrors(root); throw new ParsingError("Failed to parse Pact code due to syntax errors.", errors); } const modules = []; const namespace = getNamespaceOf(root); for (const node of root.children) if (node.type === "module") modules.push(new PactModule(node, namespace)); return modules; } /** * Collects detailed error information from the syntax tree. * @param root The root node of the syntax tree. * @returns An array of error details. */ collectErrors(root) { const errorNodes = root.descendantsOfType("ERROR"); const errors = []; for (const node of errorNodes) { const startPosition = node.startPosition; const message = `Unexpected token '${node.text}'`; errors.push({ message, line: startPosition.row + 1, column: startPosition.column + 1 }); } return errors; } }; //#endregion //#region src/transformer/pactToJS.ts function createPactToJSTransformer({ debug } = {}) { const transformer = new PactTransformer(); const transform = (pactCode) => { const modules = transformer.transform(pactCode); let code = ""; let types = ""; for (const module of modules) { const { code: moduleCode, types: moduleTypes } = generateModuleCode(module, debug); code += moduleCode + "\n"; types += moduleTypes + "\n"; } return { modules: modules.map((m) => ({ name: m.name, path: m.path })), code, types }; }; return transform; } //#endregion export { PactCapability, PactFunction, PactModule, PactParameter, PactSchema, PactSchemaField, PactTransformer, ParsingError, TransformationError, convertToJsDoc, createPactToJSTransformer, generateFunctionCode, generateFunctionTypes, generateModuleCode, generateSchemaTypes, getNamespaceOf, getRequiredCapabilities, getReturnTypeOf, pactTypeToTypescriptType }; //# sourceMappingURL=pactToJS-p3Xxm_Ew.js.map