@friehub/blueprint 0.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/.github/workflows/ci.yml +122 -0
- package/.github/workflows/publish.yml +24 -0
- package/README.md +266 -0
- package/adapters/analytics/amplitude.yaml +44 -0
- package/adapters/analytics/mixpanel.yaml +47 -0
- package/adapters/analytics/segment.yaml +40 -0
- package/adapters/auth/auth0.yaml +56 -0
- package/adapters/auth/clerk.yaml +53 -0
- package/adapters/auth/supertokens.yaml +55 -0
- package/adapters/billing/paddle.yaml +57 -0
- package/adapters/billing/stripe.yaml +49 -0
- package/adapters/caching/memcached.yaml +28 -0
- package/adapters/caching/redis.yaml +37 -0
- package/adapters/chargebacks/chargebacks911.yaml +45 -0
- package/adapters/chargebacks/stripe.yaml +45 -0
- package/adapters/crm_leads/hubspot.yaml +43 -0
- package/adapters/crm_leads/salesforce.yaml +60 -0
- package/adapters/customer_support/intercom.yaml +44 -0
- package/adapters/customer_support/zendesk.yaml +51 -0
- package/adapters/donations/paypal.yaml +47 -0
- package/adapters/donations/stripe.yaml +47 -0
- package/adapters/emails/mailgun.yaml +47 -0
- package/adapters/emails/resend.yaml +43 -0
- package/adapters/emails/sendgrid.yaml +43 -0
- package/adapters/error_tracking/bugsnag.yaml +52 -0
- package/adapters/error_tracking/sentry.yaml +58 -0
- package/adapters/feature_flags/flagsmith.yaml +41 -0
- package/adapters/feature_flags/launchdarkly.yaml +41 -0
- package/adapters/feature_flags/unleash.yaml +41 -0
- package/adapters/fraud_detection/riskified.yaml +41 -0
- package/adapters/fraud_detection/sift.yaml +40 -0
- package/adapters/fulfillment/easyship.yaml +51 -0
- package/adapters/fulfillment/shipengine.yaml +51 -0
- package/adapters/incident_management/opsgenie.yaml +49 -0
- package/adapters/incident_management/pagerduty.yaml +48 -0
- package/adapters/invoicing/freshbooks.yaml +54 -0
- package/adapters/invoicing/stripe.yaml +47 -0
- package/adapters/ip_intelligence/ipinfo.yaml +37 -0
- package/adapters/ip_intelligence/maxmind.yaml +39 -0
- package/adapters/jobs/bullmq.yaml +54 -0
- package/adapters/jobs/temporal.yaml +53 -0
- package/adapters/kyc/jumio.yaml +54 -0
- package/adapters/kyc/onfido.yaml +53 -0
- package/adapters/media/cloudinary.yaml +48 -0
- package/adapters/media/imgix.yaml +47 -0
- package/adapters/notifications/firebase.yaml +45 -0
- package/adapters/notifications/novu.yaml +46 -0
- package/adapters/notifications/onesignal.yaml +45 -0
- package/adapters/payments/adyen.yaml +46 -0
- package/adapters/payments/paystack.yaml +45 -0
- package/adapters/payments/stripe.yaml +49 -0
- package/adapters/payouts/paypal.yaml +49 -0
- package/adapters/payouts/stripe.yaml +49 -0
- package/adapters/projects/asana.yaml +49 -0
- package/adapters/projects/jira.yaml +58 -0
- package/adapters/projects/linear.yaml +49 -0
- package/adapters/queues/bullmq.yaml +47 -0
- package/adapters/queues/rabbitmq.yaml +51 -0
- package/adapters/queues/sqs.yaml +45 -0
- package/adapters/rate_limiting/cloudflare.yaml +37 -0
- package/adapters/rate_limiting/upstash.yaml +35 -0
- package/adapters/search/algolia.yaml +39 -0
- package/adapters/search/meilisearch.yaml +39 -0
- package/adapters/search/typesense.yaml +42 -0
- package/adapters/shipping/easyship.yaml +45 -0
- package/adapters/shipping/shipengine.yaml +45 -0
- package/adapters/sms/twilio.yaml +41 -0
- package/adapters/sms/vonage.yaml +41 -0
- package/adapters/storage/azure-blob.yaml +42 -0
- package/adapters/storage/gcs.yaml +41 -0
- package/adapters/storage/s3.yaml +49 -0
- package/adapters/subscriptions/chargebee.yaml +32 -0
- package/adapters/subscriptions/stripe.yaml +37 -0
- package/adapters/tasks/asana.yaml +50 -0
- package/adapters/tasks/jira.yaml +59 -0
- package/adapters/tasks/linear.yaml +50 -0
- package/adapters/taxation/avalara.yaml +52 -0
- package/adapters/taxation/taxjar.yaml +48 -0
- package/adapters/trace_query/datadog.yaml +49 -0
- package/adapters/trace_query/honeycomb.yaml +42 -0
- package/adapters/trace_query/jaeger.yaml +42 -0
- package/adapters/web_analytics/google-analytics.yaml +42 -0
- package/adapters/web_analytics/plausible.yaml +34 -0
- package/adapters/web_analytics/posthog.yaml +34 -0
- package/adapters/webhooks/relay.yaml +35 -0
- package/adapters/webhooks/svix.yaml +41 -0
- package/blueprint.json +5 -0
- package/blueprinter_system_design.svg +139 -0
- package/catalog.json +37943 -0
- package/dist/cli/commands.js +362 -0
- package/dist/cli/help.js +211 -0
- package/dist/cli/render.js +109 -0
- package/dist/cli.js +69 -0
- package/dist/core/adapters/adapter-audit.test.js +85 -0
- package/dist/core/adapters/adapter.test.js +66 -0
- package/dist/core/adapters/index.js +4 -0
- package/dist/core/adapters/load.js +131 -0
- package/dist/core/adapters/resolve.js +78 -0
- package/dist/core/adapters/select.js +80 -0
- package/dist/core/adapters/types.js +1 -0
- package/dist/core/adapters/validate.js +121 -0
- package/dist/core/catalog.js +3 -0
- package/dist/core/collectors.js +126 -0
- package/dist/core/discovery.js +57 -0
- package/dist/core/edge-cases.test.js +147 -0
- package/dist/core/envelope.js +1 -0
- package/dist/core/graph.js +123 -0
- package/dist/core/graph.test.js +62 -0
- package/dist/core/implement.js +48 -0
- package/dist/core/index.js +9 -0
- package/dist/core/load-catalog.js +24 -0
- package/dist/core/parse-document.js +114 -0
- package/dist/core/parse-document.test.js +104 -0
- package/dist/core/parser.js +6 -0
- package/dist/core/parser.test.js +134 -0
- package/dist/core/resolve.js +119 -0
- package/dist/core/resolve.test.js +108 -0
- package/dist/core/scanner.js +163 -0
- package/dist/core/search.js +34 -0
- package/dist/core/search.test.js +43 -0
- package/dist/core/section-body.js +258 -0
- package/dist/core/sections.js +53 -0
- package/dist/core/verify-implement.test.js +123 -0
- package/dist/core/verify.js +156 -0
- package/dist/generators/engine.js +86 -0
- package/dist/generators/generator.test.js +112 -0
- package/dist/generators/index.js +3 -0
- package/dist/generators/prototype/index.js +242 -0
- package/dist/generators/render.js +146 -0
- package/dist/generators/types.js +124 -0
- package/dist/generators/typescript/helpers.js +125 -0
- package/dist/generators/typescript/index.js +206 -0
- package/dist/index.js +8 -0
- package/dist/mcp/server.js +202 -0
- package/dist/mcp/server.test.js +136 -0
- package/dist/utils/args.js +142 -0
- package/package.json +38 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
export async function verifyImplementation(implementationFile, moduleName, catalog) {
|
|
3
|
+
const mod = catalog.modules.find((m) => m.name === moduleName);
|
|
4
|
+
if (!mod) {
|
|
5
|
+
return {
|
|
6
|
+
module: moduleName,
|
|
7
|
+
contractVersion: null,
|
|
8
|
+
valid: false,
|
|
9
|
+
implFunctions: [],
|
|
10
|
+
contractFunctions: [],
|
|
11
|
+
issues: [{ module: moduleName, kind: "file-not-found", function: "", message: `Module "${moduleName}" not found in catalog` }],
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
let content;
|
|
15
|
+
try {
|
|
16
|
+
content = await readFile(implementationFile, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return {
|
|
20
|
+
module: moduleName,
|
|
21
|
+
contractVersion: mod.version,
|
|
22
|
+
valid: false,
|
|
23
|
+
implFunctions: [],
|
|
24
|
+
contractFunctions: mod.functions.map((f) => f.name),
|
|
25
|
+
issues: [{ module: moduleName, kind: "file-not-found", function: "", message: `File not found: ${implementationFile}` }],
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const issues = [];
|
|
29
|
+
const implFns = extractFunctions(content);
|
|
30
|
+
for (const fn of mod.functions) {
|
|
31
|
+
const implFn = implFns.find((f) => f.name === fn.name);
|
|
32
|
+
if (!implFn) {
|
|
33
|
+
const expectedSig = formatSignature(fn);
|
|
34
|
+
issues.push({
|
|
35
|
+
module: moduleName,
|
|
36
|
+
kind: "missing",
|
|
37
|
+
function: fn.name,
|
|
38
|
+
expected: expectedSig,
|
|
39
|
+
message: `Missing: ${expectedSig}`,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const issues = compareReturnTypes(fn, implFn, moduleName);
|
|
44
|
+
issues.push(...issues);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
module: moduleName,
|
|
49
|
+
contractVersion: mod.version,
|
|
50
|
+
valid: issues.filter((i) => i.kind === "missing" || i.kind === "mismatch").length === 0,
|
|
51
|
+
implFunctions: [...implFns.keys()].map((n) => String(n)),
|
|
52
|
+
contractFunctions: mod.functions.map((f) => f.name),
|
|
53
|
+
issues,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function extractFunctions(content) {
|
|
57
|
+
const functions = [];
|
|
58
|
+
const lines = content.split("\n");
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
const line = lines[i].trim();
|
|
61
|
+
if (line.startsWith("//") || line.startsWith("/*") || line.startsWith("*"))
|
|
62
|
+
continue;
|
|
63
|
+
// Match: async initPayment(params): ReturnType {
|
|
64
|
+
// Match: async initPayment(params) {
|
|
65
|
+
// Match: initPayment(params): ReturnType {
|
|
66
|
+
// Match: initPayment = async (params): ReturnType => {
|
|
67
|
+
// Match: private async initPayment(params): ReturnType {
|
|
68
|
+
const asyncMatch = line.match(/(?:private\s+|public\s+|protected\s+)?(?:async\s+)?(\w+)\s*[=(]\s*(?:async\s*)?[=(]?\s*(?:async\s*)?/);
|
|
69
|
+
if (asyncMatch && isValidMethodName(asyncMatch[1])) {
|
|
70
|
+
const name = asyncMatch[1];
|
|
71
|
+
// Look for return type annotation
|
|
72
|
+
let returnType = null;
|
|
73
|
+
let returnTypeLine = i;
|
|
74
|
+
// Check current line for return type
|
|
75
|
+
const rtMatch = line.match(/\)\s*:\s*(\S+?)\s*[{=]/);
|
|
76
|
+
if (rtMatch) {
|
|
77
|
+
returnType = rtMatch[1].replace(/Promise<|>/g, "").split("|")[0]?.trim() ?? null;
|
|
78
|
+
returnTypeLine = i;
|
|
79
|
+
}
|
|
80
|
+
// Check next lines for return type (multi-line)
|
|
81
|
+
if (!returnType && i + 1 < lines.length) {
|
|
82
|
+
const nextLine = lines[i + 1].trim();
|
|
83
|
+
const nextMatch = nextLine.match(/\)\s*:\s*(\S+?)\s*[{=]/);
|
|
84
|
+
if (nextMatch) {
|
|
85
|
+
returnType = nextMatch[1].replace(/Promise<|>/g, "").split("|")[0]?.trim() ?? null;
|
|
86
|
+
returnTypeLine = i + 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Check for return statements in the body
|
|
90
|
+
let hasReturn = false;
|
|
91
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 20); j++) {
|
|
92
|
+
if (lines[j].trim().startsWith("return ")) {
|
|
93
|
+
hasReturn = true;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
if (lines[j].trim() === "}")
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
functions.push({
|
|
100
|
+
name,
|
|
101
|
+
returnType,
|
|
102
|
+
returnTypeLine,
|
|
103
|
+
hasReturnStatement: hasReturn,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return functions;
|
|
108
|
+
}
|
|
109
|
+
function isValidMethodName(name) {
|
|
110
|
+
const RESERVED = new Set([
|
|
111
|
+
"constructor", "if", "else", "for", "while", "return", "const", "let", "var", "new",
|
|
112
|
+
"import", "export", "from", "typeof", "function", "class", "this", "throw", "try",
|
|
113
|
+
"catch", "finally", "switch", "case", "default",
|
|
114
|
+
]);
|
|
115
|
+
return !RESERVED.has(name) && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name);
|
|
116
|
+
}
|
|
117
|
+
function formatSignature(fn) {
|
|
118
|
+
const params = fn.params.map((p) => `${p.name}${p.optional ? "?" : ""}${p.type ? ": " + p.type : ""}`).join(", ");
|
|
119
|
+
return `${fn.name}(${params}) → ${fn.returns}`;
|
|
120
|
+
}
|
|
121
|
+
function compareReturnTypes(fn, implFn, moduleName) {
|
|
122
|
+
const issues = [];
|
|
123
|
+
if (fn.returns === "void" || fn.returns === "void?") {
|
|
124
|
+
return issues;
|
|
125
|
+
}
|
|
126
|
+
if (!implFn.returnType) {
|
|
127
|
+
if (!implFn.hasReturnStatement) {
|
|
128
|
+
issues.push({
|
|
129
|
+
module: moduleName,
|
|
130
|
+
kind: "mismatch",
|
|
131
|
+
function: fn.name,
|
|
132
|
+
expected: fn.returns,
|
|
133
|
+
message: `${fn.name}: expected return type "${fn.returns}" but no return type annotation found. Add return type.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return issues;
|
|
137
|
+
}
|
|
138
|
+
const expected = fn.returns.toLowerCase().replace("?", "");
|
|
139
|
+
const actual = implFn.returnType.toLowerCase();
|
|
140
|
+
if (expected !== actual &&
|
|
141
|
+
expected !== actual.replace("[]", "") &&
|
|
142
|
+
!expected.includes(actual) &&
|
|
143
|
+
!actual.includes(expected) &&
|
|
144
|
+
actual !== "unknown" &&
|
|
145
|
+
actual !== "any") {
|
|
146
|
+
issues.push({
|
|
147
|
+
module: moduleName,
|
|
148
|
+
kind: "mismatch",
|
|
149
|
+
function: fn.name,
|
|
150
|
+
expected: fn.returns,
|
|
151
|
+
actual: implFn.returnType,
|
|
152
|
+
message: `${fn.name}: expected return type "${fn.returns}" but found "${implFn.returnType}"`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return issues;
|
|
156
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const generators = new Map();
|
|
4
|
+
export function registerGenerator(generator) {
|
|
5
|
+
generators.set(generator.language, generator);
|
|
6
|
+
}
|
|
7
|
+
export function getGenerator(language) {
|
|
8
|
+
return generators.get(language);
|
|
9
|
+
}
|
|
10
|
+
export function getAvailableLanguages() {
|
|
11
|
+
return [...generators.keys()];
|
|
12
|
+
}
|
|
13
|
+
export async function generate(catalog, adapters, options) {
|
|
14
|
+
const generator = generators.get(options.language);
|
|
15
|
+
if (!generator) {
|
|
16
|
+
return {
|
|
17
|
+
files: [],
|
|
18
|
+
errors: [`Generator for language "${options.language}" not registered`],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const context = {
|
|
22
|
+
catalog,
|
|
23
|
+
adapters,
|
|
24
|
+
module: options.module,
|
|
25
|
+
provider: options.provider,
|
|
26
|
+
};
|
|
27
|
+
let result;
|
|
28
|
+
switch (options.type) {
|
|
29
|
+
case "interfaces":
|
|
30
|
+
result = generator.generateInterfaces(context);
|
|
31
|
+
break;
|
|
32
|
+
case "adapters":
|
|
33
|
+
result = generator.generateAdapter(context);
|
|
34
|
+
break;
|
|
35
|
+
case "tests":
|
|
36
|
+
result = generator.generateTests(context);
|
|
37
|
+
break;
|
|
38
|
+
case "all":
|
|
39
|
+
result = mergeResults(generator.generateInterfaces(context), generator.generateAdapter(context), generator.generateTests(context));
|
|
40
|
+
break;
|
|
41
|
+
default:
|
|
42
|
+
result = { files: [], errors: [`Unknown generation type: ${options.type}`] };
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
export async function generateAndWrite(catalog, adapters, options) {
|
|
47
|
+
const result = await generate(catalog, adapters, options);
|
|
48
|
+
let written = 0;
|
|
49
|
+
const errors = [...result.errors];
|
|
50
|
+
for (const file of result.files) {
|
|
51
|
+
try {
|
|
52
|
+
const fullPath = join(options.outputDir, file.path);
|
|
53
|
+
await mkdir(dirname(fullPath), { recursive: true });
|
|
54
|
+
await writeFile(fullPath, file.content, "utf8");
|
|
55
|
+
written++;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
errors.push(`Failed to write ${file.path}: ${error instanceof Error ? error.message : error}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { written, errors };
|
|
62
|
+
}
|
|
63
|
+
function mergeResults(...results) {
|
|
64
|
+
const files = [];
|
|
65
|
+
const errors = [];
|
|
66
|
+
for (const result of results) {
|
|
67
|
+
files.push(...result.files);
|
|
68
|
+
errors.push(...result.errors);
|
|
69
|
+
}
|
|
70
|
+
return { files, errors };
|
|
71
|
+
}
|
|
72
|
+
export function filterModules(catalog, moduleName) {
|
|
73
|
+
if (!moduleName) {
|
|
74
|
+
return catalog.modules;
|
|
75
|
+
}
|
|
76
|
+
return catalog.modules.filter((m) => m.name === moduleName);
|
|
77
|
+
}
|
|
78
|
+
export function filterAdapters(adapters, moduleName, provider) {
|
|
79
|
+
return adapters.filter((a) => {
|
|
80
|
+
if (moduleName && a.module !== moduleName)
|
|
81
|
+
return false;
|
|
82
|
+
if (provider && a.name !== provider)
|
|
83
|
+
return false;
|
|
84
|
+
return true;
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { strict as assert } from "node:assert";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { loadCatalogFromRoot } from "../core/index.js";
|
|
4
|
+
import { loadAdapters } from "../core/adapters/load.js";
|
|
5
|
+
import { registerGenerator, generate, getAvailableLanguages } from "./engine.js";
|
|
6
|
+
import { TypeScriptGenerator } from "./typescript/index.js";
|
|
7
|
+
import { pascalCase, camelCase, snakeCase, mapType, inferType } from "./types.js";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
const ROOT = fileURLToPath(new URL("../../", import.meta.url));
|
|
11
|
+
const ADAPTERS_DIR = join(ROOT, "adapters");
|
|
12
|
+
describe("generator engine", () => {
|
|
13
|
+
it("registers TypeScript generator", () => {
|
|
14
|
+
registerGenerator(new TypeScriptGenerator());
|
|
15
|
+
const languages = getAvailableLanguages();
|
|
16
|
+
assert.ok(languages.includes("typescript"));
|
|
17
|
+
});
|
|
18
|
+
it("generates interfaces for all 108 modules", async () => {
|
|
19
|
+
const result = await loadCatalogFromRoot(ROOT, "loose");
|
|
20
|
+
const { adapters } = await loadAdapters(ADAPTERS_DIR);
|
|
21
|
+
const genResult = await generate(result.value, adapters, {
|
|
22
|
+
language: "typescript",
|
|
23
|
+
type: "interfaces",
|
|
24
|
+
module: undefined,
|
|
25
|
+
provider: undefined,
|
|
26
|
+
outputDir: "/tmp/test",
|
|
27
|
+
});
|
|
28
|
+
assert.equal(genResult.errors.length, 0, `No errors: ${genResult.errors.join(", ")}`);
|
|
29
|
+
assert.ok(genResult.files.length >= 108, `At least 108 interface files, got ${genResult.files.length}`);
|
|
30
|
+
});
|
|
31
|
+
it("generates adapters for all 82 adapters", async () => {
|
|
32
|
+
const result = await loadCatalogFromRoot(ROOT, "loose");
|
|
33
|
+
const { adapters } = await loadAdapters(ADAPTERS_DIR);
|
|
34
|
+
const genResult = await generate(result.value, adapters, {
|
|
35
|
+
language: "typescript",
|
|
36
|
+
type: "adapters",
|
|
37
|
+
module: undefined,
|
|
38
|
+
provider: undefined,
|
|
39
|
+
outputDir: "/tmp/test",
|
|
40
|
+
});
|
|
41
|
+
assert.equal(genResult.errors.length, 0, `No errors: ${genResult.errors.join(", ")}`);
|
|
42
|
+
assert.ok(genResult.files.length >= 82, `At least 82 adapter files, got ${genResult.files.length}`);
|
|
43
|
+
});
|
|
44
|
+
it("generates tests for all 82 adapters", async () => {
|
|
45
|
+
const result = await loadCatalogFromRoot(ROOT, "loose");
|
|
46
|
+
const { adapters } = await loadAdapters(ADAPTERS_DIR);
|
|
47
|
+
const genResult = await generate(result.value, adapters, {
|
|
48
|
+
language: "typescript",
|
|
49
|
+
type: "tests",
|
|
50
|
+
module: undefined,
|
|
51
|
+
provider: undefined,
|
|
52
|
+
outputDir: "/tmp/test",
|
|
53
|
+
});
|
|
54
|
+
assert.equal(genResult.errors.length, 0, `No errors: ${genResult.errors.join(", ")}`);
|
|
55
|
+
assert.ok(genResult.files.length >= 82, `At least 82 test files, got ${genResult.files.length}`);
|
|
56
|
+
});
|
|
57
|
+
it("generates for specific module", async () => {
|
|
58
|
+
const result = await loadCatalogFromRoot(ROOT, "loose");
|
|
59
|
+
const { adapters } = await loadAdapters(ADAPTERS_DIR);
|
|
60
|
+
const genResult = await generate(result.value, adapters, {
|
|
61
|
+
language: "typescript",
|
|
62
|
+
type: "all",
|
|
63
|
+
module: "billing",
|
|
64
|
+
provider: undefined,
|
|
65
|
+
outputDir: "/tmp/test",
|
|
66
|
+
});
|
|
67
|
+
assert.ok(genResult.files.some((f) => f.path.includes("billing")));
|
|
68
|
+
});
|
|
69
|
+
it("generates for specific adapter", async () => {
|
|
70
|
+
const result = await loadCatalogFromRoot(ROOT, "loose");
|
|
71
|
+
const { adapters } = await loadAdapters(ADAPTERS_DIR);
|
|
72
|
+
const genResult = await generate(result.value, adapters, {
|
|
73
|
+
language: "typescript",
|
|
74
|
+
type: "adapters",
|
|
75
|
+
module: "payments",
|
|
76
|
+
provider: "stripe",
|
|
77
|
+
outputDir: "/tmp/test",
|
|
78
|
+
});
|
|
79
|
+
assert.ok(genResult.files.some((f) => f.path.includes("stripe")));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("type utilities", () => {
|
|
83
|
+
it("pascalCase converts correctly", () => {
|
|
84
|
+
assert.equal(pascalCase("billing"), "Billing");
|
|
85
|
+
assert.equal(pascalCase("user_management"), "UserManagement");
|
|
86
|
+
assert.equal(pascalCase("api-keys"), "ApiKeys");
|
|
87
|
+
});
|
|
88
|
+
it("camelCase converts correctly", () => {
|
|
89
|
+
assert.equal(camelCase("billing"), "billing");
|
|
90
|
+
assert.equal(camelCase("user_management"), "userManagement");
|
|
91
|
+
assert.equal(camelCase("api-keys"), "apiKeys");
|
|
92
|
+
});
|
|
93
|
+
it("snakeCase converts correctly", () => {
|
|
94
|
+
assert.equal(snakeCase("billing"), "billing");
|
|
95
|
+
assert.equal(snakeCase("userManagement"), "user_management");
|
|
96
|
+
assert.equal(snakeCase("api-keys"), "api_keys");
|
|
97
|
+
});
|
|
98
|
+
it("mapType converts correctly", () => {
|
|
99
|
+
assert.equal(mapType("string", "typescript"), "string");
|
|
100
|
+
assert.equal(mapType("number", "rust"), "f64");
|
|
101
|
+
assert.equal(mapType("boolean", "python"), "bool");
|
|
102
|
+
assert.equal(mapType("string[]", "typescript"), "string[]");
|
|
103
|
+
assert.equal(mapType("string?", "typescript"), "string | undefined");
|
|
104
|
+
});
|
|
105
|
+
it("inferType infers correctly", () => {
|
|
106
|
+
assert.equal(inferType("id", "typescript"), "string");
|
|
107
|
+
assert.equal(inferType("user_id", "typescript"), "string");
|
|
108
|
+
assert.equal(inferType("created_at", "typescript"), "Timestamp");
|
|
109
|
+
assert.equal(inferType("is_active", "typescript"), "boolean");
|
|
110
|
+
assert.equal(inferType("amount", "typescript"), "unknown");
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { mapType, pascalCase, camelCase, snakeCase, kebabCase, createTemplateData, TYPE_MAPPINGS, } from "./types.js";
|
|
2
|
+
export { registerGenerator, getGenerator, getAvailableLanguages, generate, generateAndWrite, filterModules, filterAdapters, } from "./engine.js";
|
|
3
|
+
export { renderTemplate } from "./render.js";
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { pascalCase, camelCase } from "../types.js";
|
|
2
|
+
export function generatePrototype(catalog, adapters, options) {
|
|
3
|
+
const files = [];
|
|
4
|
+
const errors = [];
|
|
5
|
+
try {
|
|
6
|
+
files.push(generatePackageJson(options, adapters));
|
|
7
|
+
files.push(generateTsConfig());
|
|
8
|
+
files.push(generateGitignore());
|
|
9
|
+
files.push(generateReadme(options, adapters));
|
|
10
|
+
files.push(generateEnvExample(adapters));
|
|
11
|
+
files.push(generateConfig(options, adapters, catalog));
|
|
12
|
+
files.push(generateEntryPoint(options, adapters, catalog));
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
errors.push(`Failed to generate prototype: ${error instanceof Error ? error.message : error}`);
|
|
16
|
+
}
|
|
17
|
+
return { files, errors };
|
|
18
|
+
}
|
|
19
|
+
function generatePackageJson(options, adapters) {
|
|
20
|
+
const dependencies = {};
|
|
21
|
+
const devDependencies = {
|
|
22
|
+
"typescript": "^5.0.0",
|
|
23
|
+
"@types/node": "^20.0.0",
|
|
24
|
+
};
|
|
25
|
+
for (const adapterName of Object.values(options.adapters)) {
|
|
26
|
+
const adapter = adapters.find((a) => a.name === adapterName);
|
|
27
|
+
if (adapter) {
|
|
28
|
+
const pkg = getAdapterPackage(adapterName);
|
|
29
|
+
if (pkg) {
|
|
30
|
+
dependencies[pkg.name] = pkg.version;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const packageJson = {
|
|
35
|
+
name: options.name,
|
|
36
|
+
version: "1.0.0",
|
|
37
|
+
type: "module",
|
|
38
|
+
scripts: {
|
|
39
|
+
build: "tsc",
|
|
40
|
+
dev: "tsx src/index.ts",
|
|
41
|
+
},
|
|
42
|
+
dependencies,
|
|
43
|
+
devDependencies,
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
path: "package.json",
|
|
47
|
+
content: JSON.stringify(packageJson, null, 2) + "\n",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function generateTsConfig() {
|
|
51
|
+
const tsConfig = {
|
|
52
|
+
compilerOptions: {
|
|
53
|
+
target: "ES2022",
|
|
54
|
+
module: "NodeNext",
|
|
55
|
+
moduleResolution: "NodeNext",
|
|
56
|
+
strict: true,
|
|
57
|
+
outDir: "dist",
|
|
58
|
+
rootDir: "src",
|
|
59
|
+
esModuleInterop: true,
|
|
60
|
+
skipLibCheck: true,
|
|
61
|
+
},
|
|
62
|
+
include: ["src/**/*.ts"],
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
path: "tsconfig.json",
|
|
66
|
+
content: JSON.stringify(tsConfig, null, 2) + "\n",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function generateGitignore() {
|
|
70
|
+
return {
|
|
71
|
+
path: ".gitignore",
|
|
72
|
+
content: `node_modules/
|
|
73
|
+
dist/
|
|
74
|
+
.env
|
|
75
|
+
*.log
|
|
76
|
+
`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function generateReadme(options, adapters) {
|
|
80
|
+
const moduleList = options.modules.map((m) => `- ${m}`).join("\n");
|
|
81
|
+
const adapterList = Object.entries(options.adapters)
|
|
82
|
+
.map(([module, adapter]) => `- ${module}: ${adapter}`)
|
|
83
|
+
.join("\n");
|
|
84
|
+
return {
|
|
85
|
+
path: "README.md",
|
|
86
|
+
content: `# ${options.name}
|
|
87
|
+
|
|
88
|
+
Generated scaffold from Engineering Blueprinter contracts.
|
|
89
|
+
|
|
90
|
+
## Modules
|
|
91
|
+
|
|
92
|
+
${moduleList}
|
|
93
|
+
|
|
94
|
+
## Adapters
|
|
95
|
+
|
|
96
|
+
${adapterList}
|
|
97
|
+
|
|
98
|
+
## Getting Started
|
|
99
|
+
|
|
100
|
+
\`\`\`bash
|
|
101
|
+
npm install
|
|
102
|
+
npm run build
|
|
103
|
+
npm run dev
|
|
104
|
+
\`\`\`
|
|
105
|
+
|
|
106
|
+
## Project Structure
|
|
107
|
+
|
|
108
|
+
\`\`\`
|
|
109
|
+
src/
|
|
110
|
+
├── interfaces/ # TypeScript interfaces from contracts
|
|
111
|
+
├── adapters/ # Adapter implementations (fill in TODOs)
|
|
112
|
+
├── config/ # Adapter configuration
|
|
113
|
+
└── index.ts # Entry point
|
|
114
|
+
\`\`\`
|
|
115
|
+
|
|
116
|
+
## Next Steps
|
|
117
|
+
|
|
118
|
+
1. Fill in adapter implementations in \`src/adapters/\`
|
|
119
|
+
2. Configure environment variables in \`.env\`
|
|
120
|
+
3. Implement business logic in \`src/index.ts\`
|
|
121
|
+
`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function generateEnvExample(adapters) {
|
|
125
|
+
const lines = ["# Environment Variables", ""];
|
|
126
|
+
for (const adapter of adapters) {
|
|
127
|
+
lines.push(`# ${adapter.description || adapter.name}`);
|
|
128
|
+
for (const field of adapter.config.required) {
|
|
129
|
+
if (field.secret) {
|
|
130
|
+
lines.push(`${field.name.toUpperCase()}=your_${field.name}_here`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push("");
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
path: ".env.example",
|
|
137
|
+
content: lines.join("\n"),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function generateConfig(options, adapters, catalog) {
|
|
141
|
+
const lines = [
|
|
142
|
+
"// Adapter configuration",
|
|
143
|
+
"// Fill in your API keys in .env file",
|
|
144
|
+
"",
|
|
145
|
+
];
|
|
146
|
+
const imports = [];
|
|
147
|
+
const adapterConfigs = [];
|
|
148
|
+
for (const [module, adapterName] of Object.entries(options.adapters)) {
|
|
149
|
+
const adapter = adapters.find((a) => a.name === adapterName && a.module === module);
|
|
150
|
+
if (!adapter)
|
|
151
|
+
continue;
|
|
152
|
+
const className = `${pascalCase(adapterName)}Adapter`;
|
|
153
|
+
const importPath = `../adapters/${module}/${adapterName}`;
|
|
154
|
+
imports.push(`import { ${className} } from '${importPath}';`);
|
|
155
|
+
const configFields = adapter.config.required
|
|
156
|
+
.map((f) => {
|
|
157
|
+
if (f.secret) {
|
|
158
|
+
return ` ${f.name}: process.env.${f.name.toUpperCase()}!,`;
|
|
159
|
+
}
|
|
160
|
+
return ` ${f.name}: process.env.${f.name.toUpperCase()} || '',`;
|
|
161
|
+
})
|
|
162
|
+
.join("\n");
|
|
163
|
+
adapterConfigs.push(`export const ${module}Adapter = new ${className}({`);
|
|
164
|
+
adapterConfigs.push(configFields);
|
|
165
|
+
adapterConfigs.push(`});`);
|
|
166
|
+
adapterConfigs.push("");
|
|
167
|
+
}
|
|
168
|
+
lines.push(...imports);
|
|
169
|
+
lines.push("");
|
|
170
|
+
lines.push(...adapterConfigs);
|
|
171
|
+
return {
|
|
172
|
+
path: "src/config/adapters.ts",
|
|
173
|
+
content: lines.join("\n"),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function generateEntryPoint(options, adapters, catalog) {
|
|
177
|
+
const lines = [
|
|
178
|
+
"// Application entry point",
|
|
179
|
+
"// Implement your business logic here",
|
|
180
|
+
"",
|
|
181
|
+
];
|
|
182
|
+
for (const [module, adapterName] of Object.entries(options.adapters)) {
|
|
183
|
+
const mod = catalog.modules.find((m) => m.name === module);
|
|
184
|
+
if (!mod)
|
|
185
|
+
continue;
|
|
186
|
+
lines.push(`// ${module} functions:`);
|
|
187
|
+
for (const fn of mod.functions.slice(0, 3)) {
|
|
188
|
+
lines.push(`// ${camelCase(fn.name)}(${fn.params.map((p) => p.name).join(', ')})`);
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
}
|
|
192
|
+
lines.push("async function main() {");
|
|
193
|
+
lines.push(" // TODO: Initialize adapters");
|
|
194
|
+
lines.push(" // TODO: Implement business logic");
|
|
195
|
+
lines.push("}");
|
|
196
|
+
lines.push("");
|
|
197
|
+
lines.push("main().catch(console.error);");
|
|
198
|
+
lines.push("");
|
|
199
|
+
return {
|
|
200
|
+
path: "src/index.ts",
|
|
201
|
+
content: lines.join("\n"),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function getAdapterPackage(adapterName) {
|
|
205
|
+
const packageMap = {
|
|
206
|
+
stripe: { name: "stripe", version: "^14.0.0" },
|
|
207
|
+
paystack: { name: "paystack", version: "^3.0.0" },
|
|
208
|
+
adyen: { name: "@adyen/api-library", version: "^15.0.0" },
|
|
209
|
+
redis: { name: "redis", version: "^4.0.0" },
|
|
210
|
+
memcached: { name: "memjs", version: "^1.3.0" },
|
|
211
|
+
resend: { name: "resend", version: "^3.0.0" },
|
|
212
|
+
sendgrid: { name: "@sendgrid/mail", version: "^8.0.0" },
|
|
213
|
+
twilio: { name: "twilio", version: "^5.0.0" },
|
|
214
|
+
bullmq: { name: "bullmq", version: "^5.0.0" },
|
|
215
|
+
sqs: { name: "@aws-sdk/client-sqs", version: "^3.0.0" },
|
|
216
|
+
algolia: { name: "algoliasearch", version: "^5.0.0" },
|
|
217
|
+
sentry: { name: "@sentry/node", version: "^8.0.0" },
|
|
218
|
+
clerk: { name: "@clerk/clerk-sdk-node", version: "^2.0.0" },
|
|
219
|
+
auth0: { name: "auth0", version: "^4.0.0" },
|
|
220
|
+
launchdarkly: { name: "launchdarkly-node-server-sdk", version: "^8.0.0" },
|
|
221
|
+
flagsmith: { name: "flagsmith-nodejs", version: "^3.0.0" },
|
|
222
|
+
unleash: { name: "@unleash/nextjs", version: "^5.0.0" },
|
|
223
|
+
mixpanel: { name: "mixpanel", version: "^0.18.0" },
|
|
224
|
+
segment: { name: "@segment/analytics-node", version: "^2.0.0" },
|
|
225
|
+
amplitude: { name: "@amplitude/analytics-node", version: "^2.0.0" },
|
|
226
|
+
sift: { name: "sift", version: "^16.0.0" },
|
|
227
|
+
bugsnag: { name: "@bugsnag/node", version: "^8.0.0" },
|
|
228
|
+
pagerduty: { name: "pagerduty", version: "^3.0.0" },
|
|
229
|
+
cloudinary: { name: "cloudinary", version: "^2.0.0" },
|
|
230
|
+
zendesk: { name: "node-zendesk", version: "^3.0.0" },
|
|
231
|
+
hubspot: { name: "@hubspot/api-client", version: "^10.0.0" },
|
|
232
|
+
linear: { name: "@linear/sdk", version: "^12.0.0" },
|
|
233
|
+
jira: { name: "jira.js", version: "^4.0.0" },
|
|
234
|
+
shipengine: { name: "@shipengine/sdk", version: "^2.0.0" },
|
|
235
|
+
taxjar: { name: "taxjar", version: "^3.0.0" },
|
|
236
|
+
datadog: { name: "datadog-api-client", version: "^2.0.0" },
|
|
237
|
+
jaeger: { name: "@opentelemetry/sdk-node", version: "^1.0.0" },
|
|
238
|
+
plaid: { name: "plaid", version: "^30.0.0" },
|
|
239
|
+
svix: { name: "svix", version: "^1.0.0" },
|
|
240
|
+
};
|
|
241
|
+
return packageMap[adapterName] || null;
|
|
242
|
+
}
|