@pact-toolbox/unplugin
Version:
389 lines (378 loc) • 12.8 kB
JavaScript
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)
(#any-of?
"with-capability" "require-capability" "compose-capability" "install-capability"
)
tail: (s_expression (s_expression_head) )*
)`;
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