@secondlayer/cli 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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/cli.cjs +897 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +874 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/plugin-manager.cjs +368 -0
- package/dist/core/plugin-manager.cjs.map +1 -0
- package/dist/core/plugin-manager.d.cts +1 -0
- package/dist/core/plugin-manager.d.ts +1 -0
- package/dist/core/plugin-manager.js +333 -0
- package/dist/core/plugin-manager.js.map +1 -0
- package/dist/index.cjs +380 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +8 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +342 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-manager-DBXFfyFZ.d.cts +285 -0
- package/dist/plugin-manager-DBXFfyFZ.d.ts +285 -0
- package/dist/plugins/index.cjs +2622 -0
- package/dist/plugins/index.cjs.map +1 -0
- package/dist/plugins/index.d.cts +136 -0
- package/dist/plugins/index.d.ts +136 -0
- package/dist/plugins/index.js +2578 -0
- package/dist/plugins/index.js.map +1 -0
- package/package.json +71 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/commands/init.ts
|
|
22
|
+
var init_exports = {};
|
|
23
|
+
__export(init_exports, {
|
|
24
|
+
init: () => init
|
|
25
|
+
});
|
|
26
|
+
import { promises as fs3 } from "fs";
|
|
27
|
+
import path4 from "path";
|
|
28
|
+
import ora2 from "ora";
|
|
29
|
+
async function init() {
|
|
30
|
+
const spinner = ora2("Initializing").start();
|
|
31
|
+
const configPath = path4.join(process.cwd(), "stacks.config.ts");
|
|
32
|
+
try {
|
|
33
|
+
await fs3.access(configPath);
|
|
34
|
+
spinner.warn("stacks.config.ts already exists");
|
|
35
|
+
return;
|
|
36
|
+
} catch {
|
|
37
|
+
}
|
|
38
|
+
const hasClarinetProject = await fileExists("./Clarinet.toml");
|
|
39
|
+
let config;
|
|
40
|
+
if (hasClarinetProject) {
|
|
41
|
+
config = `import { defineConfig } from '@stacks/codegen';
|
|
42
|
+
import { clarinet } from '@stacks/codegen/plugins';
|
|
43
|
+
|
|
44
|
+
export default defineConfig({
|
|
45
|
+
out: './src/generated/contracts.ts',
|
|
46
|
+
plugins: [
|
|
47
|
+
clarinet() // Found Clarinet.toml in current directory
|
|
48
|
+
]
|
|
49
|
+
});`;
|
|
50
|
+
} else {
|
|
51
|
+
config = `import { defineConfig } from '@stacks/codegen';
|
|
52
|
+
|
|
53
|
+
export default defineConfig({
|
|
54
|
+
out: './src/generated/contracts.ts',
|
|
55
|
+
plugins: [],
|
|
56
|
+
});`;
|
|
57
|
+
}
|
|
58
|
+
await fs3.writeFile(configPath, config);
|
|
59
|
+
spinner.succeed("Created `stacks.config.ts`");
|
|
60
|
+
console.log(
|
|
61
|
+
"\nRun `codegen generate` to generate type-safe interfaces, functions, and hooks!"
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
async function fileExists(filePath) {
|
|
65
|
+
try {
|
|
66
|
+
await fs3.access(filePath);
|
|
67
|
+
return true;
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var init_init = __esm({
|
|
73
|
+
"src/commands/init.ts"() {
|
|
74
|
+
"use strict";
|
|
75
|
+
init_esm_shims();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// src/cli.ts
|
|
80
|
+
init_esm_shims();
|
|
81
|
+
import { program } from "commander";
|
|
82
|
+
|
|
83
|
+
// src/commands/generate.ts
|
|
84
|
+
init_esm_shims();
|
|
85
|
+
import chalk from "chalk";
|
|
86
|
+
import ora from "ora";
|
|
87
|
+
|
|
88
|
+
// src/utils/config.ts
|
|
89
|
+
init_esm_shims();
|
|
90
|
+
import { promises as fs2 } from "fs";
|
|
91
|
+
import path3 from "path";
|
|
92
|
+
import { pathToFileURL } from "url";
|
|
93
|
+
import { createRequire } from "module";
|
|
94
|
+
import { transformSync } from "esbuild";
|
|
95
|
+
|
|
96
|
+
// src/core/plugin-manager.ts
|
|
97
|
+
init_esm_shims();
|
|
98
|
+
import { format } from "prettier";
|
|
99
|
+
import { promises as fs } from "fs";
|
|
100
|
+
import path2 from "path";
|
|
101
|
+
import { validateStacksAddress } from "@stacks/transactions";
|
|
102
|
+
var PluginManager = class {
|
|
103
|
+
constructor() {
|
|
104
|
+
this.plugins = [];
|
|
105
|
+
this.logger = this.createLogger();
|
|
106
|
+
this.utils = this.createUtils();
|
|
107
|
+
this.executionContext = {
|
|
108
|
+
phase: "config",
|
|
109
|
+
startTime: Date.now(),
|
|
110
|
+
results: /* @__PURE__ */ new Map()
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Register a plugin
|
|
115
|
+
*/
|
|
116
|
+
register(plugin) {
|
|
117
|
+
if (!plugin.name || !plugin.version) {
|
|
118
|
+
throw new Error("Plugin must have a name and version");
|
|
119
|
+
}
|
|
120
|
+
const existing = this.plugins.find((p) => p.name === plugin.name);
|
|
121
|
+
if (existing) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`Plugin "${plugin.name}" is already registered (version ${existing.version})`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
this.plugins.push(plugin);
|
|
127
|
+
this.logger.debug(`Registered plugin: ${plugin.name}@${plugin.version}`);
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Get all registered plugins
|
|
131
|
+
*/
|
|
132
|
+
getPlugins() {
|
|
133
|
+
return [...this.plugins];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Transform user config through all plugins
|
|
137
|
+
*/
|
|
138
|
+
async transformConfig(config) {
|
|
139
|
+
this.executionContext.phase = "config";
|
|
140
|
+
let transformedConfig = { ...config };
|
|
141
|
+
for (const plugin of this.plugins) {
|
|
142
|
+
if (plugin.transformConfig) {
|
|
143
|
+
this.executionContext.currentPlugin = plugin;
|
|
144
|
+
try {
|
|
145
|
+
const result = await plugin.transformConfig(transformedConfig);
|
|
146
|
+
transformedConfig = result;
|
|
147
|
+
this.recordHookResult(plugin.name, "transformConfig", {
|
|
148
|
+
success: true
|
|
149
|
+
});
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const err = error;
|
|
152
|
+
this.recordHookResult(plugin.name, "transformConfig", {
|
|
153
|
+
success: false,
|
|
154
|
+
error: err
|
|
155
|
+
});
|
|
156
|
+
throw new Error(
|
|
157
|
+
`Plugin "${plugin.name}" failed during config transformation: ${err.message}`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const resolvedConfig = {
|
|
163
|
+
...transformedConfig,
|
|
164
|
+
plugins: this.plugins
|
|
165
|
+
};
|
|
166
|
+
return resolvedConfig;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Transform contracts through all plugins
|
|
170
|
+
*/
|
|
171
|
+
async transformContracts(contracts, _config) {
|
|
172
|
+
const processedContracts = [];
|
|
173
|
+
for (let contract of contracts) {
|
|
174
|
+
if (contract._clarinetSource && contract.abi) {
|
|
175
|
+
const address = typeof contract.address === "string" ? contract.address : "";
|
|
176
|
+
const [contractAddress, contractName] = address.split(".");
|
|
177
|
+
const processed = {
|
|
178
|
+
name: contract.name || contractName,
|
|
179
|
+
address: contractAddress,
|
|
180
|
+
contractName,
|
|
181
|
+
abi: contract.abi,
|
|
182
|
+
source: "local",
|
|
183
|
+
metadata: { source: "clarinet" }
|
|
184
|
+
};
|
|
185
|
+
processedContracts.push(processed);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
for (const plugin of this.plugins) {
|
|
189
|
+
if (plugin.transformContract) {
|
|
190
|
+
this.executionContext.currentPlugin = plugin;
|
|
191
|
+
try {
|
|
192
|
+
contract = await plugin.transformContract(contract);
|
|
193
|
+
this.recordHookResult(plugin.name, "transformContract", {
|
|
194
|
+
success: true
|
|
195
|
+
});
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const err = error;
|
|
198
|
+
this.recordHookResult(plugin.name, "transformContract", {
|
|
199
|
+
success: false,
|
|
200
|
+
error: err
|
|
201
|
+
});
|
|
202
|
+
this.logger.warn(
|
|
203
|
+
`Plugin "${plugin.name}" failed to transform contract: ${err.message}`
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (contract.abi) {
|
|
209
|
+
const processed = {
|
|
210
|
+
name: contract.name || "unknown",
|
|
211
|
+
address: typeof contract.address === "string" ? contract.address.split(".")[0] : "unknown",
|
|
212
|
+
contractName: contract.name || "unknown",
|
|
213
|
+
abi: contract.abi,
|
|
214
|
+
source: "api",
|
|
215
|
+
// Use "api" as default for plugin-processed contracts
|
|
216
|
+
metadata: contract.metadata
|
|
217
|
+
};
|
|
218
|
+
processedContracts.push(processed);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return processedContracts;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Execute lifecycle hooks
|
|
225
|
+
*/
|
|
226
|
+
async executeHook(hookName, context) {
|
|
227
|
+
for (const plugin of this.plugins) {
|
|
228
|
+
const hook = plugin[hookName];
|
|
229
|
+
if (typeof hook === "function") {
|
|
230
|
+
this.executionContext.currentPlugin = plugin;
|
|
231
|
+
try {
|
|
232
|
+
await hook.call(plugin, context);
|
|
233
|
+
this.recordHookResult(plugin.name, hookName, {
|
|
234
|
+
success: true
|
|
235
|
+
});
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const err = error;
|
|
238
|
+
this.recordHookResult(plugin.name, hookName, {
|
|
239
|
+
success: false,
|
|
240
|
+
error: err
|
|
241
|
+
});
|
|
242
|
+
this.logger.error(
|
|
243
|
+
`Plugin "${plugin.name}" failed during ${hookName}: ${err.message}`
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Execute generation phase with full context
|
|
251
|
+
*/
|
|
252
|
+
async executeGeneration(contracts, config) {
|
|
253
|
+
this.executionContext.phase = "generate";
|
|
254
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
255
|
+
const context = {
|
|
256
|
+
config,
|
|
257
|
+
logger: this.logger,
|
|
258
|
+
utils: this.utils,
|
|
259
|
+
contracts,
|
|
260
|
+
outputs,
|
|
261
|
+
augment: (outputKey, contractName, content) => {
|
|
262
|
+
this.augmentOutput(outputs, outputKey, contractName, content);
|
|
263
|
+
},
|
|
264
|
+
addOutput: (key, output) => {
|
|
265
|
+
outputs.set(key, output);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
await this.executeHook("beforeGenerate", context);
|
|
269
|
+
await this.executeHook("generate", context);
|
|
270
|
+
await this.executeHook("afterGenerate", context);
|
|
271
|
+
return outputs;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Transform outputs through plugins
|
|
275
|
+
*/
|
|
276
|
+
async transformOutputs(outputs) {
|
|
277
|
+
this.executionContext.phase = "output";
|
|
278
|
+
const transformedOutputs = /* @__PURE__ */ new Map();
|
|
279
|
+
for (const [key, output] of outputs) {
|
|
280
|
+
let transformedContent = output.content;
|
|
281
|
+
for (const plugin of this.plugins) {
|
|
282
|
+
if (plugin.transformOutput) {
|
|
283
|
+
this.executionContext.currentPlugin = plugin;
|
|
284
|
+
try {
|
|
285
|
+
transformedContent = await plugin.transformOutput(
|
|
286
|
+
transformedContent,
|
|
287
|
+
output.type || "other"
|
|
288
|
+
);
|
|
289
|
+
this.recordHookResult(plugin.name, "transformOutput", {
|
|
290
|
+
success: true
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const err = error;
|
|
294
|
+
this.recordHookResult(plugin.name, "transformOutput", {
|
|
295
|
+
success: false,
|
|
296
|
+
error: err
|
|
297
|
+
});
|
|
298
|
+
this.logger.warn(
|
|
299
|
+
`Plugin "${plugin.name}" failed to transform output: ${err.message}`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
transformedOutputs.set(key, {
|
|
305
|
+
...output,
|
|
306
|
+
content: transformedContent
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return transformedOutputs;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Write outputs to disk
|
|
313
|
+
*/
|
|
314
|
+
async writeOutputs(outputs) {
|
|
315
|
+
for (const [, output] of outputs) {
|
|
316
|
+
try {
|
|
317
|
+
const resolvedPath = path2.resolve(process.cwd(), output.path);
|
|
318
|
+
await this.utils.ensureDir(path2.dirname(resolvedPath));
|
|
319
|
+
await this.utils.writeFile(resolvedPath, output.content);
|
|
320
|
+
} catch (error) {
|
|
321
|
+
const err = error;
|
|
322
|
+
this.logger.error(`Failed to write ${output.path}: ${err.message}`);
|
|
323
|
+
throw err;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Get execution results for debugging
|
|
329
|
+
*/
|
|
330
|
+
getExecutionResults() {
|
|
331
|
+
return new Map(this.executionContext.results);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Augment existing output with additional content
|
|
335
|
+
*/
|
|
336
|
+
augmentOutput(outputs, outputKey, contractName, content) {
|
|
337
|
+
const existing = outputs.get(outputKey);
|
|
338
|
+
if (!existing) {
|
|
339
|
+
this.logger.warn(`Cannot augment non-existent output: ${outputKey}`);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const augmentedContent = `${existing.content}
|
|
343
|
+
|
|
344
|
+
// Augmented by plugin for ${contractName}
|
|
345
|
+
${JSON.stringify(content, null, 2)}`;
|
|
346
|
+
outputs.set(outputKey, {
|
|
347
|
+
...existing,
|
|
348
|
+
content: augmentedContent
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* Record hook execution result
|
|
353
|
+
*/
|
|
354
|
+
recordHookResult(pluginName, hookName, result) {
|
|
355
|
+
const key = `${pluginName}:${hookName}`;
|
|
356
|
+
const existing = this.executionContext.results.get(key) || [];
|
|
357
|
+
existing.push({ ...result, plugin: pluginName });
|
|
358
|
+
this.executionContext.results.set(key, existing);
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Create logger instance
|
|
362
|
+
*/
|
|
363
|
+
createLogger() {
|
|
364
|
+
return {
|
|
365
|
+
info: (message) => console.log(`\u2139\uFE0F ${message}`),
|
|
366
|
+
warn: (message) => console.warn(`\u26A0\uFE0F ${message}`),
|
|
367
|
+
error: (message) => console.error(`\u274C ${message}`),
|
|
368
|
+
debug: (message) => {
|
|
369
|
+
if (process.env.DEBUG) {
|
|
370
|
+
console.log(`\u{1F41B} ${message}`);
|
|
371
|
+
}
|
|
372
|
+
},
|
|
373
|
+
success: (message) => console.log(`\u2705 ${message}`)
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Create utils instance
|
|
378
|
+
*/
|
|
379
|
+
createUtils() {
|
|
380
|
+
return {
|
|
381
|
+
toCamelCase: (str) => {
|
|
382
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
383
|
+
},
|
|
384
|
+
toKebabCase: (str) => {
|
|
385
|
+
return str.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
|
|
386
|
+
},
|
|
387
|
+
validateAddress: (address) => {
|
|
388
|
+
return validateStacksAddress(address.split(".")[0]);
|
|
389
|
+
},
|
|
390
|
+
parseContractId: (contractId) => {
|
|
391
|
+
const [address, contractName] = contractId.split(".");
|
|
392
|
+
return { address, contractName };
|
|
393
|
+
},
|
|
394
|
+
formatCode: async (code) => {
|
|
395
|
+
return format(code, {
|
|
396
|
+
parser: "typescript",
|
|
397
|
+
singleQuote: true,
|
|
398
|
+
semi: true,
|
|
399
|
+
printWidth: 100,
|
|
400
|
+
trailingComma: "es5"
|
|
401
|
+
});
|
|
402
|
+
},
|
|
403
|
+
resolvePath: (relativePath) => {
|
|
404
|
+
return path2.resolve(process.cwd(), relativePath);
|
|
405
|
+
},
|
|
406
|
+
fileExists: async (filePath) => {
|
|
407
|
+
try {
|
|
408
|
+
await fs.access(filePath);
|
|
409
|
+
return true;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
},
|
|
414
|
+
readFile: async (filePath) => {
|
|
415
|
+
return fs.readFile(filePath, "utf-8");
|
|
416
|
+
},
|
|
417
|
+
writeFile: async (filePath, content) => {
|
|
418
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
419
|
+
},
|
|
420
|
+
ensureDir: async (dirPath) => {
|
|
421
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
// src/utils/config.ts
|
|
428
|
+
var CONFIG_FILE_NAMES = [
|
|
429
|
+
"stacks.config.ts",
|
|
430
|
+
"stacks.config.js",
|
|
431
|
+
"stacks.config.mjs"
|
|
432
|
+
];
|
|
433
|
+
async function findConfigFile(cwd) {
|
|
434
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
435
|
+
const filePath = path3.join(cwd, fileName);
|
|
436
|
+
try {
|
|
437
|
+
await fs2.access(filePath);
|
|
438
|
+
return filePath;
|
|
439
|
+
} catch {
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
async function loadConfig(configPath) {
|
|
445
|
+
const cwd = process.cwd();
|
|
446
|
+
const resolvedPath = configPath ? path3.resolve(cwd, configPath) : await findConfigFile(cwd);
|
|
447
|
+
if (!resolvedPath) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
"No config file found. Create a stacks.config.ts file or specify a path with --config"
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
let config;
|
|
453
|
+
if (resolvedPath.endsWith(".ts")) {
|
|
454
|
+
const code = await fs2.readFile(resolvedPath, "utf-8");
|
|
455
|
+
let replacementPath;
|
|
456
|
+
try {
|
|
457
|
+
const require2 = createRequire(import.meta.url);
|
|
458
|
+
const packagePath = require2.resolve("@stacks/codegen");
|
|
459
|
+
replacementPath = pathToFileURL(packagePath).href;
|
|
460
|
+
} catch {
|
|
461
|
+
const currentModuleDir = path3.dirname(new URL(import.meta.url).pathname);
|
|
462
|
+
const indexPath = path3.resolve(currentModuleDir, "../index.js");
|
|
463
|
+
replacementPath = pathToFileURL(indexPath).href;
|
|
464
|
+
}
|
|
465
|
+
const transformedCode = code.replace(
|
|
466
|
+
/from\s+["']@stacks\/cli["']/g,
|
|
467
|
+
`from '${replacementPath}'`
|
|
468
|
+
);
|
|
469
|
+
const result = transformSync(transformedCode, {
|
|
470
|
+
format: "esm",
|
|
471
|
+
target: "node18",
|
|
472
|
+
loader: "ts"
|
|
473
|
+
});
|
|
474
|
+
const tempPath = resolvedPath.replace(/\.ts$/, ".mjs");
|
|
475
|
+
await fs2.writeFile(tempPath, result.code);
|
|
476
|
+
try {
|
|
477
|
+
const fileUrl = pathToFileURL(tempPath).href;
|
|
478
|
+
const module = await import(fileUrl);
|
|
479
|
+
config = module.default;
|
|
480
|
+
} finally {
|
|
481
|
+
await fs2.unlink(tempPath).catch(() => {
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
} else {
|
|
485
|
+
const fileUrl = pathToFileURL(resolvedPath).href;
|
|
486
|
+
const module = await import(fileUrl);
|
|
487
|
+
config = module.default;
|
|
488
|
+
}
|
|
489
|
+
if (!config) {
|
|
490
|
+
throw new Error("Config file must export a default configuration");
|
|
491
|
+
}
|
|
492
|
+
if (typeof config === "function") {
|
|
493
|
+
config = config({});
|
|
494
|
+
}
|
|
495
|
+
validateConfig(config);
|
|
496
|
+
const pluginManager = new PluginManager();
|
|
497
|
+
if (config.plugins && Array.isArray(config.plugins)) {
|
|
498
|
+
for (const plugin of config.plugins) {
|
|
499
|
+
pluginManager.register(plugin);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const resolvedConfig = await pluginManager.transformConfig(config);
|
|
503
|
+
return resolvedConfig;
|
|
504
|
+
}
|
|
505
|
+
function validateConfig(config) {
|
|
506
|
+
if (!config || typeof config !== "object") {
|
|
507
|
+
throw new Error("Config must be an object");
|
|
508
|
+
}
|
|
509
|
+
const c = config;
|
|
510
|
+
if (c.contracts && !Array.isArray(c.contracts)) {
|
|
511
|
+
throw new Error("Config contracts must be an array");
|
|
512
|
+
}
|
|
513
|
+
if (!c.out || typeof c.out !== "string") {
|
|
514
|
+
throw new Error("Config out must be a string path");
|
|
515
|
+
}
|
|
516
|
+
if (c.contracts) {
|
|
517
|
+
for (const contract of c.contracts) {
|
|
518
|
+
if (!contract.address && !contract.source) {
|
|
519
|
+
throw new Error("Each contract must have either an address or source");
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
if (c.plugins && !Array.isArray(c.plugins)) {
|
|
524
|
+
throw new Error("Config plugins must be an array");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/utils/api.ts
|
|
529
|
+
init_esm_shims();
|
|
530
|
+
import got from "got";
|
|
531
|
+
|
|
532
|
+
// src/parsers/clarity.ts
|
|
533
|
+
init_esm_shims();
|
|
534
|
+
|
|
535
|
+
// src/generators/contract.ts
|
|
536
|
+
init_esm_shims();
|
|
537
|
+
import { format as format2 } from "prettier";
|
|
538
|
+
async function generateContractInterface(contracts) {
|
|
539
|
+
const imports = `import { Cl, validateStacksAddress } from '@stacks/transactions'`;
|
|
540
|
+
const header = `/**
|
|
541
|
+
* Generated by @stacks/codegen
|
|
542
|
+
* DO NOT EDIT MANUALLY
|
|
543
|
+
*/`;
|
|
544
|
+
const contractsCode = contracts.map((contract) => generateContract(contract)).join("\n\n");
|
|
545
|
+
const code = `${imports}
|
|
546
|
+
|
|
547
|
+
${header}
|
|
548
|
+
|
|
549
|
+
${contractsCode}`;
|
|
550
|
+
const formatted = await format2(code, {
|
|
551
|
+
parser: "typescript",
|
|
552
|
+
singleQuote: true,
|
|
553
|
+
semi: true,
|
|
554
|
+
printWidth: 100,
|
|
555
|
+
trailingComma: "es5"
|
|
556
|
+
});
|
|
557
|
+
return formatted;
|
|
558
|
+
}
|
|
559
|
+
function generateContract(contract) {
|
|
560
|
+
const { name, address, contractName, abi } = contract;
|
|
561
|
+
const abiCode = generateAbiConstant(name, abi);
|
|
562
|
+
const methods = abi.functions.filter((func) => func.access !== "private").map((func) => generateMethod(func, address, contractName)).join(",\n\n ");
|
|
563
|
+
const contractCode = `export const ${name} = {
|
|
564
|
+
address: '${address}',
|
|
565
|
+
contractAddress: '${address}',
|
|
566
|
+
contractName: '${contractName}',
|
|
567
|
+
|
|
568
|
+
${methods}
|
|
569
|
+
} as const`;
|
|
570
|
+
return `${abiCode}
|
|
571
|
+
|
|
572
|
+
${contractCode}`;
|
|
573
|
+
}
|
|
574
|
+
function generateAbiConstant(name, abi) {
|
|
575
|
+
const abiJson = JSON.stringify(abi, null, 2).replace(/"([a-zA-Z_$][a-zA-Z0-9_$]*)":/g, "$1:").replace(/"/g, "'");
|
|
576
|
+
return `export const ${name}Abi = ${abiJson} as const`;
|
|
577
|
+
}
|
|
578
|
+
function generateMethod(func, address, contractName) {
|
|
579
|
+
const methodName = toCamelCase(func.name);
|
|
580
|
+
if (func.args.length === 0) {
|
|
581
|
+
return `${methodName}() {
|
|
582
|
+
return {
|
|
583
|
+
contractAddress: '${address}',
|
|
584
|
+
contractName: '${contractName}',
|
|
585
|
+
functionName: '${func.name}',
|
|
586
|
+
functionArgs: []
|
|
587
|
+
}
|
|
588
|
+
}`;
|
|
589
|
+
}
|
|
590
|
+
if (func.args.length === 1) {
|
|
591
|
+
const originalArgName = func.args[0].name;
|
|
592
|
+
const argName = toCamelCase(originalArgName);
|
|
593
|
+
const argType = getTypeForArg(func.args[0]);
|
|
594
|
+
const clarityConversion = generateClarityConversion(argName, func.args[0]);
|
|
595
|
+
return `${methodName}(...args: [{ ${argName}: ${argType} }] | [${argType}]) {
|
|
596
|
+
const ${argName} = args.length === 1 && typeof args[0] === 'object' && args[0] !== null && '${argName}' in args[0]
|
|
597
|
+
? args[0].${argName}
|
|
598
|
+
: args[0] as ${argType}
|
|
599
|
+
|
|
600
|
+
return {
|
|
601
|
+
contractAddress: '${address}',
|
|
602
|
+
contractName: '${contractName}',
|
|
603
|
+
functionName: '${func.name}',
|
|
604
|
+
functionArgs: [${clarityConversion}]
|
|
605
|
+
}
|
|
606
|
+
}`;
|
|
607
|
+
}
|
|
608
|
+
const argsList = func.args.map((arg) => toCamelCase(arg.name)).join(", ");
|
|
609
|
+
const argsTypes = func.args.map((arg) => {
|
|
610
|
+
const camelName = toCamelCase(arg.name);
|
|
611
|
+
return `${camelName}: ${getTypeForArg(arg)}`;
|
|
612
|
+
}).join("; ");
|
|
613
|
+
const argsArray = func.args.map((arg) => {
|
|
614
|
+
const argName = toCamelCase(arg.name);
|
|
615
|
+
return generateClarityConversion(argName, arg);
|
|
616
|
+
}).join(", ");
|
|
617
|
+
const objectAccess = func.args.map((arg) => {
|
|
618
|
+
const camelName = toCamelCase(arg.name);
|
|
619
|
+
return `args[0].${camelName}`;
|
|
620
|
+
}).join(", ");
|
|
621
|
+
const positionTypes = func.args.map((arg) => getTypeForArg(arg)).join(", ");
|
|
622
|
+
return `${methodName}(...args: [{ ${argsTypes} }] | [${positionTypes}]) {
|
|
623
|
+
const [${argsList}] = args.length === 1 && typeof args[0] === 'object' && args[0] !== null
|
|
624
|
+
? [${objectAccess}]
|
|
625
|
+
: args as [${positionTypes}]
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
contractAddress: '${address}',
|
|
629
|
+
contractName: '${contractName}',
|
|
630
|
+
functionName: '${func.name}',
|
|
631
|
+
functionArgs: [${argsArray}]
|
|
632
|
+
}
|
|
633
|
+
}`;
|
|
634
|
+
}
|
|
635
|
+
function getTypeForArg(arg) {
|
|
636
|
+
const type = arg.type;
|
|
637
|
+
if (typeof type === "string") {
|
|
638
|
+
switch (type) {
|
|
639
|
+
case "uint128":
|
|
640
|
+
case "int128":
|
|
641
|
+
return "bigint";
|
|
642
|
+
case "bool":
|
|
643
|
+
return "boolean";
|
|
644
|
+
case "principal":
|
|
645
|
+
case "trait_reference":
|
|
646
|
+
return "string";
|
|
647
|
+
default:
|
|
648
|
+
return "any";
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
if (type["string-ascii"] || type["string-utf8"]) {
|
|
652
|
+
return "string";
|
|
653
|
+
}
|
|
654
|
+
if (type.buff) {
|
|
655
|
+
return "Uint8Array | string | { type: 'ascii' | 'utf8' | 'hex'; value: string }";
|
|
656
|
+
}
|
|
657
|
+
if (type.optional) {
|
|
658
|
+
const innerType = getTypeForArg({ type: type.optional });
|
|
659
|
+
return `${innerType} | null`;
|
|
660
|
+
}
|
|
661
|
+
if (type.list) {
|
|
662
|
+
const innerType = getTypeForArg({ type: type.list.type });
|
|
663
|
+
return `${innerType}[]`;
|
|
664
|
+
}
|
|
665
|
+
if (type.tuple) {
|
|
666
|
+
const fields = type.tuple.map(
|
|
667
|
+
(field) => `${toCamelCase(field.name)}: ${getTypeForArg({ type: field.type })}`
|
|
668
|
+
).join("; ");
|
|
669
|
+
return `{ ${fields} }`;
|
|
670
|
+
}
|
|
671
|
+
if (type.response) {
|
|
672
|
+
const okType = getTypeForArg({ type: type.response.ok });
|
|
673
|
+
const errType = getTypeForArg({ type: type.response.error });
|
|
674
|
+
return `{ ok: ${okType} } | { err: ${errType} }`;
|
|
675
|
+
}
|
|
676
|
+
return "any";
|
|
677
|
+
}
|
|
678
|
+
function toCamelCase(str) {
|
|
679
|
+
return str.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()).replace(/-([A-Z])/g, (_, letter) => letter).replace(/-(\d)/g, (_, digit) => digit).replace(/-/g, "");
|
|
680
|
+
}
|
|
681
|
+
function generateClarityConversion(argName, argType) {
|
|
682
|
+
const type = argType.type;
|
|
683
|
+
if (typeof type === "string") {
|
|
684
|
+
switch (type) {
|
|
685
|
+
case "uint128":
|
|
686
|
+
return `Cl.uint(${argName})`;
|
|
687
|
+
case "int128":
|
|
688
|
+
return `Cl.int(${argName})`;
|
|
689
|
+
case "bool":
|
|
690
|
+
return `Cl.bool(${argName})`;
|
|
691
|
+
case "principal":
|
|
692
|
+
case "trait_reference":
|
|
693
|
+
return `(() => {
|
|
694
|
+
const [address, contractName] = ${argName}.split(".") as [string, string];
|
|
695
|
+
if (!validateStacksAddress(address)) {
|
|
696
|
+
throw new Error("Invalid Stacks address format");
|
|
697
|
+
}
|
|
698
|
+
if (${argName}.includes(".")) {
|
|
699
|
+
return Cl.contractPrincipal(address, contractName);
|
|
700
|
+
} else {
|
|
701
|
+
return Cl.standardPrincipal(${argName});
|
|
702
|
+
}
|
|
703
|
+
})()`;
|
|
704
|
+
default:
|
|
705
|
+
return `${argName}`;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (type["string-ascii"]) {
|
|
709
|
+
return `Cl.stringAscii(${argName})`;
|
|
710
|
+
}
|
|
711
|
+
if (type["string-utf8"]) {
|
|
712
|
+
return `Cl.stringUtf8(${argName})`;
|
|
713
|
+
}
|
|
714
|
+
if (type.buff) {
|
|
715
|
+
return `(() => {
|
|
716
|
+
const value = ${argName};
|
|
717
|
+
// Direct Uint8Array
|
|
718
|
+
if (value instanceof Uint8Array) {
|
|
719
|
+
return Cl.buffer(value);
|
|
720
|
+
}
|
|
721
|
+
// Object notation with explicit type
|
|
722
|
+
if (typeof value === 'object' && value !== null && value.type && value.value) {
|
|
723
|
+
switch (value.type) {
|
|
724
|
+
case 'ascii':
|
|
725
|
+
return Cl.bufferFromAscii(value.value);
|
|
726
|
+
case 'utf8':
|
|
727
|
+
return Cl.bufferFromUtf8(value.value);
|
|
728
|
+
case 'hex':
|
|
729
|
+
return Cl.bufferFromHex(value.value);
|
|
730
|
+
default:
|
|
731
|
+
throw new Error(\`Unsupported buffer type: \${value.type}\`);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
// Auto-detect string type
|
|
735
|
+
if (typeof value === 'string') {
|
|
736
|
+
// Check for hex (0x prefix or pure hex pattern)
|
|
737
|
+
if (value.startsWith('0x') || /^[0-9a-fA-F]+$/.test(value)) {
|
|
738
|
+
return Cl.bufferFromHex(value);
|
|
739
|
+
}
|
|
740
|
+
// Check for non-ASCII characters (UTF-8)
|
|
741
|
+
if (!/^[\\x00-\\x7F]*$/.test(value)) {
|
|
742
|
+
return Cl.bufferFromUtf8(value);
|
|
743
|
+
}
|
|
744
|
+
// Default to ASCII for simple ASCII strings
|
|
745
|
+
return Cl.bufferFromAscii(value);
|
|
746
|
+
}
|
|
747
|
+
throw new Error(\`Invalid buffer value: \${value}\`);
|
|
748
|
+
})()`;
|
|
749
|
+
}
|
|
750
|
+
if (type.optional) {
|
|
751
|
+
const innerConversion = generateClarityConversion(argName, {
|
|
752
|
+
type: type.optional
|
|
753
|
+
});
|
|
754
|
+
return `${argName} !== null ? Cl.some(${innerConversion.replace(argName, `${argName}`)}) : Cl.none()`;
|
|
755
|
+
}
|
|
756
|
+
if (type.list) {
|
|
757
|
+
const innerConversion = generateClarityConversion("item", {
|
|
758
|
+
type: type.list.type
|
|
759
|
+
});
|
|
760
|
+
return `Cl.list(${argName}.map(item => ${innerConversion}))`;
|
|
761
|
+
}
|
|
762
|
+
if (type.tuple) {
|
|
763
|
+
const fields = type.tuple.map((field) => {
|
|
764
|
+
const camelFieldName = toCamelCase(field.name);
|
|
765
|
+
const fieldConversion = generateClarityConversion(
|
|
766
|
+
`${argName}.${camelFieldName}`,
|
|
767
|
+
{ type: field.type }
|
|
768
|
+
);
|
|
769
|
+
return `"${field.name}": ${fieldConversion}`;
|
|
770
|
+
}).join(", ");
|
|
771
|
+
return `Cl.tuple({ ${fields} })`;
|
|
772
|
+
}
|
|
773
|
+
if (type.response) {
|
|
774
|
+
const okConversion = generateClarityConversion(`${argName}.ok`, {
|
|
775
|
+
type: type.response.ok
|
|
776
|
+
});
|
|
777
|
+
const errConversion = generateClarityConversion(`${argName}.err`, {
|
|
778
|
+
type: type.response.error
|
|
779
|
+
});
|
|
780
|
+
return `'ok' in ${argName} ? Cl.ok(${okConversion.replace(`${argName}.ok`, `${argName}.ok`)}) : Cl.error(${errConversion.replace(`${argName}.err`, `${argName}.err`)})`;
|
|
781
|
+
}
|
|
782
|
+
return `${argName}`;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// src/commands/generate.ts
|
|
786
|
+
async function generate(options) {
|
|
787
|
+
const spinner = ora("Processing contracts").start();
|
|
788
|
+
try {
|
|
789
|
+
const config = await loadConfig(options.config);
|
|
790
|
+
const pluginManager = new PluginManager();
|
|
791
|
+
if (config.plugins) {
|
|
792
|
+
for (const plugin of config.plugins) {
|
|
793
|
+
pluginManager.register(plugin);
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
await pluginManager.executeHook("configResolved", config);
|
|
797
|
+
const contractConfigs = (config.contracts || []).map(
|
|
798
|
+
(contract) => ({
|
|
799
|
+
name: contract.name,
|
|
800
|
+
address: contract.address,
|
|
801
|
+
source: contract.source,
|
|
802
|
+
abi: contract.abi,
|
|
803
|
+
// Include ABI if it exists (from plugins)
|
|
804
|
+
_clarinetSource: contract._clarinetSource
|
|
805
|
+
// Include plugin flags
|
|
806
|
+
})
|
|
807
|
+
);
|
|
808
|
+
const processedContracts = await pluginManager.transformContracts(
|
|
809
|
+
contractConfigs,
|
|
810
|
+
config
|
|
811
|
+
);
|
|
812
|
+
if (processedContracts.length === 0) {
|
|
813
|
+
spinner.warn("No contracts found to generate");
|
|
814
|
+
console.log("\nTo get started:");
|
|
815
|
+
console.log(" \u2022 Add contracts to your config file, or");
|
|
816
|
+
console.log(" \u2022 Use plugins like clarinet() for local contracts");
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
const outputs = await pluginManager.executeGeneration(
|
|
820
|
+
processedContracts,
|
|
821
|
+
config
|
|
822
|
+
);
|
|
823
|
+
if (!outputs.has("contracts") && processedContracts.length > 0) {
|
|
824
|
+
const contractsCode = await generateContractInterface(processedContracts);
|
|
825
|
+
outputs.set("contracts", {
|
|
826
|
+
path: config.out,
|
|
827
|
+
content: contractsCode,
|
|
828
|
+
type: "contracts"
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
const transformedOutputs = await pluginManager.transformOutputs(outputs);
|
|
832
|
+
await pluginManager.writeOutputs(transformedOutputs);
|
|
833
|
+
const contractCount = processedContracts.length;
|
|
834
|
+
const contractWord = contractCount === 1 ? "contract" : "contracts";
|
|
835
|
+
spinner.succeed(`Generation complete for ${contractCount} ${contractWord}`);
|
|
836
|
+
console.log(`
|
|
837
|
+
\u{1F4C4} ${config.out}`);
|
|
838
|
+
console.log(`
|
|
839
|
+
\u{1F4A1} Import your contracts:`);
|
|
840
|
+
if (processedContracts.length > 0) {
|
|
841
|
+
const exampleContract = processedContracts[0];
|
|
842
|
+
console.log(
|
|
843
|
+
chalk.gray(
|
|
844
|
+
` import { ${exampleContract.name} } from '${config.out.replace(/\.ts$/, "")}'`
|
|
845
|
+
)
|
|
846
|
+
);
|
|
847
|
+
if (processedContracts.length > 1) {
|
|
848
|
+
console.log(
|
|
849
|
+
chalk.gray(
|
|
850
|
+
` // Also available: ${processedContracts.slice(1).map((c) => c.name).join(", ")}`
|
|
851
|
+
)
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
} catch (error) {
|
|
856
|
+
spinner.fail("Generation failed");
|
|
857
|
+
console.error(chalk.red(`
|
|
858
|
+
${error.message}`));
|
|
859
|
+
if (process.env.DEBUG) {
|
|
860
|
+
console.error(error.stack);
|
|
861
|
+
}
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// src/cli.ts
|
|
867
|
+
program.name("stacks").description("CLI tool for generating type-safe Stacks contract interfaces").version("0.1.0");
|
|
868
|
+
program.command("generate").alias("gen").description("Generate TypeScript interfaces from Clarity contracts").option("-c, --config <path>", "Path to config file").option("-w, --watch", "Watch for changes").action(generate);
|
|
869
|
+
program.command("init").description("Initialize a new stacks.config.ts file").action(async () => {
|
|
870
|
+
const { init: init2 } = await Promise.resolve().then(() => (init_init(), init_exports));
|
|
871
|
+
await init2();
|
|
872
|
+
});
|
|
873
|
+
program.parse();
|
|
874
|
+
//# sourceMappingURL=cli.js.map
|