@donezone/cli 0.1.15 → 0.1.27
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/README.md +2 -0
- package/dist/index.js +912 -420
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -3,256 +3,267 @@ import { spawn } from "node:child_process";
|
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import * as fs from "node:fs/promises";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import chokidar from "chokidar";
|
|
7
|
+
import pc from "picocolors";
|
|
6
8
|
import { Command } from "commander";
|
|
9
|
+
import { DoneBackendClient } from "@donezone/client";
|
|
10
|
+
import { DoneLocalChain, deployJsContract } from "@donezone/local-chain";
|
|
11
|
+
import { createDoneHttpLocalServer } from "@donezone/http-local";
|
|
12
|
+
import { createDoneEventsLocalServer } from "@donezone/events-local";
|
|
7
13
|
const DEFAULT_TEMPLATE_REPO = "https://github.com/mccallofthewild/done-template.git";
|
|
8
|
-
const program = new Command
|
|
14
|
+
const program = new Command;
|
|
9
15
|
program.name("done").description("Done developer toolkit (spec-first stub)").version("0.0.0-spec");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
.
|
|
15
|
-
.
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
await handleBuild(options);
|
|
19
|
-
}
|
|
20
|
-
catch (error) {
|
|
21
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
22
|
-
process.exit(1);
|
|
23
|
-
}
|
|
16
|
+
program.command("build").description("Bundle Done contracts defined in done.json").option("-j, --contracts <path>", "Path to done.json").option("-n, --name <name...>", "Filter contract names").action(async (options) => {
|
|
17
|
+
try {
|
|
18
|
+
await handleBuild(options);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
24
23
|
});
|
|
25
|
-
program
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.argument("[name]", "Project or contract name")
|
|
33
|
-
.option("-f, --force", "Overwrite existing files if they already exist")
|
|
34
|
-
.option("--template <repoOrPath>", "Template git repo or local folder (defaults to official Done template)")
|
|
35
|
-
.option("--no-install", "Skip Bun install after creating a brand new workspace")
|
|
36
|
-
.description("Set up a new Done workspace or add a contract to an existing workspace")
|
|
37
|
-
.action(async (rawName, options) => {
|
|
38
|
-
try {
|
|
39
|
-
await handleInit(rawName, options);
|
|
40
|
-
}
|
|
41
|
-
catch (error) {
|
|
42
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
24
|
+
program.command("dev").description("Boot Done local chain + HTTP + events stack with live publish_code reloads and optional frontend dev server").option("--backend", "Start backend services (done-local-chain, done-http-local, done-events-local)").option("--frontend", "Start the frontend dev server").action(async (options) => {
|
|
25
|
+
try {
|
|
26
|
+
await handleDev(options);
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
45
31
|
});
|
|
46
|
-
program
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
.option("-f, --fresh", "Force rebuild before deploy")
|
|
51
|
-
.action(() => notImplemented("deploy"));
|
|
52
|
-
program
|
|
53
|
-
.command("test")
|
|
54
|
-
.description("Run unit tests for contracts");
|
|
55
|
-
program.parseAsync().catch((error) => {
|
|
32
|
+
program.command("init").argument("[name]", "Project or contract name").option("-f, --force", "Overwrite existing files if they already exist").option("--template <repoOrPath>", "Template git repo or local folder (defaults to official Done template)").option("--no-install", "Skip Bun install after creating a brand new workspace").description("Set up a new Done workspace or add a contract to an existing workspace").action(async (rawName, options) => {
|
|
33
|
+
try {
|
|
34
|
+
await handleInit(rawName, options);
|
|
35
|
+
} catch (error) {
|
|
56
36
|
console.error(error instanceof Error ? error.message : String(error));
|
|
57
37
|
process.exit(1);
|
|
38
|
+
}
|
|
58
39
|
});
|
|
59
|
-
|
|
60
|
-
|
|
40
|
+
program.command("deploy").description("Deploy latest bundles to the configured Done workspace").option("-e, --env <profile>", "Environment/profile name").option("-f, --fresh", "Force rebuild before deploy").action(() => notImplemented("deploy"));
|
|
41
|
+
program.command("test").description("Run unit tests for contracts").action(() => notImplemented("test"));
|
|
42
|
+
program.command("publish").description("Publish a new JS bundle to an existing Done contract via Done HTTP").requiredOption("-c, --contract <address>", "Target Done contract address").requiredOption("-s, --script <path>", "Path to bundled JS module").option("-m, --msg <jsonOrPath>", "Inline JSON payload or path to migrate message").option("--http <url>", "Done HTTP base URL (defaults to DONE_HTTP or http://localhost:3000)").action(async (options) => {
|
|
43
|
+
try {
|
|
44
|
+
await handlePublish(options);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
61
47
|
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
program.parseAsync().catch((error) => {
|
|
51
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
|
54
|
+
function notImplemented(label) {
|
|
55
|
+
console.error(`The command segment '${label}' is not implemented yet.\nSee packages/done-cli/SPEC.md for the planned behaviour.`);
|
|
56
|
+
process.exit(1);
|
|
62
57
|
}
|
|
63
58
|
async function handleInit(rawName, options) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!rawName) {
|
|
68
|
-
throw new Error("Contract name is required when running init inside an existing Done workspace.");
|
|
69
|
-
}
|
|
70
|
-
await scaffoldContract(workspace, rawName, options);
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
59
|
+
const cwd = process.cwd();
|
|
60
|
+
const workspace = await detectWorkspace(cwd);
|
|
61
|
+
if (workspace) {
|
|
73
62
|
if (!rawName) {
|
|
74
|
-
|
|
63
|
+
throw new Error("Contract name is required when running init inside an existing Done workspace.");
|
|
75
64
|
}
|
|
76
|
-
await
|
|
65
|
+
await scaffoldContract(workspace, rawName, options);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (!rawName) {
|
|
69
|
+
throw new Error("Project name is required when creating a brand new Done workspace.");
|
|
70
|
+
}
|
|
71
|
+
await scaffoldWorkspace(rawName, options);
|
|
77
72
|
}
|
|
78
73
|
async function handleBuild(options) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (!contract.entry) {
|
|
98
|
-
throw new Error(`Contract '${contract.name}' is missing an 'entry' path in ${manifestInfo.manifestPath}`);
|
|
99
|
-
}
|
|
100
|
-
if (!contract.outFile) {
|
|
101
|
-
throw new Error(`Contract '${contract.name}' is missing an 'outFile' path in ${manifestInfo.manifestPath}`);
|
|
102
|
-
}
|
|
103
|
-
const entryPath = path.resolve(manifestInfo.manifestDir, contract.entry);
|
|
104
|
-
const outFilePath = path.resolve(manifestInfo.manifestDir, contract.outFile);
|
|
105
|
-
await ensureEntryExists(entryPath, contract.name);
|
|
106
|
-
await fs.mkdir(path.dirname(outFilePath), { recursive: true });
|
|
107
|
-
const relEntry = path.relative(process.cwd(), entryPath) || entryPath;
|
|
108
|
-
const relOutFile = path.relative(process.cwd(), outFilePath) || outFilePath;
|
|
109
|
-
console.log(`- bundling ${contract.name} (${relEntry})`);
|
|
110
|
-
const startedAt = Date.now();
|
|
111
|
-
try {
|
|
112
|
-
await runBunBuild(entryPath, outFilePath);
|
|
113
|
-
}
|
|
114
|
-
catch (error) {
|
|
115
|
-
console.error(`[error] failed to build ${contract.name}`);
|
|
116
|
-
throw error;
|
|
117
|
-
}
|
|
118
|
-
const duration = Date.now() - startedAt;
|
|
119
|
-
console.log(` [done] ${contract.name} -> ${relOutFile} (${formatDuration(duration)})`);
|
|
74
|
+
const manifestInfo = await resolveManifest(options.contracts);
|
|
75
|
+
const contracts = manifestInfo.manifest.contracts ?? [];
|
|
76
|
+
if (contracts.length === 0) {
|
|
77
|
+
throw new Error(`No contracts defined in ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
|
|
78
|
+
}
|
|
79
|
+
const filters = (options.name ?? []).flatMap((pattern) => pattern.split(",")).map((pattern) => pattern.trim()).filter(Boolean);
|
|
80
|
+
const matchers = filters.map(createGlobMatcher);
|
|
81
|
+
const selected = matchers.length === 0 ? contracts : contracts.filter((contract) => matchesContract(contract, manifestInfo.manifestDir, matchers));
|
|
82
|
+
if (selected.length === 0) {
|
|
83
|
+
throw new Error(`No contracts matched filters: ${filters.join(", ")}`);
|
|
84
|
+
}
|
|
85
|
+
console.log(`Building ${selected.length} contract${selected.length === 1 ? "" : "s"} from ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
|
|
86
|
+
for (const contract of selected) {
|
|
87
|
+
if (!contract.entry) {
|
|
88
|
+
throw new Error(`Contract '${contract.name}' is missing an 'entry' path in ${manifestInfo.manifestPath}`);
|
|
89
|
+
}
|
|
90
|
+
if (!contract.outFile) {
|
|
91
|
+
throw new Error(`Contract '${contract.name}' is missing an 'outFile' path in ${manifestInfo.manifestPath}`);
|
|
120
92
|
}
|
|
93
|
+
const entryPath = path.resolve(manifestInfo.manifestDir, contract.entry);
|
|
94
|
+
const outFilePath = path.resolve(manifestInfo.manifestDir, contract.outFile);
|
|
95
|
+
await ensureEntryExists(entryPath, contract.name);
|
|
96
|
+
await fs.mkdir(path.dirname(outFilePath), { recursive: true });
|
|
97
|
+
const relEntry = path.relative(process.cwd(), entryPath) || entryPath;
|
|
98
|
+
const relOutFile = path.relative(process.cwd(), outFilePath) || outFilePath;
|
|
99
|
+
console.log(`- bundling ${contract.name} (${relEntry})`);
|
|
100
|
+
const startedAt = Date.now();
|
|
101
|
+
try {
|
|
102
|
+
await runBunBuild(entryPath, outFilePath);
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`[error] failed to build ${contract.name}`);
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
const duration = Date.now() - startedAt;
|
|
108
|
+
console.log(` [done] ${contract.name} -> ${relOutFile} (${formatDuration(duration)})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function handlePublish(options) {
|
|
112
|
+
const scriptPath = path.resolve(process.cwd(), options.script);
|
|
113
|
+
await ensureFileExists(scriptPath, "script");
|
|
114
|
+
const script = await fs.readFile(scriptPath, "utf8");
|
|
115
|
+
const msg = options.msg ? await loadMsgPayload(options.msg) : undefined;
|
|
116
|
+
const baseUrl = resolveDoneHttpBase(options.http);
|
|
117
|
+
const backend = new DoneBackendClient({ baseUrl });
|
|
118
|
+
console.log(`Publishing ${path.basename(scriptPath)} to ${options.contract} via ${baseUrl}`);
|
|
119
|
+
const result = await backend.publishCode({
|
|
120
|
+
contract: options.contract,
|
|
121
|
+
script,
|
|
122
|
+
msg
|
|
123
|
+
});
|
|
124
|
+
console.log(` [tx] ${result.transactionHash ?? "(pending)"}`);
|
|
125
|
+
console.log(` [height] ${result.height}`);
|
|
126
|
+
if (typeof result.gasUsed === "number") {
|
|
127
|
+
console.log(` [gas] ${result.gasUsed}`);
|
|
128
|
+
}
|
|
129
|
+
if (result.data !== undefined) {
|
|
130
|
+
console.log(` [data] ${JSON.stringify(result.data)}`);
|
|
131
|
+
}
|
|
121
132
|
}
|
|
122
133
|
async function detectWorkspace(root) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
134
|
+
const configPath = path.join(root, "done.config.json");
|
|
135
|
+
if (!existsSync(configPath)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const configRaw = await fs.readFile(configPath, "utf8");
|
|
139
|
+
const config = JSON.parse(configRaw);
|
|
140
|
+
const configuredManifestPath = config.contractsFile ? path.isAbsolute(config.contractsFile) ? config.contractsFile : path.resolve(root, config.contractsFile) : path.join(root, "done.json");
|
|
141
|
+
const fallbackManifestPath = path.join(root, "done.json");
|
|
142
|
+
const manifestPath = existsSync(configuredManifestPath) ? configuredManifestPath : existsSync(fallbackManifestPath) ? fallbackManifestPath : null;
|
|
143
|
+
if (!manifestPath) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const manifestRaw = await fs.readFile(manifestPath, "utf8");
|
|
147
|
+
const manifest = JSON.parse(manifestRaw);
|
|
148
|
+
return { root, manifestPath, configPath, manifest, config };
|
|
135
149
|
}
|
|
136
150
|
async function resolveManifest(contractsPathOverride) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
151
|
+
if (contractsPathOverride) {
|
|
152
|
+
const manifestPath = path.resolve(process.cwd(), contractsPathOverride);
|
|
153
|
+
const manifest = await readManifest(manifestPath);
|
|
154
|
+
return { manifestPath, manifestDir: path.dirname(manifestPath), manifest };
|
|
155
|
+
}
|
|
156
|
+
const workspace = await findWorkspace(process.cwd());
|
|
157
|
+
if (!workspace) {
|
|
158
|
+
throw new Error("Unable to find done.json. Run inside a Done workspace or pass --contracts <path>.");
|
|
159
|
+
}
|
|
160
|
+
return { manifestPath: workspace.manifestPath, manifestDir: path.dirname(workspace.manifestPath), manifest: workspace.manifest };
|
|
147
161
|
}
|
|
148
162
|
async function findWorkspace(startDir) {
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
current = parent;
|
|
163
|
+
let current = path.resolve(startDir);
|
|
164
|
+
while (true) {
|
|
165
|
+
const workspace = await detectWorkspace(current);
|
|
166
|
+
if (workspace) {
|
|
167
|
+
return workspace;
|
|
168
|
+
}
|
|
169
|
+
const parent = path.dirname(current);
|
|
170
|
+
if (parent === current) {
|
|
171
|
+
return null;
|
|
160
172
|
}
|
|
173
|
+
current = parent;
|
|
174
|
+
}
|
|
161
175
|
}
|
|
162
176
|
async function readManifest(manifestPath) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
throw new Error(`Could not find ${manifestPath}. Pass a valid --contracts path or run from a Done workspace.`);
|
|
170
|
-
}
|
|
171
|
-
throw error;
|
|
172
|
-
}
|
|
173
|
-
try {
|
|
174
|
-
return JSON.parse(raw);
|
|
175
|
-
}
|
|
176
|
-
catch (error) {
|
|
177
|
-
throw new Error(`Failed to parse ${manifestPath}: ${error.message}`);
|
|
177
|
+
let raw;
|
|
178
|
+
try {
|
|
179
|
+
raw = await fs.readFile(manifestPath, "utf8");
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (error.code === "ENOENT") {
|
|
182
|
+
throw new Error(`Could not find ${manifestPath}. Pass a valid --contracts path or run from a Done workspace.`);
|
|
178
183
|
}
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
try {
|
|
187
|
+
return JSON.parse(raw);
|
|
188
|
+
} catch (error) {
|
|
189
|
+
throw new Error(`Failed to parse ${manifestPath}: ${error.message}`);
|
|
190
|
+
}
|
|
179
191
|
}
|
|
180
192
|
async function scaffoldWorkspace(rawName, options) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
console.log(" bunx done dev");
|
|
193
|
+
const slug = toSlug(rawName);
|
|
194
|
+
if (!slug) {
|
|
195
|
+
throw new Error(`Could not derive a valid directory name from '${rawName}'.`);
|
|
196
|
+
}
|
|
197
|
+
const templateRef = options.template ?? DEFAULT_TEMPLATE_REPO;
|
|
198
|
+
const cwd = process.cwd();
|
|
199
|
+
const targetDir = path.resolve(cwd, slug);
|
|
200
|
+
await prepareTargetDir(targetDir, options.force === true);
|
|
201
|
+
const localTemplatePath = await resolveLocalTemplate(templateRef);
|
|
202
|
+
if (localTemplatePath) {
|
|
203
|
+
await fs.cp(localTemplatePath, targetDir, { recursive: true });
|
|
204
|
+
} else {
|
|
205
|
+
await runCommand("git", ["clone", "--depth", "1", templateRef, targetDir]);
|
|
206
|
+
}
|
|
207
|
+
await removeGitFolder(targetDir);
|
|
208
|
+
await updatePackageJson(path.join(targetDir, "package.json"), (pkg) => {
|
|
209
|
+
pkg.name = slug;
|
|
210
|
+
});
|
|
211
|
+
await updatePackageJson(path.join(targetDir, "frontend", "package.json"), (pkg) => {
|
|
212
|
+
pkg.name = `${slug}-frontend`;
|
|
213
|
+
});
|
|
214
|
+
if (options.install !== false) {
|
|
215
|
+
await runCommand("bun", ["install"], { cwd: targetDir });
|
|
216
|
+
}
|
|
217
|
+
console.log(`Done workspace created at ${path.relative(cwd, targetDir) || "."}.`);
|
|
218
|
+
console.log("Next steps:");
|
|
219
|
+
console.log(` cd ${path.relative(cwd, targetDir) || slug}`);
|
|
220
|
+
if (options.install === false) {
|
|
221
|
+
console.log(" bun install");
|
|
222
|
+
}
|
|
223
|
+
console.log(" bunx done dev");
|
|
213
224
|
}
|
|
214
225
|
async function scaffoldContract(workspace, rawName, options) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
},
|
|
236
|
-
};
|
|
237
|
-
await writeJson(pkgJsonPath, pkgJson);
|
|
238
|
-
const rootTsconfig = path.join(workspace.root, "tsconfig.json");
|
|
239
|
-
const tsconfig = {
|
|
240
|
-
compilerOptions: {
|
|
241
|
-
module: "ESNext",
|
|
242
|
-
moduleResolution: "bundler",
|
|
243
|
-
target: "ES2022",
|
|
244
|
-
isolatedModules: true,
|
|
245
|
-
noEmit: true,
|
|
246
|
-
},
|
|
247
|
-
include: ["src/**/*.ts"],
|
|
248
|
-
};
|
|
249
|
-
if (existsSync(rootTsconfig)) {
|
|
250
|
-
tsconfig.extends = normalizeJsonPath(path.relative(contractDir, rootTsconfig));
|
|
226
|
+
const slug = toSlug(rawName);
|
|
227
|
+
if (!slug) {
|
|
228
|
+
throw new Error(`Could not derive a valid contract directory name from '${rawName}'.`);
|
|
229
|
+
}
|
|
230
|
+
const contractName = toContractName(slug);
|
|
231
|
+
const contractsDir = resolveContractsDir(workspace.root, workspace.config.contractsDir);
|
|
232
|
+
const contractDir = path.join(contractsDir, slug);
|
|
233
|
+
await ensureDirAvailable(contractDir, options.force === true);
|
|
234
|
+
await fs.mkdir(path.join(contractDir, "src"), { recursive: true });
|
|
235
|
+
const pkgJsonPath = path.join(contractDir, "package.json");
|
|
236
|
+
const pkgJson = {
|
|
237
|
+
name: slug,
|
|
238
|
+
version: "0.1.0",
|
|
239
|
+
private: true,
|
|
240
|
+
type: "module",
|
|
241
|
+
scripts: {
|
|
242
|
+
typecheck: "tsc -p tsconfig.json --noEmit"
|
|
243
|
+
},
|
|
244
|
+
dependencies: {
|
|
245
|
+
"done.zone": "^0.1.0"
|
|
251
246
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
247
|
+
};
|
|
248
|
+
await writeJson(pkgJsonPath, pkgJson);
|
|
249
|
+
const rootTsconfig = path.join(workspace.root, "tsconfig.json");
|
|
250
|
+
const tsconfig = {
|
|
251
|
+
compilerOptions: {
|
|
252
|
+
module: "ESNext",
|
|
253
|
+
moduleResolution: "bundler",
|
|
254
|
+
target: "ES2022",
|
|
255
|
+
isolatedModules: true,
|
|
256
|
+
noEmit: true
|
|
257
|
+
},
|
|
258
|
+
include: ["src/**/*.ts"]
|
|
259
|
+
};
|
|
260
|
+
if (existsSync(rootTsconfig)) {
|
|
261
|
+
tsconfig.extends = normalizeJsonPath(path.relative(contractDir, rootTsconfig));
|
|
262
|
+
}
|
|
263
|
+
await writeJson(path.join(contractDir, "tsconfig.json"), tsconfig);
|
|
264
|
+
const storeKey = `${slug}-greeting`;
|
|
265
|
+
const defaultGreeting = `Hello from ${contractName}`;
|
|
266
|
+
const contractSource = `const Greeting = Done.item<string>("${storeKey}");
|
|
256
267
|
|
|
257
268
|
interface SetGreetingBody {
|
|
258
269
|
greeting: string;
|
|
@@ -294,255 +305,736 @@ export default Done.serve()
|
|
|
294
305
|
},
|
|
295
306
|
);
|
|
296
307
|
`;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
console.log(" bunx done dev");
|
|
308
|
+
await fs.writeFile(path.join(contractDir, "src", "index.ts"), contractSource, "utf8");
|
|
309
|
+
workspace.manifest.contracts ??= [];
|
|
310
|
+
const contractDirRel = normalizeJsonPath(path.relative(workspace.root, contractDir));
|
|
311
|
+
const entry = ensureDotSlash(`${contractDirRel}/src/index.ts`);
|
|
312
|
+
const outFile = ensureDotSlash(`${contractDirRel}/dist/${slug}.contract.js`);
|
|
313
|
+
const watch = [ensureDotSlash(`${contractDirRel}/src/**/*.ts`)];
|
|
314
|
+
const newEntry = {
|
|
315
|
+
name: contractName,
|
|
316
|
+
entry,
|
|
317
|
+
outFile,
|
|
318
|
+
watch,
|
|
319
|
+
instantiate: { admin: null }
|
|
320
|
+
};
|
|
321
|
+
const existingIndex = workspace.manifest.contracts.findIndex((contract) => contract.name === contractName);
|
|
322
|
+
if (existingIndex >= 0 && options.force !== true) {
|
|
323
|
+
throw new Error(`Contract '${contractName}' already exists in done.json. Use --force to replace it.`);
|
|
324
|
+
}
|
|
325
|
+
if (existingIndex >= 0) {
|
|
326
|
+
workspace.manifest.contracts[existingIndex] = newEntry;
|
|
327
|
+
} else {
|
|
328
|
+
workspace.manifest.contracts.push(newEntry);
|
|
329
|
+
}
|
|
330
|
+
await writeJson(workspace.manifestPath, workspace.manifest);
|
|
331
|
+
console.log(`Created Done contract '${contractName}' in ${path.relative(workspace.root, contractDir)}`);
|
|
332
|
+
console.log("Next steps:");
|
|
333
|
+
console.log(" bunx done build --name", contractName);
|
|
334
|
+
console.log(" bunx done dev");
|
|
325
335
|
}
|
|
326
336
|
async function resolveLocalTemplate(reference) {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
337
|
+
const potentialPath = path.isAbsolute(reference) ? reference : path.resolve(process.cwd(), reference);
|
|
338
|
+
try {
|
|
339
|
+
const stats = await fs.stat(potentialPath);
|
|
340
|
+
if (stats.isDirectory()) {
|
|
341
|
+
return potentialPath;
|
|
333
342
|
}
|
|
334
|
-
|
|
335
|
-
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
return null;
|
|
336
346
|
}
|
|
337
347
|
async function prepareTargetDir(targetDir, force) {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
348
|
+
if (!existsSync(targetDir)) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!force) {
|
|
352
|
+
throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
|
|
353
|
+
}
|
|
354
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
345
355
|
}
|
|
346
356
|
async function ensureDirAvailable(targetDir, force) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
357
|
+
if (!existsSync(targetDir)) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
if (!force) {
|
|
361
|
+
throw new Error(`Directory '${path.relative(process.cwd(), targetDir) || targetDir}' already exists. Use --force to overwrite.`);
|
|
362
|
+
}
|
|
363
|
+
await fs.rm(targetDir, { recursive: true, force: true });
|
|
354
364
|
}
|
|
355
365
|
async function removeGitFolder(targetDir) {
|
|
356
|
-
|
|
366
|
+
await fs.rm(path.join(targetDir, ".git"), { recursive: true, force: true });
|
|
357
367
|
}
|
|
358
368
|
function resolveContractsDir(root, configured) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
369
|
+
if (!configured) {
|
|
370
|
+
return path.join(root, "contracts");
|
|
371
|
+
}
|
|
372
|
+
if (path.isAbsolute(configured)) {
|
|
373
|
+
return configured;
|
|
374
|
+
}
|
|
375
|
+
return path.resolve(root, configured);
|
|
366
376
|
}
|
|
367
377
|
async function updatePackageJson(file, mutator) {
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
return;
|
|
376
|
-
}
|
|
377
|
-
throw error;
|
|
378
|
+
try {
|
|
379
|
+
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
380
|
+
mutator(current);
|
|
381
|
+
await writeJson(file, current);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error.code === "ENOENT") {
|
|
384
|
+
return;
|
|
378
385
|
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
379
388
|
}
|
|
380
389
|
function ensureDotSlash(value) {
|
|
381
|
-
|
|
390
|
+
return value.startsWith("./") || value.startsWith("../") ? value : `./${value}`;
|
|
382
391
|
}
|
|
383
392
|
function normalizeJsonPath(relativePath) {
|
|
384
|
-
|
|
385
|
-
|
|
393
|
+
const normalized = relativePath.split(path.sep).join("/");
|
|
394
|
+
return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
|
|
386
395
|
}
|
|
387
396
|
function matchesContract(contract, manifestDir, matchers) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
const candidates = [];
|
|
398
|
+
if (contract.name) {
|
|
399
|
+
candidates.push(normalizeMatchValue(contract.name));
|
|
400
|
+
}
|
|
401
|
+
if (contract.entry) {
|
|
402
|
+
candidates.push(normalizeMatchValue(contract.entry));
|
|
403
|
+
candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.entry)));
|
|
404
|
+
}
|
|
405
|
+
if (contract.outFile) {
|
|
406
|
+
candidates.push(normalizeMatchValue(contract.outFile));
|
|
407
|
+
candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.outFile)));
|
|
408
|
+
}
|
|
409
|
+
return matchers.some((matcher) => candidates.some((candidate) => matcher(candidate)));
|
|
401
410
|
}
|
|
402
411
|
function createGlobMatcher(pattern) {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const regex = new RegExp(`^${escaped}$`);
|
|
412
|
-
return (candidate) => regex.test(normalizeMatchValue(candidate));
|
|
412
|
+
const normalized = normalizeMatchValue(pattern);
|
|
413
|
+
const hasWildcards = /[\*\?]/.test(normalized);
|
|
414
|
+
if (!hasWildcards) {
|
|
415
|
+
return (candidate) => normalizeMatchValue(candidate) === normalized;
|
|
416
|
+
}
|
|
417
|
+
const escaped = escapeRegExp(normalized).replace(/\\\*/g, ".*").replace(/\\\?/g, ".");
|
|
418
|
+
const regex = new RegExp(`^${escaped}\$`);
|
|
419
|
+
return (candidate) => regex.test(normalizeMatchValue(candidate));
|
|
413
420
|
}
|
|
414
421
|
function normalizeMatchValue(value) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
422
|
+
if (!value) {
|
|
423
|
+
return "";
|
|
424
|
+
}
|
|
425
|
+
let normalized = value.replace(/\\/g, "/");
|
|
426
|
+
if (normalized.startsWith("./")) {
|
|
427
|
+
normalized = normalized.slice(2);
|
|
428
|
+
}
|
|
429
|
+
return normalized;
|
|
423
430
|
}
|
|
424
431
|
function escapeRegExp(value) {
|
|
425
|
-
|
|
432
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
426
433
|
}
|
|
427
434
|
async function ensureEntryExists(entryPath, contractName) {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
435
|
+
try {
|
|
436
|
+
const stats = await fs.stat(entryPath);
|
|
437
|
+
if (!stats.isFile()) {
|
|
438
|
+
const relPath = path.relative(process.cwd(), entryPath) || entryPath;
|
|
439
|
+
throw new Error(`Entry path for contract '${contractName}' is not a file (${relPath})`);
|
|
434
440
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (error.code === "ENOENT") {
|
|
443
|
+
const rel = path.relative(process.cwd(), entryPath) || entryPath;
|
|
444
|
+
throw new Error(`Entry file for contract '${contractName}' not found (${rel})`);
|
|
445
|
+
}
|
|
446
|
+
throw error;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async function ensureFileExists(filePath, label) {
|
|
450
|
+
try {
|
|
451
|
+
const stats = await fs.stat(filePath);
|
|
452
|
+
if (!stats.isFile()) {
|
|
453
|
+
throw new Error(`The ${label} path is not a file (${filePath})`);
|
|
441
454
|
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (error.code === "ENOENT") {
|
|
457
|
+
throw new Error(`The ${label} file does not exist (${filePath})`);
|
|
458
|
+
}
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
async function loadMsgPayload(input) {
|
|
463
|
+
const potentialPath = path.resolve(process.cwd(), input);
|
|
464
|
+
if (await pathExists(potentialPath)) {
|
|
465
|
+
const raw = await fs.readFile(potentialPath, "utf8");
|
|
466
|
+
return JSON.parse(raw);
|
|
467
|
+
}
|
|
468
|
+
return JSON.parse(input);
|
|
469
|
+
}
|
|
470
|
+
function resolveDoneHttpBase(override) {
|
|
471
|
+
return override ?? process.env.DONE_HTTP ?? process.env.DONE_HTTP_BASE ?? "http://localhost:3000";
|
|
472
|
+
}
|
|
473
|
+
async function pathExists(candidate) {
|
|
474
|
+
try {
|
|
475
|
+
await fs.access(candidate);
|
|
476
|
+
return true;
|
|
477
|
+
} catch {
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
442
480
|
}
|
|
443
481
|
async function runBunBuild(entryPath, outFilePath) {
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
}
|
|
465
|
-
const bundledSource = await output.text();
|
|
466
|
-
await fs.writeFile(outFilePath, bundledSource);
|
|
467
|
-
return;
|
|
482
|
+
const bun = getEmbeddedBunRuntime();
|
|
483
|
+
if (bun) {
|
|
484
|
+
const result = await bun.build({
|
|
485
|
+
entrypoints: [entryPath],
|
|
486
|
+
target: "bun",
|
|
487
|
+
format: "esm",
|
|
488
|
+
splitting: false,
|
|
489
|
+
sourcemap: "none",
|
|
490
|
+
external: ["bun:*", "node:*"],
|
|
491
|
+
define: {
|
|
492
|
+
"process.env.NODE_ENV": '"production"'
|
|
493
|
+
}
|
|
494
|
+
});
|
|
495
|
+
if (!result.success) {
|
|
496
|
+
const logs = (result.logs ?? []).map((log) => log?.message ?? String(log));
|
|
497
|
+
throw new Error(logs.join("\n") || "Bun.build failed");
|
|
498
|
+
}
|
|
499
|
+
const [output] = result.outputs;
|
|
500
|
+
if (!output) {
|
|
501
|
+
throw new Error("Bun.build did not emit an output file");
|
|
468
502
|
}
|
|
469
|
-
await
|
|
503
|
+
const bundledSource = await output.text();
|
|
504
|
+
await fs.writeFile(outFilePath, bundledSource);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
await runBunCliBuild(entryPath, outFilePath);
|
|
470
508
|
}
|
|
471
509
|
function getEmbeddedBunRuntime() {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
510
|
+
const bun = globalThis.Bun;
|
|
511
|
+
if (!bun || typeof bun.build !== "function") {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
return bun;
|
|
477
515
|
}
|
|
478
516
|
async function runBunCliBuild(entryPath, outFilePath) {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
throw new Error("Bun executable not found in PATH. Install Bun from https://bun.sh and re-run the build.");
|
|
502
|
-
}
|
|
503
|
-
throw error;
|
|
517
|
+
const args = [
|
|
518
|
+
"build",
|
|
519
|
+
entryPath,
|
|
520
|
+
"--outfile",
|
|
521
|
+
outFilePath,
|
|
522
|
+
"--target",
|
|
523
|
+
"bun",
|
|
524
|
+
"--format",
|
|
525
|
+
"esm",
|
|
526
|
+
"--sourcemap",
|
|
527
|
+
"none",
|
|
528
|
+
"--define",
|
|
529
|
+
'process.env.NODE_ENV="production"'
|
|
530
|
+
];
|
|
531
|
+
for (const external of ["bun:*", "node:*"]) {
|
|
532
|
+
args.push("--external", external);
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
await runCommand("bun", args);
|
|
536
|
+
} catch (error) {
|
|
537
|
+
if (error.code === "ENOENT") {
|
|
538
|
+
throw new Error("Bun executable not found in PATH. Install Bun from https://bun.sh and re-run the build.");
|
|
504
539
|
}
|
|
540
|
+
throw error;
|
|
541
|
+
}
|
|
505
542
|
}
|
|
506
543
|
function formatDuration(ms) {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
544
|
+
if (ms < 1000) {
|
|
545
|
+
return `${ms}ms`;
|
|
546
|
+
}
|
|
547
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
511
548
|
}
|
|
512
549
|
function toSlug(input) {
|
|
513
|
-
|
|
514
|
-
.trim()
|
|
515
|
-
.toLowerCase()
|
|
516
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
517
|
-
.replace(/^-+/, "")
|
|
518
|
-
.replace(/-+$/, "");
|
|
550
|
+
return input.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
519
551
|
}
|
|
520
552
|
function toContractName(slug) {
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
553
|
+
const parts = slug.split(/[^a-zA-Z0-9]+/).filter(Boolean);
|
|
554
|
+
if (parts.length === 0) {
|
|
555
|
+
return "contract";
|
|
556
|
+
}
|
|
557
|
+
const [first, ...rest] = parts;
|
|
558
|
+
return first + rest.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
527
559
|
}
|
|
528
560
|
async function writeJson(file, data) {
|
|
529
|
-
|
|
530
|
-
|
|
561
|
+
const json = JSON.stringify(data, null, 2);
|
|
562
|
+
await fs.writeFile(file, `${json}\n`, "utf8");
|
|
531
563
|
}
|
|
532
564
|
async function runCommand(command, args, options = {}) {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
565
|
+
await new Promise((resolve, reject) => {
|
|
566
|
+
const child = spawn(command, args, {
|
|
567
|
+
cwd: options.cwd,
|
|
568
|
+
stdio: "inherit"
|
|
569
|
+
});
|
|
570
|
+
child.on("close", (code) => {
|
|
571
|
+
if (code === 0) {
|
|
572
|
+
resolve();
|
|
573
|
+
} else {
|
|
574
|
+
reject(new Error(`${command} ${args.join(" ")} failed with exit code ${code}`));
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
child.on("error", (error) => reject(error));
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
async function handleDev(options) {
|
|
581
|
+
const workspace = await findWorkspace(process.cwd());
|
|
582
|
+
if (!workspace) {
|
|
583
|
+
throw new Error("done dev must be run inside a Done workspace." + " Run `bunx done init` from your project root first.");
|
|
584
|
+
}
|
|
585
|
+
const startBackend = options.backend || !options.backend && !options.frontend;
|
|
586
|
+
const startFrontend = options.frontend || !options.backend && !options.frontend;
|
|
587
|
+
if (!startBackend && !startFrontend) {
|
|
588
|
+
throw new Error("Specify --backend and/or --frontend to start services.");
|
|
589
|
+
}
|
|
590
|
+
const devConfig = resolveDevConfig(workspace);
|
|
591
|
+
const orchestrator = new DoneDevOrchestrator({
|
|
592
|
+
workspace,
|
|
593
|
+
startBackend,
|
|
594
|
+
startFrontend,
|
|
595
|
+
devConfig
|
|
596
|
+
});
|
|
597
|
+
try {
|
|
598
|
+
await orchestrator.start();
|
|
599
|
+
await orchestrator.waitUntilStopped();
|
|
600
|
+
} finally {
|
|
601
|
+
await orchestrator.stop();
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
function resolveDevConfig(workspace) {
|
|
605
|
+
const dev = workspace.config.dev ?? {};
|
|
606
|
+
const host = dev.host ?? "127.0.0.1";
|
|
607
|
+
const port = dev.port ?? 8787;
|
|
608
|
+
const eventsHost = dev.eventsHost ?? host;
|
|
609
|
+
const eventsPort = dev.eventsPort ?? 7071;
|
|
610
|
+
const envFile = dev.envFile ? path.isAbsolute(dev.envFile) ? dev.envFile : path.resolve(workspace.root, dev.envFile) : path.resolve(workspace.root, ".done", "dev.env");
|
|
611
|
+
const frontendCwd = dev.frontend?.cwd ? path.isAbsolute(dev.frontend.cwd) ? dev.frontend.cwd : path.resolve(workspace.root, dev.frontend.cwd) : path.resolve(workspace.root, "frontend");
|
|
612
|
+
return {
|
|
613
|
+
envFile,
|
|
614
|
+
owner: dev.owner ?? "done1owner000000000000000000000000000000000000",
|
|
615
|
+
routerAddress: dev.routerAddress ?? null,
|
|
616
|
+
http: { host, port },
|
|
617
|
+
events: { host: eventsHost, port: eventsPort },
|
|
618
|
+
frontend: {
|
|
619
|
+
cwd: frontendCwd,
|
|
620
|
+
command: dev.frontend?.command ?? "bun run dev",
|
|
621
|
+
url: dev.frontend?.url ?? "http://127.0.0.1:5173",
|
|
622
|
+
open: dev.frontend?.open !== false
|
|
623
|
+
}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
const DEV_LOG_PREFIX = pc.bold(pc.cyan("[done dev]"));
|
|
627
|
+
const DEV_WARN_PREFIX = pc.bold(pc.yellow("[done dev]"));
|
|
628
|
+
const DEV_ERROR_PREFIX = pc.bold(pc.red("[done dev]"));
|
|
629
|
+
|
|
630
|
+
class DoneDevOrchestrator {
|
|
631
|
+
options;
|
|
632
|
+
chain;
|
|
633
|
+
httpApp;
|
|
634
|
+
eventsServer;
|
|
635
|
+
watchers = [];
|
|
636
|
+
frontendProcess;
|
|
637
|
+
contracts = [];
|
|
638
|
+
envEntries = {};
|
|
639
|
+
httpUrl;
|
|
640
|
+
eventsUrl;
|
|
641
|
+
stopPromise;
|
|
642
|
+
resolveStop;
|
|
643
|
+
shuttingDown = false;
|
|
644
|
+
signalHandlers = [];
|
|
645
|
+
constructor(options) {
|
|
646
|
+
this.options = options;
|
|
647
|
+
}
|
|
648
|
+
async start() {
|
|
649
|
+
if (this.options.startBackend) {
|
|
650
|
+
await this.bootstrapBackend();
|
|
651
|
+
} else {
|
|
652
|
+
await this.loadEnvFromDisk();
|
|
653
|
+
}
|
|
654
|
+
if (this.options.startFrontend) {
|
|
655
|
+
await this.launchFrontend();
|
|
656
|
+
}
|
|
657
|
+
this.registerSignalHandlers();
|
|
658
|
+
this.printSummary();
|
|
659
|
+
if (this.options.startBackend || this.options.startFrontend) {
|
|
660
|
+
this.logInfo("Press Ctrl+C to stop");
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async waitUntilStopped() {
|
|
664
|
+
if (!this.stopPromise) {
|
|
665
|
+
this.stopPromise = new Promise((resolve) => {
|
|
666
|
+
this.resolveStop = resolve;
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return this.stopPromise;
|
|
670
|
+
}
|
|
671
|
+
async stop() {
|
|
672
|
+
if (this.shuttingDown) {
|
|
673
|
+
return this.stopPromise ?? Promise.resolve();
|
|
674
|
+
}
|
|
675
|
+
this.shuttingDown = true;
|
|
676
|
+
for (const { event, handler } of this.signalHandlers) {
|
|
677
|
+
process.off(event, handler);
|
|
678
|
+
}
|
|
679
|
+
this.signalHandlers = [];
|
|
680
|
+
await this.stopFrontend();
|
|
681
|
+
await Promise.all(this.watchers.map((watcher) => watcher.close().catch(() => {
|
|
682
|
+
})));
|
|
683
|
+
this.watchers = [];
|
|
684
|
+
await Promise.all(this.contracts.map((contract) => contract.queue.catch(() => {
|
|
685
|
+
})));
|
|
686
|
+
if (this.httpApp) {
|
|
687
|
+
await this.httpApp.stop().catch(() => {
|
|
688
|
+
});
|
|
689
|
+
this.httpApp = undefined;
|
|
690
|
+
}
|
|
691
|
+
if (this.eventsServer) {
|
|
692
|
+
try {
|
|
693
|
+
this.eventsServer.unsubscribe();
|
|
694
|
+
} catch {
|
|
695
|
+
}
|
|
696
|
+
await this.eventsServer.app.stop().catch(() => {
|
|
697
|
+
});
|
|
698
|
+
this.eventsServer = undefined;
|
|
699
|
+
}
|
|
700
|
+
this.chain = undefined;
|
|
701
|
+
this.resolveStop?.();
|
|
702
|
+
}
|
|
703
|
+
registerSignalHandlers() {
|
|
704
|
+
const shutdown = (signal) => {
|
|
705
|
+
this.logInfo(`Received ${signal}, shutting down...`);
|
|
706
|
+
this.stop();
|
|
707
|
+
};
|
|
708
|
+
for (const event of ["SIGINT", "SIGTERM"]) {
|
|
709
|
+
const handler = () => shutdown(event);
|
|
710
|
+
this.signalHandlers.push({ event, handler });
|
|
711
|
+
process.on(event, handler);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
async bootstrapBackend() {
|
|
715
|
+
this.chain = new DoneLocalChain;
|
|
716
|
+
this.contracts = this.prepareContracts();
|
|
717
|
+
await this.buildAllContracts();
|
|
718
|
+
await this.deployContracts();
|
|
719
|
+
await this.startHttpServer();
|
|
720
|
+
await this.startEventsServer();
|
|
721
|
+
await this.writeEnvFile();
|
|
722
|
+
this.startWatchers();
|
|
723
|
+
}
|
|
724
|
+
prepareContracts() {
|
|
725
|
+
const manifestDir = path.dirname(this.options.workspace.manifestPath);
|
|
726
|
+
const contracts = this.options.workspace.manifest.contracts ?? [];
|
|
727
|
+
if (contracts.length === 0) {
|
|
728
|
+
throw new Error("No contracts defined in done.json. Run `bunx done init <name>` first.");
|
|
729
|
+
}
|
|
730
|
+
return contracts.map((contract) => {
|
|
731
|
+
if (!contract.entry) {
|
|
732
|
+
throw new Error(`Contract '${contract.name}' is missing an entry path.`);
|
|
733
|
+
}
|
|
734
|
+
if (!contract.outFile) {
|
|
735
|
+
throw new Error(`Contract '${contract.name}' is missing an outFile path.`);
|
|
736
|
+
}
|
|
737
|
+
const entryPath = path.resolve(manifestDir, contract.entry);
|
|
738
|
+
const outFilePath = path.resolve(manifestDir, contract.outFile);
|
|
739
|
+
const watchGlobs = (contract.watch && contract.watch.length > 0 ? contract.watch : [contract.entry]).map((pattern) => path.resolve(manifestDir, pattern));
|
|
740
|
+
return {
|
|
741
|
+
name: contract.name,
|
|
742
|
+
envKey: toEnvVarKey(contract.name),
|
|
743
|
+
entryPath,
|
|
744
|
+
outFilePath,
|
|
745
|
+
watchGlobs,
|
|
746
|
+
instantiateMsg: { ...contract.instantiate?.msg ?? {} },
|
|
747
|
+
admin: contract.instantiate?.admin ?? null,
|
|
748
|
+
label: contract.instantiate?.label,
|
|
749
|
+
publishMsg: contract.publish?.msg,
|
|
750
|
+
queue: Promise.resolve()
|
|
751
|
+
};
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
async buildAllContracts() {
|
|
755
|
+
for (const contract of this.contracts) {
|
|
756
|
+
await this.buildBundle(contract, true);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
async buildBundle(contract, initial, reason) {
|
|
760
|
+
await ensureEntryExists(contract.entryPath, contract.name);
|
|
761
|
+
await fs.mkdir(path.dirname(contract.outFilePath), { recursive: true });
|
|
762
|
+
const startedAt = Date.now();
|
|
763
|
+
const label = initial ? "Building" : "Rebuilding";
|
|
764
|
+
this.logInfo(`${label} ${contract.name}${reason ? ` (${reason})` : ""}`);
|
|
765
|
+
await runBunBuild(contract.entryPath, contract.outFilePath);
|
|
766
|
+
this.logInfo(` ${pc.green("\u2713")} ${contract.name} bundle ready (${formatDuration(Date.now() - startedAt)})`);
|
|
767
|
+
}
|
|
768
|
+
async deployContracts() {
|
|
769
|
+
if (!this.chain)
|
|
770
|
+
return;
|
|
771
|
+
for (const contract of this.contracts) {
|
|
772
|
+
const deployment = await deployJsContract({
|
|
773
|
+
chain: this.chain,
|
|
774
|
+
sender: this.options.devConfig.owner,
|
|
775
|
+
scriptPath: contract.outFilePath,
|
|
776
|
+
instantiateMsg: contract.instantiateMsg,
|
|
777
|
+
cwJsCodeId: this.chain.batteries?.cwJs.codeId,
|
|
778
|
+
admin: contract.admin ?? undefined,
|
|
779
|
+
label: contract.label ?? contract.name
|
|
780
|
+
});
|
|
781
|
+
if (deployment.result.err) {
|
|
782
|
+
throw new Error(`Failed to deploy ${contract.name}: ${deployment.result.err}`);
|
|
783
|
+
}
|
|
784
|
+
contract.address = deployment.address;
|
|
785
|
+
this.chain.advanceBlock();
|
|
786
|
+
this.logInfo(`${pc.green("\u2022")} ${contract.name} deployed at ${deployment.address}`);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
async startHttpServer() {
|
|
790
|
+
if (!this.chain)
|
|
791
|
+
return;
|
|
792
|
+
const { host, port } = this.options.devConfig.http;
|
|
793
|
+
const httpServer = await createDoneHttpLocalServer({
|
|
794
|
+
port,
|
|
795
|
+
owner: this.options.devConfig.owner,
|
|
796
|
+
routerAddress: this.options.devConfig.routerAddress ?? undefined
|
|
797
|
+
}, this.chain);
|
|
798
|
+
httpServer.app.listen({ port, hostname: host });
|
|
799
|
+
this.httpApp = httpServer.app;
|
|
800
|
+
this.httpUrl = `http://${host}:${port}`;
|
|
801
|
+
this.logInfo(`done-http-local listening on ${this.httpUrl}`);
|
|
802
|
+
}
|
|
803
|
+
async startEventsServer() {
|
|
804
|
+
if (!this.chain)
|
|
805
|
+
return;
|
|
806
|
+
const { host, port } = this.options.devConfig.events;
|
|
807
|
+
const eventsServer = createDoneEventsLocalServer({ chain: this.chain });
|
|
808
|
+
eventsServer.app.listen({ port, hostname: host });
|
|
809
|
+
this.eventsServer = eventsServer;
|
|
810
|
+
this.eventsUrl = `http://${host}:${port}`;
|
|
811
|
+
this.logInfo(`done-events-local listening on ${this.eventsUrl}`);
|
|
812
|
+
}
|
|
813
|
+
startWatchers() {
|
|
814
|
+
for (const contract of this.contracts) {
|
|
815
|
+
const watcher = chokidar.watch(contract.watchGlobs, { ignoreInitial: true });
|
|
816
|
+
watcher.on("all", (event, filePath) => {
|
|
817
|
+
const reason = `${event} ${this.formatWorkspacePath(filePath)}`;
|
|
818
|
+
this.enqueueContractUpdate(contract, reason);
|
|
819
|
+
});
|
|
820
|
+
watcher.on("error", (error) => {
|
|
821
|
+
this.logError(`Watcher error for ${contract.name}`, error);
|
|
822
|
+
});
|
|
823
|
+
this.watchers.push(watcher);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
enqueueContractUpdate(contract, reason) {
|
|
827
|
+
contract.queue = contract.queue.catch(() => {
|
|
828
|
+
}).then(async () => {
|
|
829
|
+
try {
|
|
830
|
+
await this.buildBundle(contract, false, reason);
|
|
831
|
+
await this.publishContract(contract);
|
|
832
|
+
} catch (error) {
|
|
833
|
+
this.logError(`Failed to refresh ${contract.name}`, error);
|
|
834
|
+
}
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
async publishContract(contract) {
|
|
838
|
+
if (!this.chain || !contract.address)
|
|
839
|
+
return;
|
|
840
|
+
const script = await fs.readFile(contract.outFilePath, "utf8");
|
|
841
|
+
const execResult = this.chain.execute(contract.address, {
|
|
842
|
+
publish_code: {
|
|
843
|
+
script,
|
|
844
|
+
...contract.publishMsg ? { msg: contract.publishMsg } : {}
|
|
845
|
+
}
|
|
846
|
+
}, {
|
|
847
|
+
sender: this.options.devConfig.owner,
|
|
848
|
+
funds: []
|
|
849
|
+
});
|
|
850
|
+
if (execResult.result.err) {
|
|
851
|
+
throw new Error(execResult.result.err);
|
|
852
|
+
}
|
|
853
|
+
this.chain.advanceBlock();
|
|
854
|
+
await this.writeEnvFile({ DONE_DEV_UPDATED_AT: new Date().toISOString() });
|
|
855
|
+
this.logInfo(`${pc.green("\u21BB")} updated ${contract.name} (height ${this.chain.height})`);
|
|
856
|
+
}
|
|
857
|
+
async launchFrontend() {
|
|
858
|
+
const frontend = this.options.devConfig.frontend;
|
|
859
|
+
if (!existsSync(frontend.cwd)) {
|
|
860
|
+
this.logWarn(`Skipping frontend; directory not found (${frontend.cwd})`);
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
if (Object.keys(this.envEntries).length === 0) {
|
|
864
|
+
await this.loadEnvFromDisk();
|
|
865
|
+
}
|
|
866
|
+
const env = { ...process.env, ...this.envEntries };
|
|
867
|
+
if (this.httpUrl) {
|
|
868
|
+
env.VITE_DONE_HTTP_URL = this.httpUrl;
|
|
869
|
+
env.VITE_DONE_HTTP = this.httpUrl;
|
|
870
|
+
env.VITE_DONE_HTTP_BASE = this.httpUrl;
|
|
871
|
+
}
|
|
872
|
+
if (this.eventsUrl) {
|
|
873
|
+
env.VITE_DONE_EVENTS_URL = this.eventsUrl;
|
|
874
|
+
}
|
|
875
|
+
for (const [key, value] of Object.entries(this.envEntries)) {
|
|
876
|
+
if (key.startsWith("DONE_CONTRACT_")) {
|
|
877
|
+
env[`VITE_${key}`] = value;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
this.logInfo(`Starting frontend (${frontend.command})`);
|
|
881
|
+
this.frontendProcess = spawn(frontend.command, {
|
|
882
|
+
cwd: frontend.cwd,
|
|
883
|
+
env,
|
|
884
|
+
stdio: "inherit",
|
|
885
|
+
shell: true
|
|
886
|
+
});
|
|
887
|
+
this.frontendProcess.on("exit", (code) => {
|
|
888
|
+
if (!this.shuttingDown && code !== 0) {
|
|
889
|
+
this.logError(`Frontend exited with code ${code ?? 0}`);
|
|
890
|
+
}
|
|
891
|
+
});
|
|
892
|
+
if (frontend.open) {
|
|
893
|
+
setTimeout(() => {
|
|
894
|
+
openBrowser(frontend.url).catch((error) => {
|
|
895
|
+
this.logWarn(`Failed to open browser: ${error.message}`);
|
|
545
896
|
});
|
|
546
|
-
|
|
897
|
+
}, 1500);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
async stopFrontend() {
|
|
901
|
+
if (!this.frontendProcess) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
await new Promise((resolve) => {
|
|
905
|
+
const child = this.frontendProcess;
|
|
906
|
+
const timeout = setTimeout(() => {
|
|
907
|
+
try {
|
|
908
|
+
child.kill("SIGKILL");
|
|
909
|
+
} catch {
|
|
910
|
+
}
|
|
911
|
+
resolve();
|
|
912
|
+
}, 1000);
|
|
913
|
+
child.once("exit", () => {
|
|
914
|
+
clearTimeout(timeout);
|
|
915
|
+
resolve();
|
|
916
|
+
});
|
|
917
|
+
try {
|
|
918
|
+
child.kill("SIGINT");
|
|
919
|
+
} catch {
|
|
920
|
+
resolve();
|
|
921
|
+
}
|
|
922
|
+
this.frontendProcess = undefined;
|
|
547
923
|
});
|
|
924
|
+
}
|
|
925
|
+
async writeEnvFile(extra = {}) {
|
|
926
|
+
const env = {
|
|
927
|
+
...extra
|
|
928
|
+
};
|
|
929
|
+
if (this.httpUrl) {
|
|
930
|
+
env.DONE_HTTP_URL = this.httpUrl;
|
|
931
|
+
env.DONE_HTTP_BASE = this.httpUrl;
|
|
932
|
+
env.DONE_HTTP_LOCAL = this.httpUrl;
|
|
933
|
+
}
|
|
934
|
+
if (this.eventsUrl) {
|
|
935
|
+
env.DONE_EVENTS_URL = this.eventsUrl;
|
|
936
|
+
env.DONE_EVENTS_LOCAL = this.eventsUrl;
|
|
937
|
+
}
|
|
938
|
+
env.DONE_OWNER_ADDRESS = this.options.devConfig.owner;
|
|
939
|
+
env.DONE_DEV_UPDATED_AT = new Date().toISOString();
|
|
940
|
+
for (const contract of this.contracts) {
|
|
941
|
+
if (contract.address) {
|
|
942
|
+
env[`DONE_CONTRACT_${contract.envKey}`] = contract.address;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
this.envEntries = env;
|
|
946
|
+
await writeDevEnvFile(this.options.devConfig.envFile, env);
|
|
947
|
+
}
|
|
948
|
+
async loadEnvFromDisk() {
|
|
949
|
+
try {
|
|
950
|
+
this.envEntries = await readDevEnvFile(this.options.devConfig.envFile);
|
|
951
|
+
} catch (error) {
|
|
952
|
+
this.logWarn(`Unable to read ${this.options.devConfig.envFile}: ${error.message}`);
|
|
953
|
+
this.envEntries = {};
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
printSummary() {
|
|
957
|
+
this.logInfo(pc.bold("Local stack ready"));
|
|
958
|
+
if (this.httpUrl) {
|
|
959
|
+
console.log(` ${pc.dim("http")}: ${this.httpUrl}`);
|
|
960
|
+
}
|
|
961
|
+
if (this.eventsUrl) {
|
|
962
|
+
console.log(` ${pc.dim("events")}: ${this.eventsUrl}`);
|
|
963
|
+
}
|
|
964
|
+
if (this.contracts.length > 0) {
|
|
965
|
+
console.log(" Contracts:");
|
|
966
|
+
for (const contract of this.contracts) {
|
|
967
|
+
if (contract.address) {
|
|
968
|
+
console.log(` - ${contract.name}: ${contract.address}`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
const relEnv = path.relative(process.cwd(), this.options.devConfig.envFile) || this.options.devConfig.envFile;
|
|
973
|
+
console.log(` Env file: ${relEnv}`);
|
|
974
|
+
if (this.options.startFrontend) {
|
|
975
|
+
console.log(` Frontend: ${this.options.devConfig.frontend.url}`);
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
formatWorkspacePath(filePath) {
|
|
979
|
+
return path.relative(this.options.workspace.root, filePath) || filePath;
|
|
980
|
+
}
|
|
981
|
+
logInfo(message) {
|
|
982
|
+
console.log(DEV_LOG_PREFIX, message);
|
|
983
|
+
}
|
|
984
|
+
logWarn(message) {
|
|
985
|
+
console.warn(DEV_WARN_PREFIX, message);
|
|
986
|
+
}
|
|
987
|
+
logError(message, error) {
|
|
988
|
+
if (error) {
|
|
989
|
+
console.error(DEV_ERROR_PREFIX, `${message}: ${error.message}`);
|
|
990
|
+
} else {
|
|
991
|
+
console.error(DEV_ERROR_PREFIX, message);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
async function writeDevEnvFile(file, entries) {
|
|
996
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
997
|
+
const lines = ["# generated by done dev", `# ${new Date().toISOString()}`];
|
|
998
|
+
for (const key of Object.keys(entries).sort()) {
|
|
999
|
+
lines.push(`${key}=${entries[key]}`);
|
|
1000
|
+
}
|
|
1001
|
+
await fs.writeFile(file, `${lines.join("\n")}\n`, "utf8");
|
|
1002
|
+
}
|
|
1003
|
+
async function readDevEnvFile(file) {
|
|
1004
|
+
const data = await fs.readFile(file, "utf8");
|
|
1005
|
+
const entries = {};
|
|
1006
|
+
for (const line of data.split(/\r?\n/)) {
|
|
1007
|
+
if (!line || line.startsWith("#"))
|
|
1008
|
+
continue;
|
|
1009
|
+
const [key, ...rest] = line.split("=");
|
|
1010
|
+
if (!key || rest.length === 0)
|
|
1011
|
+
continue;
|
|
1012
|
+
entries[key.trim()] = rest.join("=").trim();
|
|
1013
|
+
}
|
|
1014
|
+
return entries;
|
|
1015
|
+
}
|
|
1016
|
+
function toEnvVarKey(name) {
|
|
1017
|
+
return name.replace(/[^a-zA-Z0-9]+/g, "_").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toUpperCase();
|
|
1018
|
+
}
|
|
1019
|
+
function openBrowser(url) {
|
|
1020
|
+
return new Promise((resolve, reject) => {
|
|
1021
|
+
const platform = process.platform;
|
|
1022
|
+
let child;
|
|
1023
|
+
try {
|
|
1024
|
+
if (platform === "darwin") {
|
|
1025
|
+
child = spawn("open", [url], { stdio: "ignore", detached: true });
|
|
1026
|
+
} else if (platform === "win32") {
|
|
1027
|
+
child = spawn("cmd", ["/c", "start", "", url], {
|
|
1028
|
+
stdio: "ignore",
|
|
1029
|
+
detached: true
|
|
1030
|
+
});
|
|
1031
|
+
} else {
|
|
1032
|
+
child = spawn("xdg-open", [url], { stdio: "ignore", detached: true });
|
|
1033
|
+
}
|
|
1034
|
+
child.unref();
|
|
1035
|
+
resolve();
|
|
1036
|
+
} catch (error) {
|
|
1037
|
+
reject(error);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
548
1040
|
}
|