@donezone/cli 0.1.1 → 0.1.2

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.
Files changed (3) hide show
  1. package/dist/done +0 -0
  2. package/dist/index.js +189 -14
  3. package/package.json +5 -4
package/dist/done ADDED
Binary file
package/dist/index.js CHANGED
@@ -13,20 +13,14 @@ program
13
13
  .description("Bundle Done contracts defined in done.json")
14
14
  .option("-j, --contracts <path>", "Path to done.json")
15
15
  .option("-n, --name <name...>", "Filter contract names")
16
- .action(() => {
17
- notImplemented("build");
18
- // TODO: build contracts
19
- /*
20
-
21
- Should accept blob pattern.
22
-
23
- See how packages/demo/serve.ts handles Bun.build
24
-
25
- Output compiled js artifacts to <cwd>/dist folder.
26
-
27
- If cwd is a contract workspace, build all contract packages in workspace.
28
-
29
- */
16
+ .action(async (options) => {
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
+ }
30
24
  });
31
25
  program
32
26
  .command("dev")
@@ -81,6 +75,50 @@ async function handleInit(rawName, options) {
81
75
  }
82
76
  await scaffoldWorkspace(rawName, options);
83
77
  }
78
+ async function handleBuild(options) {
79
+ const manifestInfo = await resolveManifest(options.contracts);
80
+ const contracts = manifestInfo.manifest.contracts ?? [];
81
+ if (contracts.length === 0) {
82
+ throw new Error(`No contracts defined in ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
83
+ }
84
+ const filters = (options.name ?? [])
85
+ .flatMap((pattern) => pattern.split(","))
86
+ .map((pattern) => pattern.trim())
87
+ .filter(Boolean);
88
+ const matchers = filters.map(createGlobMatcher);
89
+ const selected = matchers.length === 0
90
+ ? contracts
91
+ : contracts.filter((contract) => matchesContract(contract, manifestInfo.manifestDir, matchers));
92
+ if (selected.length === 0) {
93
+ throw new Error(`No contracts matched filters: ${filters.join(", ")}`);
94
+ }
95
+ console.log(`Building ${selected.length} contract${selected.length === 1 ? "" : "s"} from ${path.relative(process.cwd(), manifestInfo.manifestPath) || manifestInfo.manifestPath}`);
96
+ for (const contract of selected) {
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)})`);
120
+ }
121
+ }
84
122
  async function detectWorkspace(root) {
85
123
  const manifestPath = path.join(root, "done.json");
86
124
  const configPath = path.join(root, "done.config.json");
@@ -95,6 +133,50 @@ async function detectWorkspace(root) {
95
133
  const config = JSON.parse(configRaw);
96
134
  return { root, manifestPath, configPath, manifest, config };
97
135
  }
136
+ async function resolveManifest(contractsPathOverride) {
137
+ if (contractsPathOverride) {
138
+ const manifestPath = path.resolve(process.cwd(), contractsPathOverride);
139
+ const manifest = await readManifest(manifestPath);
140
+ return { manifestPath, manifestDir: path.dirname(manifestPath), manifest };
141
+ }
142
+ const workspace = await findWorkspace(process.cwd());
143
+ if (!workspace) {
144
+ throw new Error("Unable to find done.json. Run inside a Done workspace or pass --contracts <path>.");
145
+ }
146
+ return { manifestPath: workspace.manifestPath, manifestDir: path.dirname(workspace.manifestPath), manifest: workspace.manifest };
147
+ }
148
+ async function findWorkspace(startDir) {
149
+ let current = path.resolve(startDir);
150
+ while (true) {
151
+ const workspace = await detectWorkspace(current);
152
+ if (workspace) {
153
+ return workspace;
154
+ }
155
+ const parent = path.dirname(current);
156
+ if (parent === current) {
157
+ return null;
158
+ }
159
+ current = parent;
160
+ }
161
+ }
162
+ async function readManifest(manifestPath) {
163
+ let raw;
164
+ try {
165
+ raw = await fs.readFile(manifestPath, "utf8");
166
+ }
167
+ catch (error) {
168
+ if (error.code === "ENOENT") {
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}`);
178
+ }
179
+ }
98
180
  async function scaffoldWorkspace(rawName, options) {
99
181
  const slug = toSlug(rawName);
100
182
  if (!slug) {
@@ -302,6 +384,99 @@ function normalizeJsonPath(relativePath) {
302
384
  const normalized = relativePath.split(path.sep).join("/");
303
385
  return normalized.startsWith("./") || normalized.startsWith("../") ? normalized : `./${normalized}`;
304
386
  }
387
+ function matchesContract(contract, manifestDir, matchers) {
388
+ const candidates = [];
389
+ if (contract.name) {
390
+ candidates.push(normalizeMatchValue(contract.name));
391
+ }
392
+ if (contract.entry) {
393
+ candidates.push(normalizeMatchValue(contract.entry));
394
+ candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.entry)));
395
+ }
396
+ if (contract.outFile) {
397
+ candidates.push(normalizeMatchValue(contract.outFile));
398
+ candidates.push(normalizeMatchValue(path.resolve(manifestDir, contract.outFile)));
399
+ }
400
+ return matchers.some((matcher) => candidates.some((candidate) => matcher(candidate)));
401
+ }
402
+ function createGlobMatcher(pattern) {
403
+ const normalized = normalizeMatchValue(pattern);
404
+ const hasWildcards = /[\*\?]/.test(normalized);
405
+ if (!hasWildcards) {
406
+ return (candidate) => normalizeMatchValue(candidate) === normalized;
407
+ }
408
+ const escaped = escapeRegExp(normalized)
409
+ .replace(/\\\*/g, ".*")
410
+ .replace(/\\\?/g, ".");
411
+ const regex = new RegExp(`^${escaped}$`);
412
+ return (candidate) => regex.test(normalizeMatchValue(candidate));
413
+ }
414
+ function normalizeMatchValue(value) {
415
+ if (!value) {
416
+ return "";
417
+ }
418
+ let normalized = value.replace(/\\/g, "/");
419
+ if (normalized.startsWith("./")) {
420
+ normalized = normalized.slice(2);
421
+ }
422
+ return normalized;
423
+ }
424
+ function escapeRegExp(value) {
425
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
426
+ }
427
+ async function ensureEntryExists(entryPath, contractName) {
428
+ try {
429
+ const stats = await fs.stat(entryPath);
430
+ if (!stats.isFile()) {
431
+ const relPath = path.relative(process.cwd(), entryPath) || entryPath;
432
+ throw new Error(`Entry path for contract '${contractName}' is not a file (${relPath})`);
433
+ }
434
+ }
435
+ catch (error) {
436
+ if (error.code === "ENOENT") {
437
+ const rel = path.relative(process.cwd(), entryPath) || entryPath;
438
+ throw new Error(`Entry file for contract '${contractName}' not found (${rel})`);
439
+ }
440
+ throw error;
441
+ }
442
+ }
443
+ async function runBunBuild(entryPath, outFilePath) {
444
+ const bun = getBunRuntime();
445
+ const result = await bun.build({
446
+ entrypoints: [entryPath],
447
+ target: "bun",
448
+ format: "esm",
449
+ splitting: false,
450
+ sourcemap: "none",
451
+ external: ["bun:*", "node:*"],
452
+ define: {
453
+ "process.env.NODE_ENV": '"production"',
454
+ },
455
+ });
456
+ if (!result.success) {
457
+ const logs = (result.logs ?? []).map((log) => log?.message ?? String(log));
458
+ throw new Error(logs.join("\n") || "Bun.build failed");
459
+ }
460
+ const [output] = result.outputs;
461
+ if (!output) {
462
+ throw new Error("Bun.build did not emit an output file");
463
+ }
464
+ const bundledSource = await output.text();
465
+ await fs.writeFile(outFilePath, bundledSource);
466
+ }
467
+ function getBunRuntime() {
468
+ const bun = globalThis.Bun;
469
+ if (!bun || typeof bun.build !== "function") {
470
+ throw new Error("Bun runtime unavailable. Rebuild the done CLI using Bun to embed the runtime.");
471
+ }
472
+ return bun;
473
+ }
474
+ function formatDuration(ms) {
475
+ if (ms < 1000) {
476
+ return `${ms}ms`;
477
+ }
478
+ return `${(ms / 1000).toFixed(2)}s`;
479
+ }
305
480
  function toSlug(input) {
306
481
  return input
307
482
  .trim()
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@donezone/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
7
- "done": "dist/index.js"
7
+ "done": "dist/done"
8
8
  },
9
9
  "files": [
10
10
  "dist"
11
11
  ],
12
12
  "scripts": {
13
- "build": "tsc -p tsconfig.json",
14
- "clean": "rimraf dist"
13
+ "build": "tsc -p tsconfig.json && bun build ./src/index.ts --compile --outfile dist/done",
14
+ "clean": "rimraf dist",
15
+ "test": "bun test"
15
16
  },
16
17
  "dependencies": {
17
18
  "@donezone/core": "^0.1.0",