diff --git a/bin/cli/src/commands/build-command.test.ts b/bin/cli/src/commands/build-command.test.ts index e351092..b5786d7 100644 --- a/bin/cli/src/commands/build-command.test.ts +++ b/bin/cli/src/commands/build-command.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; import { resolve } from "node:path"; import { SDK_CONFIGS, type SdkConfig, type ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { buildService } from "./build-command"; +import { callDockerBuild } from "./build-command"; describe("build-command", () => { describe("buildService - Docker command generation", () => { @@ -41,7 +41,7 @@ describe("build-command", () => { sdk: "jambrains-1cfc41c", }; - await buildService(service, "/test/project"); + await callDockerBuild(service, "/test/project"); expect(mockSpawn).toHaveBeenCalledTimes(1); const spawnCall = mockSpawn.mock.calls[0]; @@ -64,7 +64,7 @@ describe("build-command", () => { sdk: "jade-0.0.15-pre.1", }; - await buildService(service, "/test/project"); + await callDockerBuild(service, "/test/project"); expect(mockSpawn).toHaveBeenCalledTimes(1); const spawnCall = mockSpawn.mock.calls[0]; @@ -91,7 +91,7 @@ describe("build-command", () => { sdk: customSdk, }; - await buildService(service, "/test/project"); + await callDockerBuild(service, "/test/project"); expect(mockSpawn).toHaveBeenCalledTimes(1); const spawnCall = mockSpawn.mock.calls[0]; @@ -132,8 +132,8 @@ describe("build-command", () => { sdk: "jambrains-1cfc41c", }; - expect(buildService(service, "/test/project")).rejects.toThrow(); - expect(buildService(service, "/test/project")).rejects.toThrow("Build failed for service 'failing-service'"); + expect(callDockerBuild(service, "/test/project")).rejects.toThrow(); + expect(callDockerBuild(service, "/test/project")).rejects.toThrow("Build failed for service 'failing-service'"); }); test("should return build output on success", async () => { @@ -164,7 +164,7 @@ describe("build-command", () => { sdk: "jambrains-1cfc41c", }; - const output = await buildService(service, "/test/project"); + const output = await callDockerBuild(service, "/test/project"); expect(output).toBe(expectedOutput); }); }); @@ -208,7 +208,7 @@ describe("build-command", () => { }; const projectRoot = "/absolute/project/root"; - await buildService(service, projectRoot); + await callDockerBuild(service, projectRoot); expect(mockSpawn).toHaveBeenCalledTimes(1); expect(mockSpawn.mock.calls.length).toBeGreaterThan(0); diff --git a/bin/cli/src/commands/build-command.ts b/bin/cli/src/commands/build-command.ts index 7fc26e5..e2fee22 100644 --- a/bin/cli/src/commands/build-command.ts +++ b/bin/cli/src/commands/build-command.ts @@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises"; import { join, relative, resolve } from "node:path"; import * as p from "@clack/prompts"; import type { ServiceConfig } from "@fluffylabs/jammin-sdk"; -import { getJamFiles, getServiceConfigs, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; +import { copyJamToDist, getJamFiles, getServiceConfigs, SDK_CONFIGS } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; /** @@ -17,7 +17,7 @@ export class DockerError extends Error { } } -export async function buildService(service: ServiceConfig, projectRoot: string): Promise { +export async function callDockerBuild(service: ServiceConfig, projectRoot: string): Promise { const sdk = typeof service.sdk === "string" ? SDK_CONFIGS[service.sdk] : service.sdk; const servicePath = resolve(projectRoot, service.path); @@ -39,6 +39,33 @@ export async function buildService(service: ServiceConfig, projectRoot: string): return combinedOutput; } +export async function buildService(service: ServiceConfig, projectRoot: string): Promise { + const timestamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-"); + const logsDir = join(projectRoot, "logs"); + await mkdir(logsDir, { recursive: true }); + const logFileName = `jammin-build-${service.name}-${timestamp}.log`; + const logFilePath = join(logsDir, logFileName); + + const servicePath = resolve(projectRoot, service.path); + const output = await callDockerBuild(service, projectRoot); + + const files = await getJamFiles(servicePath); + + const file = files.length > 0 ? files[0] : undefined; + let distPath: string | undefined; + if (file) { + distPath = await copyJamToDist(file, service.name, projectRoot); + } else { + throw new Error(`Failed to find generated file for: '${service.name}'`); + } + + if (output) { + await Bun.write(logFilePath, output); + } + + return relative(projectRoot, distPath); +} + /** * Build command definition */ @@ -65,60 +92,17 @@ Examples: s.stop("✅ Configuration loaded"); const projectRoot = process.cwd(); - const logsDir = join(projectRoot, "logs"); - await mkdir(logsDir, { recursive: true }); - let buildFailed = false; + const buildFailed = false; for (const service of services) { p.log.info("--------------------------------"); + s.start(`Building service '${service.name}'...`); - const timestamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-"); - const logFileName = `jammin-build-${service.name}-${timestamp}.log`; - const logFilePath = join(logsDir, logFileName); - - const servicePath = resolve(projectRoot, service.path); - const filesBefore = await getJamFiles(servicePath); - - let output: string | undefined; - - // Build service and save log - try { - output = await buildService(service, projectRoot); - s.stop(`✅ Service '${service.name}' built successfully`); - } catch (error) { - buildFailed = true; - let errorMessage: string; - if (error instanceof DockerError) { - output = error.output; - errorMessage = error.message; - } else { - const errorObj = error instanceof Error ? error : new Error(String(error)); - errorMessage = errorObj.message; - } - s.stop(`❌ Failed to build service '${service.name}': ${errorMessage}`); - } - - // Find new or updated .jam files - const filesAfter = await getJamFiles(servicePath); - const newFiles: string[] = []; - - for (const [file, mtimeAfter] of filesAfter) { - const mtimeBefore = filesBefore.get(file); - if (mtimeBefore === undefined || mtimeAfter > mtimeBefore) { - newFiles.push(file); - } - } - - if (newFiles.length > 0) { - const filesList = newFiles.map((f) => ` - ${relative(projectRoot, f)}`).join("\n"); - p.log.info(`🎁 Output files for '${service.name}':\n${filesList}`); - } - - if (output) { - await Bun.write(logFilePath, output); - p.log.info(`📝 Log saved: ${relative(projectRoot, logFilePath)}`); - } + const createdFile = await buildService(service, projectRoot); + s.stop(`✅ Service '${service.name}' built successfully`); + + p.log.message(`🎁 Output file: ${createdFile}`); } p.log.info("--------------------------------"); diff --git a/bin/cli/src/commands/deploy-command.ts b/bin/cli/src/commands/deploy-command.ts index 046744a..5c1c24b 100644 --- a/bin/cli/src/commands/deploy-command.ts +++ b/bin/cli/src/commands/deploy-command.ts @@ -1,13 +1,5 @@ import * as p from "@clack/prompts"; -import { - generateGenesis, - generateServiceOutput, - getJamFiles, - getServiceConfigs, - loadBuildConfig, - type ServiceBuildOutput, - saveStateFile, -} from "@fluffylabs/jammin-sdk"; +import { generateGenesis, getServiceConfigs, loadServices, saveStateFile } from "@fluffylabs/jammin-sdk"; import { Command } from "commander"; import { buildService } from "./build-command"; @@ -46,66 +38,14 @@ Examples: s.stop("✅ Building was successful!"); s.start("Generating Genesis State..."); - - const serviceJamFiles = new Map(); - for (const service of services) { - // NOTE: Taking only first jam blob (closest to service path directory) - // since each service should produce one blob - // if they produce more its probably for testing purposes - const jamFiles = await getJamFiles(service.path); - const jamFile = jamFiles.keys().next().value; - if (!jamFile) { - throw new DeployError(`Cannot find a jam file for ${service.name} service`); - } - serviceJamFiles.set(service.name, jamFile); - } - - if (serviceJamFiles.size === 0) { - throw new DeployError("No JAM files found"); - } - - // Get deployment config for services - const buildConfig = await loadBuildConfig(); - const serviceDeploymentConfigs = buildConfig.deployment?.services ?? {}; - - // generate ids avoiding collisions - const usedIds = new Set(); - for (const service of services) { - const deploymentConfig = serviceDeploymentConfigs[service.name]; - if (deploymentConfig?.id !== undefined) { - usedIds.add(deploymentConfig.id); - } - } - let nextAvailableId = 0; - const getNextAvailableId = () => { - while (usedIds.has(nextAvailableId)) { - nextAvailableId++; - } - return nextAvailableId++; - }; - - const buildOutputs: ServiceBuildOutput[] = await Promise.all( - services.map(async (service) => { - const jamFile = serviceJamFiles.get(service.name); - if (!jamFile) { - throw new DeployError(`Jam file not found for service ${service.name}`); - } - - const deploymentConfig = serviceDeploymentConfigs[service.name]; - const serviceId = deploymentConfig?.id ?? getNextAvailableId(); - - return generateServiceOutput(jamFile, serviceId, deploymentConfig?.storage, deploymentConfig?.info); - }), - ); - - const genesisOutput = `${projectRoot}/genesis.json`; - await saveStateFile(generateGenesis(buildOutputs), genesisOutput); - + const buildOutputs = await loadServices(projectRoot); + const genesisOutput = "dist/genesis.json"; + await saveStateFile(generateGenesis(buildOutputs), `${projectRoot}/${genesisOutput}`); s.stop("✅ Genesis state generated!"); p.log.info(`Found ${buildOutputs.length} service(s)`); - p.note(`🎁 Genesis file: ${genesisOutput}`); + p.log.message(`🎁 Generated file: ${genesisOutput}`); p.outro("✅ Deployment was successful!"); }); diff --git a/packages/jammin-sdk/utils/file-utils.ts b/packages/jammin-sdk/utils/file-utils.ts index bb0531f..d1e24dd 100644 --- a/packages/jammin-sdk/utils/file-utils.ts +++ b/packages/jammin-sdk/utils/file-utils.ts @@ -1,4 +1,4 @@ -import { readdir, stat } from "node:fs/promises"; +import { copyFile, mkdir, readdir, stat } from "node:fs/promises"; import { homedir } from "node:os"; import { join, resolve } from "node:path"; @@ -51,23 +51,18 @@ export async function findConfigFile(fileName: string, startDir: string = proces } /** - * Recursively get all jam files in a directory with their modification times + * Recursively get all jam files in a directory */ -export async function getJamFiles(dirPath: string): Promise> { - const files = new Map(); +export async function getJamFiles(dirPath: string): Promise { + const files: string[] = []; try { const entries = await readdir(dirPath, { withFileTypes: true, recursive: true }); for (const entry of entries) { if (entry.name.endsWith(".jam")) { - try { - const fullPath = resolve(entry.parentPath, entry.name); - const stats = await stat(fullPath); - files.set(fullPath, stats.mtimeMs); - } catch { - // Ignore stat errors - } + const fullPath = resolve(entry.parentPath, entry.name); + files.push(fullPath); } } } catch { @@ -103,3 +98,16 @@ export async function updatePackageJson( await Bun.write(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`); } + +/** + * Copy a .jam file to the dist/ directory with a service-specific name + */ +export async function copyJamToDist(jamFilePath: string, serviceName: string, projectRoot: string): Promise { + const distDir = join(projectRoot, "dist"); + await mkdir(distDir, { recursive: true }); + + const destPath = join(distDir, `${serviceName}.jam`); + await copyFile(jamFilePath, destPath); + + return destPath; +} diff --git a/packages/jammin-sdk/utils/generate-service-output.ts b/packages/jammin-sdk/utils/generate-service-output.ts index 53a1afb..25755e9 100644 --- a/packages/jammin-sdk/utils/generate-service-output.ts +++ b/packages/jammin-sdk/utils/generate-service-output.ts @@ -1,14 +1,52 @@ -import { resolve } from "node:path"; +import { join, resolve } from "node:path"; import { type ServiceId as ServiceIdType, tryAsServiceGas, tryAsServiceId, tryAsTimeSlot } from "@typeberry/lib/block"; import { BytesBlob } from "@typeberry/lib/bytes"; import { tryAsU32, tryAsU64 } from "@typeberry/lib/numbers"; +import type { ServiceAccountInfo } from "@typeberry/lib/state"; +import { loadBuildConfig } from "../config/config-loader.js"; import { ServiceId } from "../types.js"; export interface ServiceBuildOutput { id: ServiceIdType; code: BytesBlob; storage?: Record; - info?: Partial; + info?: Partial; +} + +/** + * Load services from the dist/ directory + */ +export async function loadServices(projectRoot: string = process.cwd()): Promise { + const outputs: ServiceBuildOutput[] = []; + const config = await loadBuildConfig(); + const serviceDeployConfigs = config.deployment?.services ?? {}; + const usedIds = new Set(); + + // Prepare ServiceIds + for (const service of config.services) { + const deployInfo = serviceDeployConfigs[service.name]; + if (deployInfo?.id !== undefined) { + usedIds.add(deployInfo.id); + } + } + + let nextId = 0; + const getNextAvailableId = () => { + while (usedIds.has(nextId)) { + nextId++; + } + return nextId++; + }; + + // Generate service build outputs + for (const service of config.services) { + const jamFilePath = join(projectRoot, "dist", `${service.name}.jam`); + const deployConfig = serviceDeployConfigs[service.name]; + const serviceId = deployConfig?.id ?? getNextAvailableId(); + outputs.push(await generateServiceOutput(jamFilePath, serviceId, deployConfig?.storage, deployConfig?.info)); + } + + return outputs; } export async function generateServiceOutput(