diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f55014c..1a25ac4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,8 +8,9 @@ on: required: true type: choice options: - - compact-tools-cli - - compact-tools-simulator + - compact-builder + - compact-cli + - compact-simulator version_bump: description: "Version bump type" required: true @@ -46,10 +47,13 @@ jobs: id: pkg run: | case "${{ inputs.package }}" in - "compact-tools-cli") + "compact-builder") + echo "dir=builder" >> $GITHUB_OUTPUT + ;; + "compact-cli") echo "dir=cli" >> $GITHUB_OUTPUT ;; - "compact-tools-simulator") + "compact-simulator") echo "dir=simulator" >> $GITHUB_OUTPUT ;; esac diff --git a/.gitignore b/.gitignore index 2a32ed6..075ffa5 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,5 @@ coverage *~ *temp + +.claude/ diff --git a/README.md b/README.md index 781a1c6..954c236 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.26.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/minokawa-0-18-26-0) +[![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.29.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact/) [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/OpenZeppelin/compact-tools/badge)](https://api.securityscorecards.dev/projects/github.com/OpenZeppelin/compact-tools) @@ -8,32 +8,38 @@ This project extends the Midnight Network with additional developer tooling. Tools for compiling, building, and testing Compact smart contracts. This is a monorepo containing: -- `packages/cli`: CLI utilities to run the Compact compiler and builder -- `packages/simulator`: TypeScript simulator to run and test Compact contracts locally +- [`packages/builder`](./packages/builder) — programmatic library that drives the Compact compiler + builder +- [`packages/cli`](./packages/cli) — thin bin wrapper around the builder library (`compact-compiler`, `compact-builder`) +- [`packages/simulator`](./packages/simulator) — TypeScript simulator to run and test Compact contracts locally -## External usage (via git submodule until npm publish) +See each package's README for usage, options, and examples. -Until packages are published to the npm registry, you can consume this repo from another project using a git submodule: +## Installation + +Pick the package that matches what you need: + +```bash +# Programmatic library — call the compiler/builder from TypeScript +yarn add --dev @openzeppelin/compact-builder + +# CLI bins (compact-compiler, compact-builder) for use in package.json scripts +yarn add --dev @openzeppelin/compact-cli + +# Simulator — test Compact contracts locally +yarn add --dev @openzeppelin/compact-simulator +``` + +`compact-cli` depends transitively on `compact-builder`, so installing the CLI +gives you both the binaries and the underlying library. ```bash -# In your project -git submodule add https://github.com/OpenZeppelin/compact-tools -git submodule update --init --recursive - -# Install and build the tools -yarn --cwd tools/compact-tools install -yarn --cwd tools/compact-tools build - -# Use the simulator as a local dependency -# package.json -"devDependencies": { - "@openzeppelin/compact-tools-simulator": "file:./compact-tools/packages/simulator" -} -yarn install - -# Call the CLIs directly or via scripts -node compact-tools/packages/cli/dist/runCompiler.js --help -node compact-tools/packages/cli/dist/runBuilder.js --help +yarn compact-compiler --help +yarn compact-builder --help +``` + +```ts +import { CompactCompiler, CompactBuilder } from '@openzeppelin/compact-builder'; +import { createSimulator } from '@openzeppelin/compact-simulator'; ``` ## Requirements @@ -48,13 +54,13 @@ Confirm your Compact toolchain: ```bash $ compact compile --version -Compactc version: 0.28.0 -0.28.0 +Compactc version: 0.29.0 +0.29.0 ``` -## Getting started +## Development -Install dependencies at the repo root: +Clone the repo and install dependencies at the root: ```bash nvm install @@ -86,45 +92,6 @@ Clean generated artifacts: yarn clean ``` -## Packages - -### `@openzeppelin/compact-tools-cli` ([packages/cli](./packages/cli)) - -CLI utilities for compiling and building Compact smart contracts. - -**Quickstart:** - -```bash -# Compile all .compact files -compact-compiler - -# Skip ZK proofs for faster development builds -compact-compiler --skip-zk - -# Compile specific directory -compact-compiler --dir security - -# Full build (compile + TypeScript + copy artifacts) -compact-builder -``` - -See [packages/cli/README.md](./packages/cli/README.md) for full documentation including all options, programmatic API, and examples. - -### `@openzeppelin/compact-tools-simulator` ([packages/simulator](./packages/simulator)) - -TypeScript simulator for testing Compact contracts locally. - -**Quickstart:** - -```ts -import { createSimulator } from '@openzeppelin/compact-tools-simulator'; - -const simulator = createSimulator({}); -// Deploy and execute contract circuits, inspect state, etc. -``` - -See package tests in `packages/simulator/src/integration` and `src/unit` for full examples. - ## Contributing Before opening a PR, please read `CODE_OF_CONDUCT.md`. Use the root scripts to build, test, and format. For targeted work inside a package, run the scripts in that package directory. diff --git a/RELEASING.md b/RELEASING.md index 500eae5..c9635f6 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,5 +1,7 @@ # Releasing +## Running the workflow + 1. Go to "Release Package" in Actions. 2. Click on the "Run workflow" dropdown menu. 3. Choose the package to release and the version bump type. @@ -14,4 +16,27 @@ - Bump the version. - Create a git tag. - Publish the package to npm. -6. Once published, go to "Releases" and create a GitHub release using the generated tag. \ No newline at end of file +6. Once published, go to "Releases" and create a GitHub release using the generated tag. + +## First-release order + +There's a one-step dependency chain across the three published packages: + +```text +compact-cli (bin wrapper) + └─ depends on compact-builder +compact-builder (library) +compact-simulator (library) +``` + +The `workspace:^` dep is rewritten by yarn into the resolved version at +`yarn pack` time. For the very first release, publish in dependency order so +each dependent finds its deps already on npm: + +1. `compact-builder` (no internal deps) +2. `compact-simulator` (no internal deps) +3. `compact-cli` (depends on `compact-builder`; pull `main` first so the bump + commit is present locally before triggering) + +After the first release, the three packages version independently — bump any +one of them in isolation without re-publishing the others. diff --git a/package.json b/package.json index a601bc2..a9bb156 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin/compact-tools", + "name": "compact-tools-monorepo", "description": "Tools for compiling, building and testing Compact smart contracts", "private": true, "packageManager": "yarn@4.10.3", diff --git a/packages/builder/README.md b/packages/builder/README.md new file mode 100644 index 0000000..aeb58e8 --- /dev/null +++ b/packages/builder/README.md @@ -0,0 +1,71 @@ +# @openzeppelin/compact-builder + +Programmatic library for compiling and building Compact smart contracts on the +Midnight network. Drives the `compactc` toolchain with progress reporting, +structured error handling, and configurable output layouts. + +This is the **library** — it ships no CLI binaries. If you want the bins +(`compact-compiler`, `compact-builder`) for use in `package.json` scripts, +install [`@openzeppelin/compact-cli`](../cli) instead, which is a thin +wrapper around this library. + +## Install + +```bash +yarn add --dev @openzeppelin/compact-builder +``` + +## Quick Start + +```ts +import { CompactCompiler, CompactBuilder } from '@openzeppelin/compact-builder'; + +// Compile all .compact files in src/ to artifacts/ +await new CompactCompiler({ flags: '--skip-zk' }).compile(); + +// Or run the full build pipeline (compile + dist assembly) +const builder = new CompactBuilder({ + cleanDist: true, + hierarchical: true, + exclude: ['Mock*', '*/archive/*'], + copyToDist: ['package.json', '../README.md'], +}); +await builder.build(); +``` + +## Public API + +```ts +// Orchestrators +export class CompactCompiler { /* … */ } +export class CompactBuilder { /* … */ } + +// Service classes (use for advanced custom pipelines) +export class EnvironmentValidator { /* … */ } +export class FileDiscovery { /* … */ } +export class CompilerService { /* … */ } +export const UIService = { /* … */ }; + +// Option types +export interface CompilerOptions { /* flags, targetDir, version, hierarchical, srcDir, outDir, exclude */ } +export type BuilderOptions = CompilerOptions & { + cleanDist?: boolean; + copyToDist?: string[]; +}; + +// Errors +export class CompactCliNotFoundError extends Error { /* … */ } +export class CompilationError extends Error { /* … */ } +export class DirectoryNotFoundError extends Error { /* … */ } +``` + +## See also + +- [`@openzeppelin/compact-cli`](https://www.npmjs.com/package/@openzeppelin/compact-cli) — bin wrapper around this library +- [`@openzeppelin/compact-simulator`](https://www.npmjs.com/package/@openzeppelin/compact-simulator) — TypeScript simulator for testing Compact contracts locally + +See the [monorepo README](https://github.com/OpenZeppelin/compact-tools#readme) for the full developer guide. + +## License + +MIT diff --git a/packages/builder/package.json b/packages/builder/package.json new file mode 100644 index 0000000..5c4667b --- /dev/null +++ b/packages/builder/package.json @@ -0,0 +1,50 @@ +{ + "name": "@openzeppelin/compact-builder", + "description": "Programmatic library for compiling and building Compact smart contracts", + "version": "0.0.1", + "keywords": [ + "compact", + "midnight", + "compiler", + "builder", + "library" + ], + "author": "OpenZeppelin Community ", + "license": "MIT", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "engines": { + "node": ">=20" + }, + "scripts": { + "build": "tsc -p .", + "types": "tsc -p tsconfig.json --noEmit", + "test": "yarn vitest run", + "clean": "git clean -fXd" + }, + "devDependencies": { + "@tsconfig/node24": "^24.0.3", + "@types/node": "24.10.1", + "@types/shell-quote": "^1.7.5", + "typescript": "^5.9.3", + "vitest": "^4.0.15" + }, + "dependencies": { + "chalk": "^5.6.2", + "log-symbols": "^7.0.0", + "ora": "^9.0.0", + "shell-quote": "^1.8.3" + } +} diff --git a/packages/builder/src/Builder.ts b/packages/builder/src/Builder.ts new file mode 100755 index 0000000..1c10b69 --- /dev/null +++ b/packages/builder/src/Builder.ts @@ -0,0 +1,273 @@ +#!/usr/bin/env node + +import { exec } from 'node:child_process'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { CompactCompiler } from './Compiler.ts'; +import { isPromisifiedChildProcessError } from './types/errors.ts'; +import { + type BuilderOnlyOptions, + type BuilderOptions, + type BuildStep, + DEFAULT_EXCLUDE_PATTERNS, +} from './types/options.ts'; +import { buildFindExcludes, shellQuote } from './utils.ts'; + +// Re-export public types so consumers keep importing them from './Builder.js'. +export type { BuilderOnlyOptions, BuilderOptions }; + +// Promisified exec for async execution +const execAsync = promisify(exec); + +/** + * A class to handle the build process for a project. + * Runs CompactCompiler as a prerequisite, then executes build steps (TypeScript compilation, + * artifact copying, etc.) with progress feedback and colored output for success and error states. + * + * Build steps are derived from {@link BuilderOptions} so consumers can produce a + * publishable distribution that matches their package conventions + * (preserve source tree, copy metadata, clean dist, custom excludes). + * + * @notice `cmd` scripts discard `stderr` output and fail silently because this is + * handled in `executeStep`. + * + * @example + * ```typescript + * // Default: flatten .compact files, exclude Mock* + * const builder = new CompactBuilder({ flags: '--skip-zk' }); + * + * // Library publish: clean dist, hierarchical tree, exclude mocks + archive, copy metadata. + * const builder = new CompactBuilder({ + * cleanDist: true, + * hierarchical: true, + * exclude: ['Mock*', '*\/archive\/*'], + * copyToDist: ['package.json', '../README.md'], + * }); + * builder.build().catch(err => console.error(err)); + * ``` + */ +export class CompactBuilder { + private readonly options: BuilderOptions; + private readonly steps: BuildStep[]; + + /** + * Constructs a new CompactBuilder instance. + * @param options - Compiler + builder options (see {@link BuilderOptions}). + */ + constructor(options: BuilderOptions = {}) { + this.options = options; + this.steps = this.buildSteps(); + } + + /** + * Factory method to create a CompactBuilder from command-line arguments. + * + * @param args - Array of command-line arguments + * @param env - Environment variables (defaults to process.env) + * @returns New CompactBuilder instance configured from arguments + */ + static fromArgs( + args: string[], + env: typeof process.env = process.env, + ): CompactBuilder { + const options = CompactBuilder.parseArgs(args, env); + return new CompactBuilder(options); + } + + /** + * Parses command-line arguments into {@link BuilderOptions}. + * Builder-only flags are extracted here; remaining args are forwarded to + * {@link CompactCompiler.parseArgs} for compiler-side parsing. + * + * Builder-only flags (compiler flags like `--hierarchical` and `--exclude` + * are forwarded to {@link CompactCompiler.parseArgs}): + * - `--clean-dist` - rm -rf dist before building + * - `--copy ` - copy an extra file into dist/ (repeatable) + * + * @throws {Error} If `--copy` is provided without a value. + */ + static parseArgs( + args: string[], + env: typeof process.env = process.env, + ): BuilderOptions { + const builderOnly: BuilderOnlyOptions = {}; + const compilerArgs: string[] = []; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (arg === '--clean-dist') { + builderOnly.cleanDist = true; + } else if (arg === '--copy') { + const value = args[i + 1]; + if (value === undefined || value.startsWith('--')) { + throw new Error('--copy flag requires a path'); + } + builderOnly.copyToDist ??= []; + builderOnly.copyToDist.push(value); + i++; + } else { + compilerArgs.push(arg); + } + } + + const compilerOptions = CompactCompiler.parseArgs(compilerArgs, env); + return { ...compilerOptions, ...builderOnly }; + } + + /** + * Executes the full build process: compiles .compact files first, then runs build steps. + * Displays progress with spinners and outputs results in color. + * + * @returns A promise that resolves when all steps complete successfully + * @throws Error if compilation or any build step fails + */ + public async build(): Promise { + // Run compact compilation as a prerequisite. CompactCompiler ignores + // BuilderOnlyOptions because they aren't in its resolved shape. + const compiler = new CompactCompiler(this.options); + await compiler.compile(); + + for (const [index, step] of this.steps.entries()) { + await this.executeStep(step, index, this.steps.length); + } + } + + /** + * Exposes the resolved build steps. Public for testing/introspection. + */ + public getSteps(): readonly BuildStep[] { + return this.steps; + } + + /** + * Assembles the build-step pipeline from the configured options. + */ + private buildSteps(): BuildStep[] { + const srcDir = this.options.srcDir ?? 'src'; + const quotedSrc = shellQuote(srcDir); + const excludes = buildFindExcludes( + this.options.exclude ?? DEFAULT_EXCLUDE_PATTERNS, + ); + const steps: BuildStep[] = []; + + if (this.options.cleanDist) { + steps.push({ + cmd: 'rm -rf dist && mkdir -p dist', + msg: 'Cleaning dist directory', + shell: '/bin/bash', + }); + } + + steps.push({ + cmd: 'tsc --project tsconfig.build.json', + msg: 'Compiling TypeScript', + }); + + steps.push({ + cmd: `mkdir -p dist/artifacts && cp -Rf ${quotedSrc}/artifacts/* dist/artifacts/ 2>/dev/null || true`, + msg: 'Copying artifacts', + shell: '/bin/bash', + }); + + if (this.options.hierarchical) { + steps.push({ + // biome-ignore-start lint/suspicious/noUselessEscapeInString: shell vars must survive JS template-literal interpolation + cmd: ` + SRC_DIR=${quotedSrc} + find "$SRC_DIR" -type f -name '*.compact' ${excludes} | while read -r file; do + rel_path="\${file#$SRC_DIR/}" + mkdir -p "dist/$(dirname "$rel_path")" + cp "$file" "dist/$rel_path" + done + `, + // biome-ignore-end lint/suspicious/noUselessEscapeInString: shell vars must survive JS template-literal interpolation + msg: 'Copying .compact files (preserving structure)', + shell: '/bin/bash', + }); + } else { + steps.push({ + cmd: `mkdir -p dist && find ${quotedSrc} -type f -name '*.compact' ${excludes} -exec cp {} dist/ \\; 2>/dev/null || true`, + msg: 'Copying .compact files', + shell: '/bin/bash', + }); + } + + const copyTargets = this.options.copyToDist ?? []; + if (copyTargets.length > 0) { + const copyCmds = copyTargets + .map((path) => `cp ${shellQuote(path)} dist/ 2>/dev/null || true`) + .join(' && '); + steps.push({ + cmd: `mkdir -p dist && ${copyCmds}`, + msg: 'Copying additional files to dist', + shell: '/bin/bash', + }); + } + + return steps; + } + + /** + * Executes a single build step. + * Runs the command, shows a spinner, and prints output with indentation. + * + * @param step - The build step containing command and message + * @param index - Current step index (0-based) for progress display + * @param total - Total number of steps for progress display + * @returns A promise that resolves when the step completes successfully + * @throws Error if the step fails + */ + private async executeStep( + step: BuildStep, + index: number, + total: number, + ): Promise { + const stepLabel: string = `[${index + 1}/${total}]`; + const spinner: Ora = ora(`[BUILD] ${stepLabel} ${step.msg}`).start(); + + try { + const { stdout, stderr }: { stdout: string; stderr: string } = + await execAsync(step.cmd, { + shell: step.shell, // Only pass shell where needed + }); + spinner.succeed(`[BUILD] ${stepLabel} ${step.msg}`); + this.printOutput(stdout, chalk.cyan); + this.printOutput(stderr, chalk.yellow); // Show stderr (warnings) in yellow if present + } catch (error: unknown) { + spinner.fail(`[BUILD] ${stepLabel} ${step.msg}`); + if (isPromisifiedChildProcessError(error)) { + this.printOutput(error.stdout, chalk.cyan); + this.printOutput(error.stderr, chalk.red); + // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason + console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); + } else if (error instanceof Error) { + // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason + console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); + } + + // Library code must not call process.exit — let the caller (CLI wrapper + // or programmatic consumer) decide how to react. We've already surfaced + // the failure to the user via the spinner + printOutput above. + if (error instanceof Error) { + throw error; + } + throw new Error('[BUILD] Build failed with a non-Error exception'); + } + } + + /** + * Prints command output with indentation and specified color. + * Filters out empty lines and indents each line for readability. + * + * @param output - The command output string to print (stdout or stderr) + * @param colorFn - Chalk color function to style the output (e.g., `chalk.cyan` for success, `chalk.red` for errors) + */ + private printOutput(output: string, colorFn: (text: string) => string): void { + const lines: string[] = output + .split('\n') + .filter((line: string): boolean => line.trim() !== '') + .map((line: string): string => ` ${line}`); + console.log(colorFn(lines.join('\n'))); + } +} diff --git a/packages/builder/src/Compiler.ts b/packages/builder/src/Compiler.ts new file mode 100755 index 0000000..cec51f2 --- /dev/null +++ b/packages/builder/src/Compiler.ts @@ -0,0 +1,332 @@ +#!/usr/bin/env node + +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import ora from 'ora'; +import { CompilerService } from './services/CompilerService.ts'; +import { EnvironmentValidator } from './services/EnvironmentValidator.ts'; +import { FileDiscovery } from './services/FileDiscovery.ts'; +import { UIService } from './services/UIService.ts'; +import { + CompilationError, + DirectoryNotFoundError, + isPromisifiedChildProcessError, +} from './types/errors.ts'; +import { + type CompilerOptions, + type CompilerServiceOptions, + DEFAULT_OUT_DIR, + DEFAULT_SRC_DIR, + type ExecFunction, +} from './types/options.ts'; + +// Re-export public types and services so consumers keep importing them +// from './Compiler.js' regardless of the internal file layout. +// biome-ignore lint/performance/noBarrelFile: package entrypoint +export { CompilerService } from './services/CompilerService.ts'; +export { EnvironmentValidator } from './services/EnvironmentValidator.ts'; +export { FileDiscovery } from './services/FileDiscovery.ts'; +export { UIService } from './services/UIService.ts'; +export type { CompilerOptions, CompilerServiceOptions, ExecFunction }; + +/** Resolved compiler options with defaults applied */ +type ResolvedCompilerOptions = Required< + Pick< + CompilerOptions, + 'flags' | 'hierarchical' | 'srcDir' | 'outDir' | 'exclude' + > +> & + Pick; + +/** + * Main compiler class that orchestrates the compilation process. + * Coordinates environment validation, file discovery, and compilation services + * to provide a complete .compact file compilation solution. + * + * Features: + * - Dependency injection for testability + * - Structured error propagation with custom error types + * - Progress reporting and user feedback + * - Support for compiler flags and toolchain versions + * - Environment variable integration + * - Configurable artifact output structure (flattened or hierarchical) + * + * @example + * ```typescript + * // Basic usage with options object (flattened artifacts by default) + * const compiler = new CompactCompiler({ + * flags: '--skip-zk', + * targetDir: 'security', + * version: '0.26.0', + * }); + * await compiler.compile(); + * + * // Factory method usage + * const compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); + * await compiler.compile(); + * + * // With hierarchical artifacts structure + * const compiler = CompactCompiler.fromArgs(['--hierarchical', '--skip-zk']); + * await compiler.compile(); + * + * // With environment variables + * process.env.SKIP_ZK = 'true'; + * const compiler = CompactCompiler.fromArgs(['--dir', 'token']); + * await compiler.compile(); + * ``` + */ +export class CompactCompiler { + /** Environment validation service */ + private readonly environmentValidator: EnvironmentValidator; + /** File discovery service */ + private readonly fileDiscovery: FileDiscovery; + /** Compilation execution service */ + private readonly compilerService: CompilerService; + /** Compiler options */ + private readonly options: ResolvedCompilerOptions; + + /** + * Creates a new CompactCompiler instance with specified configuration. + * + * @param options - Compiler configuration options + * @param execFn - Optional custom exec function for dependency injection + */ + constructor(options: CompilerOptions = {}, execFn?: ExecFunction) { + this.options = { + flags: (options.flags ?? '').trim(), + targetDir: options.targetDir, + version: options.version, + hierarchical: options.hierarchical ?? false, + srcDir: options.srcDir ?? DEFAULT_SRC_DIR, + outDir: options.outDir ?? DEFAULT_OUT_DIR, + exclude: options.exclude ?? [], + }; + this.environmentValidator = new EnvironmentValidator(execFn); + this.fileDiscovery = new FileDiscovery( + this.options.srcDir, + this.options.exclude, + ); + this.compilerService = new CompilerService(execFn, { + hierarchical: this.options.hierarchical, + srcDir: this.options.srcDir, + outDir: this.options.outDir, + }); + } + + /** + * Parses command-line arguments into a CompilerOptions object. + * + * Supported argument patterns: + * - `--dir ` - Target specific subdirectory within srcDir + * - `--src ` - Source directory containing .compact files (default: 'src') + * - `--out ` - Output directory for artifacts (default: 'artifacts') + * - `--hierarchical` - Preserve source directory structure in artifacts output + * - `--exclude ` - Skip `.compact` files matching the glob pattern (repeatable) + * - `+` - Use specific toolchain version + * - Other arguments - Treated as compiler flags + * - `SKIP_ZK=true` environment variable - Adds --skip-zk flag + * + * @param args - Array of command-line arguments + * @param env - Environment variables (defaults to process.env) + * @returns Parsed CompilerOptions object + * @throws {Error} If --dir, --src, --out, or --exclude is provided without a value + */ + static parseArgs( + args: string[], + env: typeof process.env = process.env, + ): CompilerOptions { + const options: CompilerOptions = { + hierarchical: false, + }; + const flags: string[] = []; + + if (env.SKIP_ZK === 'true') { + flags.push('--skip-zk'); + } + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--dir') { + const valueExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); + if (valueExists) { + options.targetDir = args[i + 1]; + i++; + } else { + throw new Error('--dir flag requires a directory name'); + } + } else if (args[i] === '--src') { + const valueExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); + if (valueExists) { + options.srcDir = args[i + 1]; + i++; + } else { + throw new Error('--src flag requires a directory path'); + } + } else if (args[i] === '--out') { + const valueExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); + if (valueExists) { + options.outDir = args[i + 1]; + i++; + } else { + throw new Error('--out flag requires a directory path'); + } + } else if (args[i] === '--hierarchical') { + options.hierarchical = true; + } else if (args[i] === '--exclude') { + const valueExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); + if (valueExists) { + options.exclude ??= []; + options.exclude.push(args[i + 1]); + i++; + } else { + throw new Error('--exclude flag requires a pattern'); + } + } else if (args[i].startsWith('+')) { + options.version = args[i].slice(1); + } else { + // Forward flags in original order, no dedup — repeatable flags + // (e.g. `--define x=1 --define y=2`) must be preserved as given. + flags.push(args[i]); + } + } + + options.flags = flags.join(' '); + return options; + } + + /** + * Factory method to create a CompactCompiler from command-line arguments. + * See {@link CompactCompiler.parseArgs} for the supported argument shapes. + * + * @param args - Array of command-line arguments + * @param env - Environment variables (defaults to process.env) + * @returns New CompactCompiler instance configured from arguments + * @throws {Error} If --dir, --src, --out, or --exclude is provided without a value + */ + static fromArgs( + args: string[], + env: typeof process.env = process.env, + ): CompactCompiler { + const options = CompactCompiler.parseArgs(args, env); + return new CompactCompiler(options); + } + + /** + * Validates the compilation environment and displays version information. + * + * @throws {CompactCliNotFoundError} If Compact CLI is not available in PATH + * @throws {Error} If version retrieval or other validation steps fail + */ + async validateEnvironment(): Promise { + const { devToolsVersion, toolchainVersion } = + await this.environmentValidator.validate(this.options.version); + UIService.displayEnvInfo( + devToolsVersion, + toolchainVersion, + this.options.targetDir, + this.options.version, + ); + } + + /** + * Main compilation method that orchestrates the entire compilation process. + * + * @throws {CompactCliNotFoundError} If Compact CLI is not available + * @throws {DirectoryNotFoundError} If target directory doesn't exist + * @throws {CompilationError} If any file compilation fails + */ + async compile(): Promise { + await this.validateEnvironment(); + + const searchDir = this.options.targetDir + ? join(this.options.srcDir, this.options.targetDir) + : this.options.srcDir; + + // Validate target directory exists + if (this.options.targetDir && !existsSync(searchDir)) { + throw new DirectoryNotFoundError( + `Target directory ${searchDir} does not exist`, + searchDir, + ); + } + + const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); + + if (compactFiles.length === 0) { + UIService.showNoFiles(this.options.targetDir); + return; + } + + UIService.showCompilationStart(compactFiles.length, this.options.targetDir); + + for (const [index, file] of compactFiles.entries()) { + await this.compileFile(file, index, compactFiles.length); + } + } + + /** + * Compiles a single file with progress reporting and error handling. + * + * @param file - Relative path to the .compact file + * @param index - Current file index (0-based) for progress tracking + * @param total - Total number of files being compiled + * @throws {CompilationError} If compilation fails + */ + private async compileFile( + file: string, + index: number, + total: number, + ): Promise { + const step = `[${index + 1}/${total}]`; + const spinner = ora( + chalk.blue(`[COMPILE] ${step} Compiling ${file}`), + ).start(); + + try { + const result = await this.compilerService.compileFile( + file, + this.options.flags, + this.options.version, + ); + + spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); + // Filter out compactc version output from compact compile + const filteredOutput = result.stdout.split('\n').slice(1).join('\n'); + + if (filteredOutput) { + UIService.printOutput(filteredOutput, chalk.cyan); + } + UIService.printOutput(result.stderr, chalk.yellow); + } catch (error) { + spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); + + // CompilationError wraps the underlying child-process error in `.cause`. + // The previous guard `isPromisifiedChildProcessError(error)` on a + // CompilationError instance was unreachable — unwrap via `.cause` to + // surface compactc's stdout/stderr to the user. + const execError = error instanceof CompilationError ? error.cause : error; + if (isPromisifiedChildProcessError(execError)) { + // Filter out compactc version output from compact compile + const filteredOutput = execError.stdout.split('\n').slice(1).join('\n'); + + if (filteredOutput) { + UIService.printOutput(filteredOutput, chalk.cyan); + } + UIService.printOutput(execError.stderr, chalk.red); + } + + throw error; + } + } + + /** + * For testing - returns the resolved options object + */ + get testOptions(): ResolvedCompilerOptions { + return this.options; + } +} diff --git a/packages/builder/src/index.ts b/packages/builder/src/index.ts new file mode 100644 index 0000000..e1868ec --- /dev/null +++ b/packages/builder/src/index.ts @@ -0,0 +1,23 @@ +export type { BuilderOnlyOptions, BuilderOptions } from './Builder.js'; +// biome-ignore lint/performance/noBarrelFile: package entrypoint +export { CompactBuilder } from './Builder.js'; +export type { + CompilerOptions, + CompilerServiceOptions, + ExecFunction, +} from './Compiler.js'; +export { + CompactCompiler, + CompilerService, + EnvironmentValidator, + FileDiscovery, + UIService, +} from './Compiler.js'; +export type { PromisifiedChildProcessError } from './types/errors.js'; +export { + CompactCliNotFoundError, + CompilationError, + DirectoryNotFoundError, + isPromisifiedChildProcessError, +} from './types/errors.js'; +export type { BuildStep } from './types/options.js'; diff --git a/packages/builder/src/services/CompilerService.ts b/packages/builder/src/services/CompilerService.ts new file mode 100644 index 0000000..9c9f046 --- /dev/null +++ b/packages/builder/src/services/CompilerService.ts @@ -0,0 +1,130 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { basename, dirname, join } from 'node:path'; +import { promisify } from 'node:util'; +import { parse as parseShellArgs } from 'shell-quote'; +import { CompilationError } from '../types/errors.ts'; +import { + type CompilerServiceOptions, + DEFAULT_OUT_DIR, + DEFAULT_SRC_DIR, + type ExecFunction, +} from '../types/options.ts'; + +/** Resolved options for CompilerService with defaults applied */ +type ResolvedCompilerServiceOptions = Required; + +const defaultExecFn: ExecFunction = (file, args) => + promisify(execFileCallback)(file, [...args]); + +/** + * Tokenizes a user-supplied `flags` string into discrete argv entries using + * `shell-quote` (the same rules a shell would apply for splitting). Any + * non-string tokens (e.g. operators like `;`, `&&`) are filtered out so they + * cannot leak into argv as data — defense in depth against command injection + * via the `flags` option. + */ +function tokenizeFlags(flags: string): string[] { + if (!flags) { + return []; + } + return parseShellArgs(flags).filter( + (token): token is string => typeof token === 'string', + ); +} + +/** + * Service responsible for compiling individual .compact files. + * Builds argv arrays and invokes the Compact CLI via `child_process.execFile` + * (no shell), so user-supplied values cannot inject extra commands. + * + * @example + * ```typescript + * const compiler = new CompilerService(); + * const result = await compiler.compileFile( + * 'contracts/Token.compact', + * '--skip-zk --verbose', + * '0.26.0' + * ); + * ``` + */ +export class CompilerService { + private execFn: ExecFunction; + private options: ResolvedCompilerServiceOptions; + + /** + * Creates a new CompilerService instance. + * + * @param execFn - Function to invoke the Compact CLI binary (defaults to + * a promisified `child_process.execFile` — argv array, no shell). + * @param options - Compiler service options + */ + constructor( + execFn: ExecFunction = defaultExecFn, + options: CompilerServiceOptions = {}, + ) { + this.execFn = execFn; + this.options = { + hierarchical: options.hierarchical ?? false, + srcDir: options.srcDir ?? DEFAULT_SRC_DIR, + outDir: options.outDir ?? DEFAULT_OUT_DIR, + }; + } + + /** + * Compiles a single .compact file using the Compact CLI. + * Builds the argv array (no shell interpolation) and invokes the binary. + * + * By default, uses flattened output structure where all artifacts go to `//`. + * When `hierarchical` is true, preserves source directory structure: `///`. + * + * @param file - Relative path to the .compact file from srcDir + * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose'). + * Tokenized via `shell-quote` so quoted whitespace is preserved + * and shell operators (`;`, `&&`, …) cannot inject commands. + * @param version - Optional specific toolchain version to use + * @returns Promise resolving to compilation output (stdout/stderr) + * @throws {CompilationError} If compilation fails for any reason + */ + async compileFile( + file: string, + flags: string, + version?: string, + ): Promise<{ stdout: string; stderr: string }> { + const inputPath = join(this.options.srcDir, file); + const fileDir = dirname(file); + const fileName = basename(file, '.compact'); + + // Flattened (default): // + // Hierarchical: /// + const outputDir = + this.options.hierarchical && fileDir !== '.' + ? join(this.options.outDir, fileDir, fileName) + : join(this.options.outDir, fileName); + + const args: string[] = [ + 'compile', + ...(version ? [`+${version}`] : []), + ...tokenizeFlags(flags), + inputPath, + outputDir, + ]; + + try { + return await this.execFn('compact', args); + } catch (error: unknown) { + let message: string; + + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); // fallback for strings, objects, numbers, etc. + } + + throw new CompilationError( + `Failed to compile ${file}: ${message}`, + file, + error, + ); + } + } +} diff --git a/packages/builder/src/services/EnvironmentValidator.ts b/packages/builder/src/services/EnvironmentValidator.ts new file mode 100644 index 0000000..4c99821 --- /dev/null +++ b/packages/builder/src/services/EnvironmentValidator.ts @@ -0,0 +1,95 @@ +import { execFile as execFileCallback } from 'node:child_process'; +import { promisify } from 'node:util'; +import { CompactCliNotFoundError } from '../types/errors.ts'; +import type { ExecFunction } from '../types/options.ts'; + +const defaultExecFn: ExecFunction = (file, args) => + promisify(execFileCallback)(file, [...args]); + +/** + * Service responsible for validating the Compact CLI environment. + * Checks CLI availability, retrieves version information, and ensures + * the toolchain is properly configured before compilation. + * + * @example + * ```typescript + * const validator = new EnvironmentValidator(); + * await validator.validate('0.26.0'); + * const version = await validator.getDevToolsVersion(); + * ``` + */ +export class EnvironmentValidator { + private execFn: ExecFunction; + + /** + * Creates a new EnvironmentValidator instance. + * + * @param execFn - Function to execute the Compact CLI binary (defaults to + * a promisified `child_process.execFile` — argv array, no shell). + */ + constructor(execFn: ExecFunction = defaultExecFn) { + this.execFn = execFn; + } + + /** + * Checks if the Compact CLI is available in the system PATH. + * + * @returns Promise resolving to true if CLI is available, false otherwise + */ + async checkCompactAvailable(): Promise { + try { + await this.execFn('compact', ['--version']); + return true; + } catch { + return false; + } + } + + /** + * Retrieves the version of the Compact developer tools. + * + * @returns Promise resolving to the version string + * @throws {Error} If the CLI is not available or command fails + */ + async getDevToolsVersion(): Promise { + const { stdout } = await this.execFn('compact', ['--version']); + return stdout.trim(); + } + + /** + * Retrieves the version of the Compact toolchain/compiler. + * + * @param version - Optional specific toolchain version to query + * @returns Promise resolving to the toolchain version string + * @throws {Error} If the CLI is not available or command fails + */ + async getToolchainVersion(version?: string): Promise { + const args = ['compile', ...(version ? [`+${version}`] : []), '--version']; + const { stdout } = await this.execFn('compact', args); + return stdout.trim(); + } + + /** + * Validates the entire Compact environment and ensures it's ready for compilation. + * Checks CLI availability and retrieves version information. + * + * @param version - Optional specific toolchain version to validate + * @throws {CompactCliNotFoundError} If the Compact CLI is not available + * @throws {Error} If version commands fail + */ + async validate( + version?: string, + ): Promise<{ devToolsVersion: string; toolchainVersion: string }> { + const isAvailable = await this.checkCompactAvailable(); + if (!isAvailable) { + throw new CompactCliNotFoundError( + "'compact' CLI not found in PATH. Please install the Compact developer tools.", + ); + } + + const devToolsVersion = await this.getDevToolsVersion(); + const toolchainVersion = await this.getToolchainVersion(version); + + return { devToolsVersion, toolchainVersion }; + } +} diff --git a/packages/builder/src/services/FileDiscovery.ts b/packages/builder/src/services/FileDiscovery.ts new file mode 100644 index 0000000..f451a6f --- /dev/null +++ b/packages/builder/src/services/FileDiscovery.ts @@ -0,0 +1,81 @@ +import { readdir } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import { DEFAULT_SRC_DIR } from '../types/options.ts'; +import { isExcluded } from '../utils.ts'; + +/** + * Service responsible for discovering .compact files in the source directory. + * Recursively scans directories and filters for .compact file extensions, + * applying user-supplied exclude patterns. + * + * @example + * ```typescript + * const discovery = new FileDiscovery('src', ['Mock*']); + * const files = await discovery.getCompactFiles('src/security'); + * ``` + */ +export class FileDiscovery { + private srcDir: string; + private excludes: readonly string[]; + + /** + * Creates a new FileDiscovery instance. + * + * @param srcDir - Base source directory for relative path calculation (default: 'src') + * @param excludes - Glob-style patterns of `.compact` files to skip. + * Patterns containing `/` match against the full path + * (as `find ` would emit it); others match against + * the filename only. Default: `[]`. + */ + constructor( + srcDir: string = DEFAULT_SRC_DIR, + excludes: readonly string[] = [], + ) { + this.srcDir = srcDir; + this.excludes = excludes; + } + + /** + * Recursively discovers all .compact files in a directory. + * Returns relative paths from the srcDir for consistent processing. + * + * @param dir - Directory path to search (relative or absolute) + * @returns Promise resolving to array of relative file paths + */ + async getCompactFiles(dir: string): Promise { + try { + const dirents = await readdir(dir, { withFileTypes: true }); + const filePromises = dirents.map(async (entry) => { + const fullPath = join(dir, entry.name); + try { + if (entry.isDirectory()) { + return await this.getCompactFiles(fullPath); + } + + if (entry.isFile() && fullPath.endsWith('.compact')) { + const relPath = relative(this.srcDir, fullPath); + // Match path-style patterns against fullPath (i.e. the path that + // `find srcDir` would emit) so users can write `*/archive/*` etc., + // identical to what they'd pass to `find -path`. + if (isExcluded(entry.name, fullPath, this.excludes)) { + return []; + } + return [relPath]; + } + return []; + } catch (err) { + // biome-ignore lint/suspicious/noConsole: Needed to display error and file path + console.warn(`Error accessing ${fullPath}:`, err); + return []; + } + }); + + const results = await Promise.all(filePromises); + return results.flat(); + } catch (err) { + // biome-ignore lint/suspicious/noConsole: Needed to display error and dir path + console.error(`Failed to read dir: ${dir}`, err); + return []; + } + } +} diff --git a/packages/builder/src/services/UIService.ts b/packages/builder/src/services/UIService.ts new file mode 100644 index 0000000..6839130 --- /dev/null +++ b/packages/builder/src/services/UIService.ts @@ -0,0 +1,91 @@ +import chalk from 'chalk'; +import ora from 'ora'; + +/** + * Utility service for handling user interface output and formatting. + * Provides consistent styling and formatting for compiler messages and output. + * + * @example + * ```typescript + * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.26.0', 'security'); + * UIService.printOutput('Compilation successful', chalk.green); + * ``` + */ +export const UIService = { + /** + * Prints formatted output with consistent indentation and coloring. + * Filters empty lines and adds consistent indentation for readability. + * + * @param output - Raw output text to format + * @param colorFn - Chalk color function for styling + */ + printOutput(output: string, colorFn: (text: string) => string): void { + const lines = output + .split('\n') + .filter((line) => line.trim() !== '') + .map((line) => ` ${line}`); + console.log(colorFn(lines.join('\n'))); + }, + + /** + * Displays environment information including tool versions and configuration. + * Shows developer tools version, toolchain version, and optional settings. + * + * @param devToolsVersion - Version string of the Compact developer tools + * @param toolchainVersion - Version string of the Compact toolchain/compiler + * @param targetDir - Optional target directory being compiled + * @param version - Optional specific version being used + */ + displayEnvInfo( + devToolsVersion: string, + toolchainVersion: string, + targetDir?: string, + version?: string, + ): void { + const spinner = ora(); + + if (targetDir) { + spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${targetDir}`)); + } + + spinner.info( + chalk.blue(`[COMPILE] Compact developer tools: ${devToolsVersion}`), + ); + spinner.info( + chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), + ); + + if (version) { + spinner.info(chalk.blue(`[COMPILE] Using toolchain version: ${version}`)); + } + }, + + /** + * Displays compilation start message with file count and optional location. + * + * @param fileCount - Number of files to be compiled + * @param targetDir - Optional target directory being compiled + */ + showCompilationStart(fileCount: number, targetDir?: string): void { + const searchLocation = targetDir ? ` in ${targetDir}/` : ''; + const spinner = ora(); + spinner.info( + chalk.blue( + `[COMPILE] Found ${fileCount} .compact file(s) to compile${searchLocation}`, + ), + ); + }, + + /** + * Displays a warning message when no .compact files are found. + * + * @param targetDir - Optional target directory that was searched + */ + showNoFiles(targetDir?: string): void { + const searchLocation = targetDir ? `${targetDir}/` : ''; + const spinner = ora(); + spinner.warn( + chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), + ); + }, +}; diff --git a/packages/cli/src/types/errors.ts b/packages/builder/src/types/errors.ts similarity index 100% rename from packages/cli/src/types/errors.ts rename to packages/builder/src/types/errors.ts diff --git a/packages/builder/src/types/options.ts b/packages/builder/src/types/options.ts new file mode 100644 index 0000000..76cf464 --- /dev/null +++ b/packages/builder/src/types/options.ts @@ -0,0 +1,143 @@ +/** + * Shared option/type definitions and defaults for the Compact CLI tools. + * + * This module is the canonical home for cross-cutting types (`CompilerOptions`, + * `BuilderOptions`, `ExecFunction`, …) plus their default constants. Splitting + * them out keeps `Compiler.ts` and `Builder.ts` focused on behaviour rather + * than data shapes. + */ + +/** Default source directory containing .compact files. */ +export const DEFAULT_SRC_DIR = 'src'; + +/** Default output directory for compiled artifacts. */ +export const DEFAULT_OUT_DIR = 'artifacts'; + +/** + * Default `.compact` glob patterns the builder strips from `dist/` when the + * user hasn't supplied an explicit `--exclude` list. Covers both common mock + * naming conventions. + */ +export const DEFAULT_EXCLUDE_PATTERNS: readonly string[] = [ + 'Mock*', + '*.mock.compact', +]; + +/** + * Function type for executing a child process. + * + * Matches the shape of `promisify(child_process.execFile)`: the binary name + * (no shell), followed by positional arguments. This signature is injection- + * safe by construction — values flow as separate argv entries rather than + * being interpolated into a shell command string. + * + * @param file - The binary to invoke (e.g. `'compact'`) + * @param args - Positional arguments passed verbatim to the binary + * @returns Promise resolving to the captured stdout/stderr + */ +export type ExecFunction = ( + file: string, + args: readonly string[], +) => Promise<{ stdout: string; stderr: string }>; + +/** + * Configuration options for the Compact compiler CLI. + * + * @example + * ```typescript + * const options: CompilerOptions = { + * flags: '--skip-zk --verbose', + * targetDir: 'security', + * version: '0.26.0', + * hierarchical: false, + * }; + * ``` + */ +export interface CompilerOptions { + /** Compiler flags to pass to the Compact CLI (e.g., '--skip-zk --verbose') */ + flags?: string; + /** Optional subdirectory within srcDir to compile (e.g., 'security', 'token') */ + targetDir?: string; + /** Optional toolchain version to use (e.g., '0.26.0') */ + version?: string; + /** + * Whether to preserve directory structure in artifacts output. + * - `false` (default): Flattened output - `//` + * - `true`: Hierarchical output - `///` + */ + hierarchical?: boolean; + /** Source directory containing .compact files (default: 'src') */ + srcDir?: string; + /** Output directory for compiled artifacts (default: 'artifacts') */ + outDir?: string; + /** + * Glob-style patterns to exclude `.compact` files from both the compiler's + * file discovery and the builder's `.compact` copy step. + * - Patterns without `/` match against the filename only (e.g. `'Mock*'`). + * - Patterns with `/` match against the path as `find ` would emit + * it (e.g. `'*\/archive\/*'`, matching `src/archive/Foo.compact`). This is + * the same semantic as `find -path ''`. + * + * Default: `undefined` (no excludes for the compiler). The builder + * substitutes its own default ({@link DEFAULT_EXCLUDE_PATTERNS}) when + * undefined; pass an explicit `[]` to disable that too. + */ + exclude?: string[]; +} + +/** + * Subset of {@link CompilerOptions} consumed by `CompilerService` when + * compiling individual files. + */ +export type CompilerServiceOptions = Pick< + CompilerOptions, + 'hierarchical' | 'srcDir' | 'outDir' +>; + +/** + * Builder-only configuration options that don't apply to the compiler. + * + * These control the *distribution* layout produced by `compact-builder`, + * letting consumers ship a publishable `dist/` directory matching their + * package's conventions (e.g. copying metadata files for npm publish). + * + * Two inherited {@link CompilerOptions} fields also affect builder behaviour: + * - `hierarchical` — drives both compiler artifacts layout AND `.compact` copy layout. + * - `exclude` — drives both compiler file discovery AND `.compact` copy filtering. + * When `exclude` is undefined, the builder substitutes its own default + * ({@link DEFAULT_EXCLUDE_PATTERNS}) so mocks are stripped from the dist + * even when the compiler is told to consume them. + */ +export interface BuilderOnlyOptions { + /** + * If true, runs `rm -rf dist && mkdir -p dist` before building. + * Use when you want a fully clean `dist/` on every build. + * @default false + */ + cleanDist?: boolean; + /** + * Additional file paths to copy into `dist/` for distribution + * (e.g. `['package.json', '../README.md']`). Paths are relative to cwd. + * Each entry is copied individually with `cp dist/`. + * @default [] + */ + copyToDist?: string[]; +} + +/** + * Configuration options for the Builder CLI. + * Extends {@link CompilerOptions} with builder-only distribution controls. + */ +export type BuilderOptions = CompilerOptions & BuilderOnlyOptions; + +/** + * Single build step executed by `CompactBuilder`. + */ +export interface BuildStep { + /** Shell command to execute. */ + cmd: string; + /** Human-readable progress message. */ + msg: string; + /** Optional explicit shell (e.g. `'/bin/bash'`) when bash features are required. */ + shell?: string; +} diff --git a/packages/builder/src/utils.ts b/packages/builder/src/utils.ts new file mode 100644 index 0000000..c60cd67 --- /dev/null +++ b/packages/builder/src/utils.ts @@ -0,0 +1,70 @@ +/** + * Internal helpers for the Compact CLI tools. + * + * - **Glob matching** ({@link globToRegex}, {@link isExcluded}) — used by + * `FileDiscovery` to skip `.compact` files matching user-supplied patterns. + * - **Shell quoting** ({@link shellQuote}, {@link buildFindExcludes}) — used by + * `CompactBuilder` to interpolate user-supplied values into bash commands + * safely. + */ + +/** + * Converts a simple glob pattern to a regular expression. + * Supports `*` (any sequence) and `?` (single char). All other glob features + * (brace expansion, character classes) are not supported — keep patterns simple. + */ +export function globToRegex(glob: string): RegExp { + const escaped = glob.replace(/[\\^$+|.()[\]{}]/g, '\\$&'); + const pattern = escaped.replace(/\*/g, '.*').replace(/\?/g, '.'); + return new RegExp(`^${pattern}$`); +} + +/** + * Returns true if `filename`/`fullPath` matches any of the given glob patterns. + * + * - Patterns containing `/` are matched against `fullPath` (the path as + * `find srcDir` would emit it, e.g. `'src/archive/Foo.compact'`). + * - Patterns without `/` are matched against `filename` only. + * + * This mirrors the semantic of `find -name ` vs `find -path `. + */ +export function isExcluded( + filename: string, + fullPath: string, + patterns: readonly string[], +): boolean { + return patterns.some((pattern) => { + const target = pattern.includes('/') ? fullPath : filename; + return globToRegex(pattern).test(target); + }); +} + +/** + * Shell-quotes a string for safe interpolation into a single-quoted bash arg. + * + * @example + * shellQuote("foo") // "'foo'" + * shellQuote("it's") // "'it'\\''s'" + */ +export function shellQuote(value: string): string { + return `'${value.replace(/'/g, `'\\''`)}'`; +} + +/** + * Builds the `find`-compatible exclusion fragment for the given patterns. + * Patterns containing `/` are emitted as `! -path ''`; others as + * `! -name ''`. Single-quoting ensures safe shell interpolation. + * + * @example + * buildFindExcludes(['Mock*', '*\/archive\/*']) + * // "! -name 'Mock*' ! -path '*\/archive\/*'" + */ +export function buildFindExcludes(patterns: readonly string[]): string { + return patterns + .map((pattern) => + pattern.includes('/') + ? `! -path ${shellQuote(pattern)}` + : `! -name ${shellQuote(pattern)}`, + ) + .join(' '); +} diff --git a/packages/builder/test/Builder.test.ts b/packages/builder/test/Builder.test.ts new file mode 100644 index 0000000..35c487a --- /dev/null +++ b/packages/builder/test/Builder.test.ts @@ -0,0 +1,206 @@ +import { describe, expect, it } from 'vitest'; +import { CompactBuilder } from '../src/Builder.js'; + +describe('CompactBuilder.parseArgs', () => { + it('returns defaults for empty input', () => { + const options = CompactBuilder.parseArgs([]); + + expect(options.cleanDist).toBeUndefined(); + expect(options.exclude).toBeUndefined(); + expect(options.copyToDist).toBeUndefined(); + // Compiler-side defaults still apply + expect(options.hierarchical).toBe(false); + expect(options.flags).toBe(''); + }); + + it('parses --clean-dist', () => { + expect(CompactBuilder.parseArgs(['--clean-dist']).cleanDist).toBe(true); + }); + + it('forwards --hierarchical to the compiler parser', () => { + // `hierarchical` is parsed by CompactCompiler.parseArgs and drives both + // compiler artifacts layout and builder .compact copy layout. + expect(CompactBuilder.parseArgs(['--hierarchical']).hierarchical).toBe( + true, + ); + }); + + it('accumulates repeated --exclude patterns', () => { + const options = CompactBuilder.parseArgs([ + '--exclude', + 'Mock*', + '--exclude', + '*/archive/*', + ]); + + expect(options.exclude).toEqual(['Mock*', '*/archive/*']); + }); + + it('accumulates repeated --copy paths', () => { + const options = CompactBuilder.parseArgs([ + '--copy', + 'package.json', + '--copy', + '../README.md', + ]); + + expect(options.copyToDist).toEqual(['package.json', '../README.md']); + }); + + it('throws when --exclude is missing a pattern', () => { + expect(() => CompactBuilder.parseArgs(['--exclude'])).toThrow( + '--exclude flag requires a pattern', + ); + expect(() => + CompactBuilder.parseArgs(['--exclude', '--clean-dist']), + ).toThrow('--exclude flag requires a pattern'); + }); + + it('throws when --copy is missing a path', () => { + expect(() => CompactBuilder.parseArgs(['--copy'])).toThrow( + '--copy flag requires a path', + ); + expect(() => CompactBuilder.parseArgs(['--copy', '--clean-dist'])).toThrow( + '--copy flag requires a path', + ); + }); + + it('forwards unknown args to the compiler parser', () => { + const options = CompactBuilder.parseArgs([ + '--dir', + 'security', + '--skip-zk', + '+0.29.0', + ]); + + expect(options.targetDir).toBe('security'); + expect(options.flags).toBe('--skip-zk'); + expect(options.version).toBe('0.29.0'); + }); + + it('combines builder and compiler flags', () => { + const options = CompactBuilder.parseArgs([ + '--clean-dist', + '--dir', + 'token', + '--hierarchical', + '--exclude', + 'Mock*', + '--copy', + 'package.json', + '--src', + 'contracts', + '+0.29.0', + ]); + + expect(options.cleanDist).toBe(true); + expect(options.hierarchical).toBe(true); + expect(options.exclude).toEqual(['Mock*']); + expect(options.copyToDist).toEqual(['package.json']); + expect(options.targetDir).toBe('token'); + expect(options.srcDir).toBe('contracts'); + expect(options.version).toBe('0.29.0'); + }); +}); + +describe('CompactBuilder step pipeline', () => { + it('uses the legacy 3-step pipeline by default', () => { + const builder = new CompactBuilder(); + const steps = builder.getSteps(); + + expect(steps.map((s) => s.msg)).toEqual([ + 'Compiling TypeScript', + 'Copying artifacts', + 'Copying .compact files', + ]); + }); + + it('excludes Mock* and *.mock.compact by default in the flat copy step', () => { + const builder = new CompactBuilder(); + const copyStep = builder + .getSteps() + .find((s) => s.msg === 'Copying .compact files'); + + expect(copyStep?.cmd).toContain("! -name 'Mock*'"); + expect(copyStep?.cmd).toContain("! -name '*.mock.compact'"); + }); + + it('prepends a clean-dist step when cleanDist is true', () => { + const builder = new CompactBuilder({ cleanDist: true }); + const steps = builder.getSteps(); + + expect(steps[0].msg).toBe('Cleaning dist directory'); + expect(steps[0].cmd).toBe('rm -rf dist && mkdir -p dist'); + expect(steps).toHaveLength(4); + }); + + it('uses the hierarchical copy step when hierarchical is true', () => { + const builder = new CompactBuilder({ hierarchical: true }); + const copyStep = builder + .getSteps() + .find((s) => s.msg === 'Copying .compact files (preserving structure)'); + + expect(copyStep).toBeDefined(); + expect(copyStep?.cmd).toContain('rel_path='); + expect(copyStep?.cmd).toContain('mkdir -p "dist/$(dirname "$rel_path")"'); + }); + + it('appends a copy-to-dist step for each entry in copyToDist', () => { + const builder = new CompactBuilder({ + copyToDist: ['package.json', '../README.md'], + }); + const lastStep = builder.getSteps().at(-1); + + expect(lastStep?.msg).toBe('Copying additional files to dist'); + expect(lastStep?.cmd).toContain("cp 'package.json' dist/"); + expect(lastStep?.cmd).toContain("cp '../README.md' dist/"); + }); + + it('classifies excludes into -name and -path', () => { + const builder = new CompactBuilder({ + hierarchical: true, + exclude: ['Mock*', '*/archive/*'], + }); + const copyStep = builder + .getSteps() + .find((s) => s.msg === 'Copying .compact files (preserving structure)'); + + expect(copyStep?.cmd).toContain("! -name 'Mock*'"); + expect(copyStep?.cmd).toContain("! -path '*/archive/*'"); + }); + + it('honours an explicit empty exclude list (disables the default Mock*)', () => { + const builder = new CompactBuilder({ exclude: [] }); + const copyStep = builder + .getSteps() + .find((s) => s.msg === 'Copying .compact files'); + + expect(copyStep?.cmd).not.toContain('! -name'); + }); + + it('shell-quotes srcDir into find paths', () => { + const builder = new CompactBuilder({ srcDir: 'my src' }); + const copyStep = builder + .getSteps() + .find((s) => s.msg === 'Copying .compact files'); + + expect(copyStep?.cmd).toContain("find 'my src'"); + }); + + it('produces the full pipeline for a library-publish configuration', () => { + const builder = new CompactBuilder({ + cleanDist: true, + hierarchical: true, + exclude: ['Mock*', '*/archive/*'], + copyToDist: ['package.json', '../README.md'], + }); + + expect(builder.getSteps().map((s) => s.msg)).toEqual([ + 'Cleaning dist directory', + 'Compiling TypeScript', + 'Copying artifacts', + 'Copying .compact files (preserving structure)', + 'Copying additional files to dist', + ]); + }); +}); diff --git a/packages/cli/test/Compiler.test.ts b/packages/builder/test/Compiler.test.ts similarity index 82% rename from packages/cli/test/Compiler.test.ts rename to packages/builder/test/Compiler.test.ts index 3913df8..4287acf 100644 --- a/packages/cli/test/Compiler.test.ts +++ b/packages/builder/test/Compiler.test.ts @@ -69,7 +69,7 @@ describe('EnvironmentValidator', () => { const result = await validator.checkCompactAvailable(); expect(result).toBe(true); - expect(mockExec).toHaveBeenCalledWith('compact --version'); + expect(mockExec).toHaveBeenCalledWith('compact', ['--version']); }); it('should return false when compact CLI is not available', async () => { @@ -78,7 +78,7 @@ describe('EnvironmentValidator', () => { const result = await validator.checkCompactAvailable(); expect(result).toBe(false); - expect(mockExec).toHaveBeenCalledWith('compact --version'); + expect(mockExec).toHaveBeenCalledWith('compact', ['--version']); }); }); @@ -89,7 +89,7 @@ describe('EnvironmentValidator', () => { const version = await validator.getDevToolsVersion(); expect(version).toBe('compact 0.1.0'); - expect(mockExec).toHaveBeenCalledWith('compact --version'); + expect(mockExec).toHaveBeenCalledWith('compact', ['--version']); }); it('should throw error when command fails', async () => { @@ -111,7 +111,10 @@ describe('EnvironmentValidator', () => { const version = await validator.getToolchainVersion(); expect(version).toBe('Compactc version: 0.26.0'); - expect(mockExec).toHaveBeenCalledWith('compact compile --version'); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--version', + ]); }); it('should get version with specific version flag', async () => { @@ -123,9 +126,11 @@ describe('EnvironmentValidator', () => { const version = await validator.getToolchainVersion('0.26.0'); expect(version).toBe('Compactc version: 0.26.0'); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.26.0 --version', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '+0.26.0', + '--version', + ]); }); }); @@ -237,6 +242,58 @@ describe('FileDiscovery', () => { expect(files).toEqual(['Ownable.compact']); }); + + it('should skip files matching name-only exclude patterns', async () => { + const excludingDiscovery = new FileDiscovery('src', [ + 'Mock*', + '*.mock.compact', + ]); + const mockDirents = [ + { name: 'Token.compact', isFile: () => true, isDirectory: () => false }, + { + name: 'MockToken.compact', + isFile: () => true, + isDirectory: () => false, + }, + { + name: 'Token.mock.compact', + isFile: () => true, + isDirectory: () => false, + }, + ]; + + mockReaddir.mockResolvedValue(mockDirents as any); + + const files = await excludingDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['Token.compact']); + }); + + it('should skip files matching path globs', async () => { + // Path-style patterns (containing `/`) are matched against the full + // path as `find srcDir` would emit it, so `*/archive/*` works the same + // way as `find -path '*/archive/*'`. + const excludingDiscovery = new FileDiscovery('src', ['*/archive/*']); + const mockDirents = [ + { name: 'Token.compact', isFile: () => true, isDirectory: () => false }, + { name: 'archive', isFile: () => false, isDirectory: () => true }, + ]; + const mockArchiveDirents = [ + { + name: 'Legacy.compact', + isFile: () => true, + isDirectory: () => false, + }, + ]; + + mockReaddir + .mockResolvedValueOnce(mockDirents as any) + .mockResolvedValueOnce(mockArchiveDirents as any); + + const files = await excludingDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['Token.compact']); + }); }); }); @@ -260,9 +317,12 @@ describe('CompilerService', () => { const result = await service.compileFile('MyToken.compact', '--skip-zk'); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/MyToken.compact', + 'artifacts/MyToken', + ]); }); it('should compile file with version flag', async () => { @@ -278,9 +338,13 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.26.0 --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '+0.26.0', + '--skip-zk', + 'src/MyToken.compact', + 'artifacts/MyToken', + ]); }); it('should handle empty flags', async () => { @@ -292,9 +356,11 @@ describe('CompilerService', () => { const result = await service.compileFile('MyToken.compact', ''); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile "src/MyToken.compact" "artifacts/MyToken"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + 'src/MyToken.compact', + 'artifacts/MyToken', + ]); }); it('should use flattened artifacts output by default', async () => { @@ -309,9 +375,12 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/access/AccessControl.compact" "artifacts/AccessControl"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/access/AccessControl.compact', + 'artifacts/AccessControl', + ]); }); it('should flatten nested directory structure by default', async () => { @@ -326,9 +395,12 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/access/test/AccessControl.mock.compact" "artifacts/AccessControl.mock"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/access/test/AccessControl.mock.compact', + 'artifacts/AccessControl.mock', + ]); }); it('should throw CompilationError when compilation fails', async () => { @@ -380,9 +452,12 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/access/AccessControl.compact" "artifacts/access/AccessControl"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/access/AccessControl.compact', + 'artifacts/access/AccessControl', + ]); }); it('should preserve nested directory structure when hierarchical is true', async () => { @@ -397,9 +472,12 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/access/test/AccessControl.mock.compact" "artifacts/access/test/AccessControl.mock"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/access/test/AccessControl.mock.compact', + 'artifacts/access/test/AccessControl.mock', + ]); }); it('should use flattened output for root-level files even when hierarchical is true', async () => { @@ -411,9 +489,12 @@ describe('CompilerService', () => { const result = await service.compileFile('MyToken.compact', '--skip-zk'); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'src/MyToken.compact', + 'artifacts/MyToken', + ]); }); }); @@ -434,9 +515,12 @@ describe('CompilerService', () => { const result = await service.compileFile('MyToken.compact', '--skip-zk'); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "contracts/MyToken.compact" "build/MyToken"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'contracts/MyToken.compact', + 'build/MyToken', + ]); }); it('should use custom directories with hierarchical option', async () => { @@ -456,9 +540,12 @@ describe('CompilerService', () => { ); expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "contracts/access/AccessControl.compact" "dist/artifacts/access/AccessControl"', - ); + expect(mockExec).toHaveBeenCalledWith('compact', [ + 'compile', + '--skip-zk', + 'contracts/access/AccessControl.compact', + 'dist/artifacts/access/AccessControl', + ]); }); }); }); @@ -694,12 +781,27 @@ describe('CompactCompiler', () => { expect(compiler.testOptions.flags).toBe('--skip-zk --verbose'); }); - it('should deduplicate flags when both env var and CLI flag are present', () => { + it('should preserve repeatable flags without deduplication', () => { + // Forwarding args in original order is important: repeatable flags + // (e.g. `--define x=1 --define y=2`) must reach the compiler unchanged. + // SKIP_ZK=true env + --skip-zk on the CLI consequently produces two + // `--skip-zk` entries, which is harmless for boolean flags. compiler = CompactCompiler.fromArgs(['--skip-zk', '--verbose'], { SKIP_ZK: 'true', }); - expect(compiler.testOptions.flags).toBe('--skip-zk --verbose'); + expect(compiler.testOptions.flags).toBe('--skip-zk --skip-zk --verbose'); + }); + + it('should forward repeated flags in order', () => { + compiler = CompactCompiler.fromArgs([ + '--define', + 'x=1', + '--define', + 'y=2', + ]); + + expect(compiler.testOptions.flags).toBe('--define x=1 --define y=2'); }); it('should throw error for --dir without argument', () => { @@ -798,6 +900,31 @@ describe('CompactCompiler', () => { '--out flag requires a directory path', ); }); + + it('should accumulate repeated --exclude patterns', () => { + compiler = CompactCompiler.fromArgs([ + '--exclude', + 'Mock*', + '--exclude', + '*.mock.compact', + ]); + + expect(compiler.testOptions.exclude).toEqual(['Mock*', '*.mock.compact']); + }); + + it('should default exclude to empty array when not specified', () => { + compiler = CompactCompiler.fromArgs([]); + expect(compiler.testOptions.exclude).toEqual([]); + }); + + it('should throw for --exclude without a pattern', () => { + expect(() => CompactCompiler.fromArgs(['--exclude'])).toThrow( + '--exclude flag requires a pattern', + ); + expect(() => + CompactCompiler.fromArgs(['--exclude', '--skip-zk']), + ).toThrow('--exclude flag requires a pattern'); + }); }); describe('validateEnvironment', () => { @@ -826,12 +953,13 @@ describe('CompactCompiler', () => { // Check steps expect(mockExec).toHaveBeenCalledTimes(3); - expect(mockExec).toHaveBeenNthCalledWith(1, 'compact --version'); // validate() calls - expect(mockExec).toHaveBeenNthCalledWith(2, 'compact --version'); // getDevToolsVersion() - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.26.0 --version', - ); // getToolchainVersion() + expect(mockExec).toHaveBeenNthCalledWith(1, 'compact', ['--version']); // validate() calls + expect(mockExec).toHaveBeenNthCalledWith(2, 'compact', ['--version']); // getDevToolsVersion() + expect(mockExec).toHaveBeenNthCalledWith(3, 'compact', [ + 'compile', + '+0.26.0', + '--version', + ]); // getToolchainVersion() // Verify passed args expect(displaySpy).toHaveBeenCalledWith( @@ -904,10 +1032,11 @@ describe('CompactCompiler', () => { await compiler.validateEnvironment(); // Verify version-specific toolchain call - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.26.0 --version', - ); + expect(mockExec).toHaveBeenNthCalledWith(3, 'compact', [ + 'compile', + '+0.26.0', + '--version', + ]); expect(displaySpy).toHaveBeenCalledWith( 'compact 0.1.0', 'Compactc version: 0.26.0', @@ -935,7 +1064,10 @@ describe('CompactCompiler', () => { await compiler.validateEnvironment(); // Verify default toolchain call (no version flag) - expect(mockExec).toHaveBeenNthCalledWith(3, 'compact compile --version'); + expect(mockExec).toHaveBeenNthCalledWith(3, 'compact', [ + 'compile', + '--version', + ]); expect(displaySpy).toHaveBeenCalledWith( 'compact 0.1.0', 'Compactc version: 0.26.0', @@ -981,7 +1113,8 @@ describe('CompactCompiler', () => { await compiler.compile(); expect(mockExec).toHaveBeenCalledWith( - expect.stringContaining('compact compile --skip-zk'), + 'compact', + expect.arrayContaining(['compile', '--skip-zk']), ); }); @@ -1077,6 +1210,7 @@ describe('CompactCompiler', () => { '+0.26.0', ], env: { SKIP_ZK: 'true' }, + flags: '--skip-zk --no-communications-commitment', }, { name: 'with skip-zk flag only', @@ -1088,8 +1222,12 @@ describe('CompactCompiler', () => { '+0.26.0', ], env: { SKIP_ZK: 'false' }, + flags: '--skip-zk --no-communications-commitment', }, { + // CLI `--skip-zk` plus `SKIP_ZK=true` produces two `--skip-zk` entries + // because we no longer dedup forwarded flags — repeatable flags like + // `--define x=1 --define y=2` must be preserved as given. name: 'with both skip-zk flag and env var', args: [ '--dir', @@ -1099,13 +1237,12 @@ describe('CompactCompiler', () => { '+0.26.0', ], env: { SKIP_ZK: 'true' }, + flags: '--skip-zk --skip-zk --no-communications-commitment', }, - ])('should handle complex command $name', ({ args, env }) => { + ])('should handle complex command $name', ({ args, env, flags }) => { compiler = CompactCompiler.fromArgs(args, env); - expect(compiler.testOptions.flags).toBe( - '--skip-zk --no-communications-commitment', - ); + expect(compiler.testOptions.flags).toBe(flags); expect(compiler.testOptions.targetDir).toBe('security'); expect(compiler.testOptions.version).toBe('0.26.0'); }); diff --git a/packages/builder/tsconfig.json b/packages/builder/tsconfig.json new file mode 100644 index 0000000..e2ab1e9 --- /dev/null +++ b/packages/builder/tsconfig.json @@ -0,0 +1,21 @@ +{ + "extends": "@tsconfig/node24/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"], + "declaration": true, + "skipLibCheck": true, + "sourceMap": true, + "rewriteRelativeImportExtensions": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist" + ] +} diff --git a/packages/builder/vitest.config.ts b/packages/builder/vitest.config.ts new file mode 100644 index 0000000..d57e53a --- /dev/null +++ b/packages/builder/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.ts'], + reporters: 'verbose', + }, +}); diff --git a/packages/cli/README.md b/packages/cli/README.md index 966bb61..b3fad84 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -1,250 +1,73 @@ -# @openzeppelin/compact-tools-cli +# @openzeppelin/compact-cli -CLI utilities for compiling and building Compact smart contracts. +CLI wrapper around [`@openzeppelin/compact-builder`](../builder). +Provides the `compact-compiler` and `compact-builder` binaries for use in +`package.json` scripts. Contains no programmatic API of its own. If you want +to call the compiler/builder from TypeScript, use the library package directly. -## Installation - -Until published to npm, use via git submodule or local path: - -```bash -# As a local dependency -yarn add @openzeppelin/compact-tools-cli@file:./compact-tools/packages/cli - -# Or invoke directly after building -node compact-tools/packages/cli/dist/runCompiler.js -``` - -## Requirements - -- Node.js >= 20 -- Midnight Compact toolchain installed and available in `PATH` - -Verify your Compact installation: - -```bash -$ compact compile --version -Compactc version: 0.28.0 -``` - -## Binaries - -This package provides two CLI binaries: - -| Binary | Script | Description | -|--------|--------|-------------| -| `compact-compiler` | `dist/runCompiler.js` | Compile `.compact` files to artifacts | -| `compact-builder` | `dist/runBuilder.js` | Compile + build TypeScript + copy artifacts | - -## Compiler CLI - -### Usage - -```bash -compact-compiler [options] -``` - -### Options - -| Option | Description | Default | -|--------|-------------|---------| -| `--dir ` | Compile specific subdirectory within src | (all) | -| `--src ` | Source directory containing `.compact` files | `src` | -| `--out ` | Output directory for compiled artifacts | `artifacts` | -| `--hierarchical` | Preserve source directory structure in output | `false` | -| `--skip-zk` | Skip zero-knowledge proof generation | `false` | -| `+` | Use specific toolchain version (e.g., `+0.28.0`) | (default) | - -### Environment Variables - -| Variable | Description | -|----------|-------------| -| `SKIP_ZK=true` | Equivalent to `--skip-zk` flag | - -### Artifact Output Structure - -**Default (flattened):** All contract artifacts go directly under the output directory. - -``` -src/ - access/ - AccessControl.compact - token/ - Token.compact - -artifacts/ # Flattened output - AccessControl/ - Token/ -``` - -**Hierarchical (`--hierarchical`):** Preserves source directory structure. - -``` -artifacts/ # Hierarchical output - access/ - AccessControl/ - token/ - Token/ -``` - -### Examples - -```bash -# Compile all contracts (flattened output) -compact-compiler - -# Compile with hierarchical artifact structure -compact-compiler --hierarchical - -# Compile specific directory only -compact-compiler --dir security - -# Skip ZK proof generation (faster, for development) -compact-compiler --skip-zk - -# Use specific toolchain version -compact-compiler +0.28.0 - -# Custom source and output directories -compact-compiler --src contracts --out build - -# Combine options -compact-compiler --dir access --skip-zk --hierarchical - -# Use environment variable -SKIP_ZK=true compact-compiler -``` - -## Builder CLI - -The builder runs the compiler as a prerequisite, then executes additional build steps: - -1. Compile `.compact` files (via `compact-compiler`) -2. Compile TypeScript (`tsc --project tsconfig.build.json`) -3. Copy artifacts to `dist/artifacts/` -4. Copy and clean `.compact` files to `dist/` - -### Usage +## Install ```bash -compact-builder [options] +yarn add --dev @openzeppelin/compact-cli ``` -Accepts all compiler options except `--skip-zk` (builds always include ZK proofs). - -### Examples +## Use ```bash -# Full build -compact-builder - -# Build specific directory -compact-builder --dir token - -# Build with custom directories -compact-builder --src contracts --out build +yarn compact-compiler --help +yarn compact-builder --help ``` -## Programmatic API - -The compiler can be used programmatically: - -```typescript -import { CompactCompiler } from '@openzeppelin/compact-tools-cli'; - -// Using options object -const compiler = new CompactCompiler({ - flags: '--skip-zk', - targetDir: 'security', - version: '0.28.0', - hierarchical: true, - srcDir: 'src', - outDir: 'artifacts', -}); - -await compiler.compile(); - -// Using factory method (parses CLI-style args) -const compiler = CompactCompiler.fromArgs([ - '--dir', 'security', - '--skip-zk', - '+0.28.0' -]); - -await compiler.compile(); -``` - -### Classes and Types - -```typescript -// Main compiler class -class CompactCompiler { - constructor(options?: CompilerOptions, execFn?: ExecFunction); - static fromArgs(args: string[], env?: NodeJS.ProcessEnv): CompactCompiler; - static parseArgs(args: string[], env?: NodeJS.ProcessEnv): CompilerOptions; - compile(): Promise; - validateEnvironment(): Promise; -} - -// Builder class -class CompactBuilder { - constructor(options?: CompilerOptions); - static fromArgs(args: string[], env?: NodeJS.ProcessEnv): CompactBuilder; - build(): Promise; -} - -// Options interface -interface CompilerOptions { - flags?: string; // Compiler flags (e.g., '--skip-zk --verbose') - targetDir?: string; // Subdirectory within srcDir to compile - version?: string; // Toolchain version (e.g., '0.28.0') - hierarchical?: boolean; // Preserve directory structure in output - srcDir?: string; // Source directory (default: 'src') - outDir?: string; // Output directory (default: 'artifacts') +Typical `package.json` scripts (replace `` with the Compact +toolchain release you want to pin, e.g. `+0.29.0`): + +```json +{ + "scripts": { + "compact": "compact-compiler + --exclude '*/archive/*'", + "compact:access": "compact-compiler + --dir access", + "build": "compact-builder + --clean-dist --hierarchical --copy package.json --copy ../README.md", + "test": "compact-compiler + --skip-zk && vitest run" + } } ``` -### Error Types +## Options -```typescript -import { - CompactCliNotFoundError, // Compact CLI not in PATH - CompilationError, // Compilation failed (includes file path) - DirectoryNotFoundError, // Target directory doesn't exist -} from '@openzeppelin/compact-tools-cli'; -``` +Both binaries accept the same compiler-side options (forwarded to the +underlying library); `compact-builder` additionally accepts dist-layout +options: -## Development +| Flag | Applies to | Description | +|---|---|---| +| `--dir ` | both | Scope to a subdirectory inside `--src`. | +| `--src ` | both | Source directory containing `.compact` files (default: `src`). | +| `--out ` | both | Output directory for compiled artifacts (default: `artifacts`). | +| `--hierarchical` | both | Preserve source directory structure in artifacts AND in the builder's `.compact` copy. | +| `--exclude ` | both | Skip `.compact` files matching the glob (repeatable). Default for the builder: `Mock*`, `*.mock.compact`. | +| `--skip-zk` | compiler | Skip zero-knowledge proof generation (also via `SKIP_ZK=true` env var). | +| `+` | both | Pin the Compact toolchain version (e.g `+0.29.0`). | +| `--clean-dist` | builder | `rm -rf dist` before building. | +| `--copy ` | builder | Copy an extra file into `dist/` (repeatable; e.g. `package.json`, `../README.md`). | -```bash -cd packages/cli +See [`@openzeppelin/compact-builder`](../builder) for the full +documentation, programmatic API, and behavioural details. -# Build -yarn build - -# Type-check only -yarn types +## Requirements -# Run tests -yarn test +- Node.js >= 20 +- Midnight Compact toolchain installed and available in `PATH` -# Clean -yarn clean +```bash +$ compact compile --version +Compactc version: 0.29.0 ``` -## Output Example +## See also -```bash -ℹ [COMPILE] Compact compiler started -ℹ [COMPILE] Compact developer tools: compact 0.2.0 -ℹ [COMPILE] Compact toolchain: Compactc version: 0.28.0 -ℹ [COMPILE] Found 2 .compact file(s) to compile -✔ [COMPILE] [1/2] Compiled AccessControl.compact - Compactc version: 0.28.0 -✔ [COMPILE] [2/2] Compiled Token.compact - Compactc version: 0.28.0 -``` +- [`@openzeppelin/compact-builder`](https://www.npmjs.com/package/@openzeppelin/compact-builder) — programmatic library backing this CLI +- [`@openzeppelin/compact-simulator`](https://www.npmjs.com/package/@openzeppelin/compact-simulator) — simulator for testing Compact contracts ## License MIT - diff --git a/packages/cli/package.json b/packages/cli/package.json index 5cf08ab..11c8d91 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin/compact-tools-cli", + "name": "@openzeppelin/compact-cli", "description": "CLI for compiling and building Compact smart contracts", "version": "0.0.1", "keywords": [ @@ -9,10 +9,18 @@ "builder", "testing" ], - "author": "", + "author": "OpenZeppelin Community ", "license": "MIT", "type": "module", - "exports": "./index.js", + "exports": { + "./run-builder": "./dist/runBuilder.js", + "./run-compiler": "./dist/runCompiler.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], "engines": { "node": ">=20" }, @@ -33,8 +41,8 @@ "vitest": "^4.0.15" }, "dependencies": { + "@openzeppelin/compact-builder": "workspace:^", "chalk": "^5.6.2", - "log-symbols": "^7.0.0", "ora": "^9.0.0" } } diff --git a/packages/cli/src/Builder.ts b/packages/cli/src/Builder.ts deleted file mode 100755 index f94027f..0000000 --- a/packages/cli/src/Builder.ts +++ /dev/null @@ -1,183 +0,0 @@ -#!/usr/bin/env node - -import { exec } from 'node:child_process'; -import { promisify } from 'node:util'; -import chalk from 'chalk'; -import ora, { type Ora } from 'ora'; -import { CompactCompiler, type CompilerOptions } from './Compiler.ts'; -import { isPromisifiedChildProcessError } from './types/errors.ts'; - -// Promisified exec for async execution -const execAsync = promisify(exec); - -/** - * Configuration options for the Builder CLI. - * Inherits from CompilerOptions but excludes `flags` (which would allow --skip-zk). - * Builds should always include ZK proofs. - */ -export type BuilderOptions = Omit; - -/** - * A class to handle the build process for a project. - * Runs CompactCompiler as a prerequisite, then executes build steps (TypeScript compilation, - * artifact copying, etc.) - * with progress feedback and colored output for success and error states. - * - * @notice `cmd` scripts discard `stderr` output and fail silently because this is - * handled in `executeStep`. - * - * @example - * ```typescript - * const builder = new CompactBuilder({ flags: '--skip-zk' }); - * builder.build().catch(err => console.error(err)); - * ``` - * - * @example Successful Build Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✔ [COMPILE] [1/2] Compiled AccessControl.compact - * Compactc version: 0.26.0 - * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact - * Compactc version: 0.26.0 - * ✔ [BUILD] [1/3] Compiling TypeScript - * ✔ [BUILD] [2/3] Copying artifacts - * ✔ [BUILD] [3/3] Copying and cleaning .compact files - * ``` - * - * @example Failed Compilation Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✖ [COMPILE] [1/2] Failed AccessControl.compact - * Compactc version: 0.26.0 - * Error: Expected ';' at line 5 in AccessControl.compact - * ``` - * - * @example Failed Build Step Output - * ``` - * ℹ [COMPILE] Found 2 .compact file(s) to compile - * ✔ [COMPILE] [1/2] Compiled AccessControl.compact - * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact - * ✖ [BUILD] [1/3] Failed Compiling TypeScript - * error TS1005: ';' expected at line 10 in file.ts - * [BUILD] ❌ Build failed: Command failed: tsc --project tsconfig.build.json - * ``` - */ -export class CompactBuilder { - private readonly options: BuilderOptions; - private readonly steps: Array<{ cmd: string; msg: string; shell?: string }> = - [ - { - cmd: 'tsc --project tsconfig.build.json', - msg: 'Compiling TypeScript', - }, - { - cmd: 'mkdir -p dist/artifacts && cp -Rf src/artifacts/* dist/artifacts/ 2>/dev/null || true', - msg: 'Copying artifacts', - shell: '/bin/bash', - }, - { - cmd: 'mkdir -p dist && find src -type f -name "*.compact" -exec cp {} dist/ \\; 2>/dev/null && rm dist/Mock*.compact 2>/dev/null || true', - msg: 'Copying and cleaning .compact files', - shell: '/bin/bash', - }, - ]; - - /** - * Constructs a new CompactBuilder instance. - * @param options - Compiler options (flags, srcDir, outDir, hierarchical, etc.) - */ - constructor(options: CompilerOptions = {}) { - this.options = options; - } - - /** - * Factory method to create a CompactBuilder from command-line arguments. - * Reuses CompactCompiler.parseArgs for consistent argument parsing. - * - * @param args - Array of command-line arguments - * @param env - Environment variables (defaults to process.env) - * @returns New CompactBuilder instance configured from arguments - */ - static fromArgs( - args: string[], - env: NodeJS.ProcessEnv = process.env, - ): CompactBuilder { - const options = CompactCompiler.parseArgs(args, env); - return new CompactBuilder(options); - } - - /** - * Executes the full build process: compiles .compact files first, then runs build steps. - * Displays progress with spinners and outputs results in color. - * - * @returns A promise that resolves when all steps complete successfully - * @throws Error if compilation or any build step fails - */ - public async build(): Promise { - // Run compact compilation as a prerequisite - const compiler = new CompactCompiler(this.options); - await compiler.compile(); - - // Proceed with build steps - for (const [index, step] of this.steps.entries()) { - await this.executeStep(step, index, this.steps.length); - } - } - - /** - * Executes a single build step. - * Runs the command, shows a spinner, and prints output with indentation. - * - * @param step - The build step containing command and message - * @param index - Current step index (0-based) for progress display - * @param total - Total number of steps for progress display - * @returns A promise that resolves when the step completes successfully - * @throws Error if the step fails - */ - private async executeStep( - step: { cmd: string; msg: string; shell?: string }, - index: number, - total: number, - ): Promise { - const stepLabel: string = `[${index + 1}/${total}]`; - const spinner: Ora = ora(`[BUILD] ${stepLabel} ${step.msg}`).start(); - - try { - const { stdout, stderr }: { stdout: string; stderr: string } = - await execAsync(step.cmd, { - shell: step.shell, // Only pass shell where needed - }); - spinner.succeed(`[BUILD] ${stepLabel} ${step.msg}`); - this.printOutput(stdout, chalk.cyan); - this.printOutput(stderr, chalk.yellow); // Show stderr (warnings) in yellow if present - } catch (error: unknown) { - spinner.fail(`[BUILD] ${stepLabel} ${step.msg}`); - if (isPromisifiedChildProcessError(error)) { - this.printOutput(error.stdout, chalk.cyan); - this.printOutput(error.stderr, chalk.red); - // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason - console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); - } else if (error instanceof Error) { - // biome-ignore lint/suspicious/noConsole: Needed to display build failure reason - console.error(chalk.red('[BUILD] ❌ Build failed:', error.message)); - } - - process.exit(1); - } - } - - /** - * Prints command output with indentation and specified color. - * Filters out empty lines and indents each line for readability. - * - * @param output - The command output string to print (stdout or stderr) - * @param colorFn - Chalk color function to style the output (e.g., `chalk.cyan` for success, `chalk.red` for errors) - */ - private printOutput(output: string, colorFn: (text: string) => string): void { - const lines: string[] = output - .split('\n') - .filter((line: string): boolean => line.trim() !== '') - .map((line: string): string => ` ${line}`); - console.log(colorFn(lines.join('\n'))); - } -} diff --git a/packages/cli/src/Compiler.ts b/packages/cli/src/Compiler.ts deleted file mode 100755 index e20d0fb..0000000 --- a/packages/cli/src/Compiler.ts +++ /dev/null @@ -1,864 +0,0 @@ -#!/usr/bin/env node - -import { exec as execCallback } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { basename, dirname, join, relative } from 'node:path'; -import { promisify } from 'node:util'; -import chalk from 'chalk'; -import ora from 'ora'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, - isPromisifiedChildProcessError, -} from './types/errors.ts'; - -/** Default source directory containing .compact files */ -const DEFAULT_SRC_DIR = 'src'; -/** Default output directory for compiled artifacts */ -const DEFAULT_OUT_DIR = 'artifacts'; - -/** - * Function type for executing shell commands. - * Allows dependency injection for testing and customization. - * - * @param command - The shell command to execute - * @returns Promise resolving to command output - */ -export type ExecFunction = ( - command: string, -) => Promise<{ stdout: string; stderr: string }>; - -/** - * Configuration options for the Compact compiler CLI. - * - * @interface CompilerOptions - * @example - * ```typescript - * const options: CompilerOptions = { - * flags: '--skip-zk --verbose', - * targetDir: 'security', - * version: '0.26.0', - * hierarchical: false, - * }; - * ``` - */ -export interface CompilerOptions { - /** Compiler flags to pass to the Compact CLI (e.g., '--skip-zk --verbose') */ - flags?: string; - /** Optional subdirectory within srcDir to compile (e.g., 'security', 'token') */ - targetDir?: string; - /** Optional toolchain version to use (e.g., '0.26.0') */ - version?: string; - /** - * Whether to preserve directory structure in artifacts output. - * - `false` (default): Flattened output - `//` - * - `true`: Hierarchical output - `///` - */ - hierarchical?: boolean; - /** Source directory containing .compact files (default: 'src') */ - srcDir?: string; - /** Output directory for compiled artifacts (default: 'artifacts') */ - outDir?: string; -} - -/** Resolved compiler options with defaults applied */ -type ResolvedCompilerOptions = Required< - Pick -> & - Pick; - -/** - * Service responsible for validating the Compact CLI environment. - * Checks CLI availability, retrieves version information, and ensures - * the toolchain is properly configured before compilation. - * - * @class EnvironmentValidator - * @example - * ```typescript - * const validator = new EnvironmentValidator(); - * await validator.validate('0.26.0'); - * const version = await validator.getDevToolsVersion(); - * ``` - */ -export class EnvironmentValidator { - private execFn: ExecFunction; - - /** - * Creates a new EnvironmentValidator instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - - /** - * Checks if the Compact CLI is available in the system PATH. - * - * @returns Promise resolving to true if CLI is available, false otherwise - * @example - * ```typescript - * const isAvailable = await validator.checkCompactAvailable(); - * if (!isAvailable) { - * throw new Error('Compact CLI not found'); - * } - * ``` - */ - async checkCompactAvailable(): Promise { - try { - await this.execFn('compact --version'); - return true; - } catch { - return false; - } - } - - /** - * Retrieves the version of the Compact developer tools. - * - * @returns Promise resolving to the version string - * @throws {Error} If the CLI is not available or command fails - * @example - * ```typescript - * const version = await validator.getDevToolsVersion(); - * console.log(`Using Compact ${version}`); - * ``` - */ - async getDevToolsVersion(): Promise { - const { stdout } = await this.execFn('compact --version'); - return stdout.trim(); - } - - /** - * Retrieves the version of the Compact toolchain/compiler. - * - * @param version - Optional specific toolchain version to query - * @returns Promise resolving to the toolchain version string - * @throws {Error} If the CLI is not available or command fails - * @example - * ```typescript - * const toolchainVersion = await validator.getToolchainVersion('0.26.0'); - * console.log(`Toolchain: ${toolchainVersion}`); - * ``` - */ - async getToolchainVersion(version?: string): Promise { - const versionFlag = version ? `+${version}` : ''; - const { stdout } = await this.execFn( - `compact compile ${versionFlag} --version`, - ); - return stdout.trim(); - } - - /** - * Validates the entire Compact environment and ensures it's ready for compilation. - * Checks CLI availability and retrieves version information. - * - * @param version - Optional specific toolchain version to validate - * @throws {CompactCliNotFoundError} If the Compact CLI is not available - * @throws {Error} If version commands fail - * @example - * ```typescript - * try { - * await validator.validate('0.26.0'); - * console.log('Environment validated successfully'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } - * ``` - */ - async validate( - version?: string, - ): Promise<{ devToolsVersion: string; toolchainVersion: string }> { - const isAvailable = await this.checkCompactAvailable(); - if (!isAvailable) { - throw new CompactCliNotFoundError( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - } - - const devToolsVersion = await this.getDevToolsVersion(); - const toolchainVersion = await this.getToolchainVersion(version); - - return { devToolsVersion, toolchainVersion }; - } -} - -/** - * Service responsible for discovering .compact files in the source directory. - * Recursively scans directories and filters for .compact file extensions. - * - * @class FileDiscovery - * @example - * ```typescript - * const discovery = new FileDiscovery('src'); - * const files = await discovery.getCompactFiles('src/security'); - * console.log(`Found ${files.length} .compact files`); - * ``` - */ -export class FileDiscovery { - private srcDir: string; - - /** - * Creates a new FileDiscovery instance. - * - * @param srcDir - Base source directory for relative path calculation (default: 'src') - */ - constructor(srcDir: string = DEFAULT_SRC_DIR) { - this.srcDir = srcDir; - } - - /** - * Recursively discovers all .compact files in a directory. - * Returns relative paths from the srcDir for consistent processing. - * - * @param dir - Directory path to search (relative or absolute) - * @returns Promise resolving to array of relative file paths - * @example - * ```typescript - * const files = await discovery.getCompactFiles('src'); - * // Returns: ['contracts/Token.compact', 'security/AccessControl.compact'] - * ``` - */ - async getCompactFiles(dir: string): Promise { - try { - const dirents = await readdir(dir, { withFileTypes: true }); - const filePromises = dirents.map(async (entry) => { - const fullPath = join(dir, entry.name); - try { - if (entry.isDirectory()) { - return await this.getCompactFiles(fullPath); - } - - if (entry.isFile() && fullPath.endsWith('.compact')) { - return [relative(this.srcDir, fullPath)]; - } - return []; - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and file path - console.warn(`Error accessing ${fullPath}:`, err); - return []; - } - }); - - const results = await Promise.all(filePromises); - return results.flat(); - } catch (err) { - // biome-ignore lint/suspicious/noConsole: Needed to display error and dir path - console.error(`Failed to read dir: ${dir}`, err); - return []; - } - } -} - -/** - * Options for configuring the CompilerService. - * A subset of CompilerOptions relevant to the compilation process. - */ -export type CompilerServiceOptions = Pick< - CompilerOptions, - 'hierarchical' | 'srcDir' | 'outDir' ->; - -/** Resolved options for CompilerService with defaults applied */ -type ResolvedCompilerServiceOptions = Required; - -/** - * Service responsible for compiling individual .compact files. - * Handles command construction, execution, and error processing. - * - * @class CompilerService - * @example - * ```typescript - * const compiler = new CompilerService(); - * const result = await compiler.compileFile( - * 'contracts/Token.compact', - * '--skip-zk --verbose', - * '0.26.0' - * ); - * console.log('Compilation output:', result.stdout); - * ``` - */ -export class CompilerService { - private execFn: ExecFunction; - private options: ResolvedCompilerServiceOptions; - - /** - * Creates a new CompilerService instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - * @param options - Compiler service options - */ - constructor( - execFn: ExecFunction = promisify(execCallback), - options: CompilerServiceOptions = {}, - ) { - this.execFn = execFn; - this.options = { - hierarchical: options.hierarchical ?? false, - srcDir: options.srcDir ?? DEFAULT_SRC_DIR, - outDir: options.outDir ?? DEFAULT_OUT_DIR, - }; - } - - /** - * Compiles a single .compact file using the Compact CLI. - * Constructs the appropriate command with flags and version, then executes it. - * - * By default, uses flattened output structure where all artifacts go to `//`. - * When `hierarchical` is true, preserves source directory structure: `///`. - * - * @param file - Relative path to the .compact file from srcDir - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param version - Optional specific toolchain version to use - * @returns Promise resolving to compilation output (stdout/stderr) - * @throws {CompilationError} If compilation fails for any reason - * @example - * ```typescript - * try { - * const result = await compiler.compileFile( - * 'security/AccessControl.compact', - * '--skip-zk', - * '0.26.0' - * ); - * console.log('Success:', result.stdout); - * } catch (error) { - * if (error instanceof CompilationError) { - * console.error('Compilation failed for', error.file); - * } - * } - * ``` - */ - async compileFile( - file: string, - flags: string, - version?: string, - ): Promise<{ stdout: string; stderr: string }> { - const inputPath = join(this.options.srcDir, file); - const fileDir = dirname(file); - const fileName = basename(file, '.compact'); - - // Flattened (default): // - // Hierarchical: /// - const outputDir = - this.options.hierarchical && fileDir !== '.' - ? join(this.options.outDir, fileDir, fileName) - : join(this.options.outDir, fileName); - - const versionFlag = version ? `+${version}` : ''; - const flagsStr = flags ? ` ${flags}` : ''; - const command = `compact compile${versionFlag ? ` ${versionFlag}` : ''}${flagsStr} "${inputPath}" "${outputDir}"`; - - try { - return await this.execFn(command); - } catch (error: unknown) { - let message: string; - - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); // fallback for strings, objects, numbers, etc. - } - - throw new CompilationError( - `Failed to compile ${file}: ${message}`, - file, - error, - ); - } - } -} - -/** - * Utility service for handling user interface output and formatting. - * Provides consistent styling and formatting for compiler messages and output. - * - * @class UIService - * @example - * ```typescript - * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.26.0', 'security'); - * UIService.printOutput('Compilation successful', chalk.green); - * ``` - */ -export const UIService = { - /** - * Prints formatted output with consistent indentation and coloring. - * Filters empty lines and adds consistent indentation for readability. - * - * @param output - Raw output text to format - * @param colorFn - Chalk color function for styling - * @example - * ```typescript - * UIService.printOutput(stdout, chalk.cyan); - * UIService.printOutput(stderr, chalk.red); - * ``` - */ - printOutput(output: string, colorFn: (text: string) => string): void { - const lines = output - .split('\n') - .filter((line) => line.trim() !== '') - .map((line) => ` ${line}`); - console.log(colorFn(lines.join('\n'))); - }, - - /** - * Displays environment information including tool versions and configuration. - * Shows developer tools version, toolchain version, and optional settings. - * - * @param devToolsVersion - Version string of the Compact developer tools - * @param toolchainVersion - Version string of the Compact toolchain/compiler - * @param targetDir - Optional target directory being compiled - * @param version - Optional specific version being used - * @example - * ```typescript - * UIService.displayEnvInfo( - * 'compact 0.1.0', - * 'Compactc version: 0.26.0', - * 'security', - * '0.26.0' - * ); - * ``` - */ - displayEnvInfo( - devToolsVersion: string, - toolchainVersion: string, - targetDir?: string, - version?: string, - ): void { - const spinner = ora(); - - if (targetDir) { - spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${targetDir}`)); - } - - spinner.info( - chalk.blue(`[COMPILE] Compact developer tools: ${devToolsVersion}`), - ); - spinner.info( - chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), - ); - - if (version) { - spinner.info(chalk.blue(`[COMPILE] Using toolchain version: ${version}`)); - } - }, - - /** - * Displays compilation start message with file count and optional location. - * - * @param fileCount - Number of files to be compiled - * @param targetDir - Optional target directory being compiled - * @example - * ```typescript - * UIService.showCompilationStart(5, 'security'); - * // Output: "Found 5 .compact file(s) to compile in security/" - * ``` - */ - showCompilationStart(fileCount: number, targetDir?: string): void { - const searchLocation = targetDir ? ` in ${targetDir}/` : ''; - const spinner = ora(); - spinner.info( - chalk.blue( - `[COMPILE] Found ${fileCount} .compact file(s) to compile${searchLocation}`, - ), - ); - }, - - /** - * Displays a warning message when no .compact files are found. - * - * @param targetDir - Optional target directory that was searched - * @example - * ```typescript - * UIService.showNoFiles('security'); - * // Output: "No .compact files found in security/." - * ``` - */ - showNoFiles(targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; - const spinner = ora(); - spinner.warn( - chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), - ); - }, -}; - -/** - * Main compiler class that orchestrates the compilation process. - * Coordinates environment validation, file discovery, and compilation services - * to provide a complete .compact file compilation solution. - * - * Features: - * - Dependency injection for testability - * - Structured error propagation with custom error types - * - Progress reporting and user feedback - * - Support for compiler flags and toolchain versions - * - Environment variable integration - * - Configurable artifact output structure (flattened or hierarchical) - * - * @class CompactCompiler - * @example - * ```typescript - * // Basic usage with options object (flattened artifacts by default) - * const compiler = new CompactCompiler({ - * flags: '--skip-zk', - * targetDir: 'security', - * version: '0.26.0', - * }); - * await compiler.compile(); - * - * // Factory method usage - * const compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - * await compiler.compile(); - * - * // With hierarchical artifacts structure - * const compiler = CompactCompiler.fromArgs(['--hierarchical', '--skip-zk']); - * await compiler.compile(); - * - * // With environment variables - * process.env.SKIP_ZK = 'true'; - * const compiler = CompactCompiler.fromArgs(['--dir', 'token']); - * await compiler.compile(); - * ``` - */ -export class CompactCompiler { - /** Environment validation service */ - private readonly environmentValidator: EnvironmentValidator; - /** File discovery service */ - private readonly fileDiscovery: FileDiscovery; - /** Compilation execution service */ - private readonly compilerService: CompilerService; - /** Compiler options */ - private readonly options: ResolvedCompilerOptions; - - /** - * Creates a new CompactCompiler instance with specified configuration. - * - * @param options - Compiler configuration options - * @param execFn - Optional custom exec function for dependency injection - * @example - * ```typescript - * // Compile all files with flags (flattened artifacts) - * const compiler = new CompactCompiler({ flags: '--skip-zk --verbose' }); - * - * // Compile specific directory - * const compiler = new CompactCompiler({ targetDir: 'security' }); - * - * // Compile with specific version - * const compiler = new CompactCompiler({ flags: '--skip-zk', version: '0.26.0' }); - * - * // Compile with hierarchical artifacts structure - * const compiler = new CompactCompiler({ flags: '--skip-zk', hierarchical: true }); - * - * // For testing with custom exec function - * const mockExec = vi.fn(); - * const compiler = new CompactCompiler({}, mockExec); - * ``` - */ - constructor(options: CompilerOptions = {}, execFn?: ExecFunction) { - this.options = { - flags: (options.flags ?? '').trim(), - targetDir: options.targetDir, - version: options.version, - hierarchical: options.hierarchical ?? false, - srcDir: options.srcDir ?? DEFAULT_SRC_DIR, - outDir: options.outDir ?? DEFAULT_OUT_DIR, - }; - this.environmentValidator = new EnvironmentValidator(execFn); - this.fileDiscovery = new FileDiscovery(this.options.srcDir); - this.compilerService = new CompilerService(execFn, { - hierarchical: this.options.hierarchical, - srcDir: this.options.srcDir, - outDir: this.options.outDir, - }); - } - - /** - * Parses command-line arguments into a CompilerOptions object. - * - * Supported argument patterns: - * - `--dir ` - Target specific subdirectory within srcDir - * - `--src ` - Source directory containing .compact files (default: 'src') - * - `--out ` - Output directory for artifacts (default: 'artifacts') - * - `--hierarchical` - Preserve source directory structure in artifacts output - * - `+` - Use specific toolchain version - * - Other arguments - Treated as compiler flags - * - `SKIP_ZK=true` environment variable - Adds --skip-zk flag - * - * @param args - Array of command-line arguments - * @param env - Environment variables (defaults to process.env) - * @returns Parsed CompilerOptions object - * @throws {Error} If --dir, --src, or --out flag is provided without a value - */ - static parseArgs( - args: string[], - env: NodeJS.ProcessEnv = process.env, - ): CompilerOptions { - const options: CompilerOptions = { - hierarchical: false, - }; - const flags: string[] = []; - - if (env.SKIP_ZK === 'true') { - flags.push('--skip-zk'); - } - - for (let i = 0; i < args.length; i++) { - if (args[i] === '--dir') { - const valueExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (valueExists) { - options.targetDir = args[i + 1]; - i++; - } else { - throw new Error('--dir flag requires a directory name'); - } - } else if (args[i] === '--src') { - const valueExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (valueExists) { - options.srcDir = args[i + 1]; - i++; - } else { - throw new Error('--src flag requires a directory path'); - } - } else if (args[i] === '--out') { - const valueExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (valueExists) { - options.outDir = args[i + 1]; - i++; - } else { - throw new Error('--out flag requires a directory path'); - } - } else if (args[i] === '--hierarchical') { - options.hierarchical = true; - } else if (args[i].startsWith('+')) { - options.version = args[i].slice(1); - } else { - // Only add flag if it's not already present - if (!flags.includes(args[i])) { - flags.push(args[i]); - } - } - } - - options.flags = flags.join(' '); - return options; - } - - /** - * Factory method to create a CompactCompiler from command-line arguments. - * Parses various argument formats including flags, directories, versions, and environment variables. - * - * Supported argument patterns: - * - `--dir ` - Target specific subdirectory within srcDir - * - `--src ` - Source directory containing .compact files (default: 'src') - * - `--out ` - Output directory for artifacts (default: 'artifacts') - * - `--hierarchical` - Preserve source directory structure in artifacts output - * - `+` - Use specific toolchain version - * - Other arguments - Treated as compiler flags - * - `SKIP_ZK=true` environment variable - Adds --skip-zk flag - * - * @param args - Array of command-line arguments - * @param env - Environment variables (defaults to process.env) - * @returns New CompactCompiler instance configured from arguments - * @throws {Error} If --dir, --src, or --out flag is provided without a value - * @example - * ```typescript - * // Parse command line: compact-compiler --dir security --skip-zk +0.26.0 - * const compiler = CompactCompiler.fromArgs([ - * '--dir', 'security', - * '--skip-zk', - * '+0.26.0' - * ]); - * - * // With custom source and output directories - * const compiler = CompactCompiler.fromArgs([ - * '--src', 'contracts', - * '--out', 'build/artifacts', - * '--skip-zk' - * ]); - * - * // With hierarchical artifacts structure - * const compiler = CompactCompiler.fromArgs([ - * '--hierarchical', - * '--skip-zk' - * ]); - * - * // With environment variable - * const compiler = CompactCompiler.fromArgs( - * ['--dir', 'token'], - * { SKIP_ZK: 'true' } - * ); - * - * // Empty args with environment - * const compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - * ``` - */ - static fromArgs( - args: string[], - env: NodeJS.ProcessEnv = process.env, - ): CompactCompiler { - const options = CompactCompiler.parseArgs(args, env); - return new CompactCompiler(options); - } - - /** - * Validates the compilation environment and displays version information. - * Performs environment validation, retrieves toolchain versions, and shows configuration details. - * - * Process: - * - * 1. Validates CLI availability and toolchain compatibility - * 2. Retrieves developer tools and compiler versions - * 3. Displays environment configuration information - * - * @throws {CompactCliNotFoundError} If Compact CLI is not available in PATH - * @throws {Error} If version retrieval or other validation steps fail - * @example - * ```typescript - * try { - * await compiler.validateEnvironment(); - * console.log('Environment ready for compilation'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } - * ``` - */ - async validateEnvironment(): Promise { - const { devToolsVersion, toolchainVersion } = - await this.environmentValidator.validate(this.options.version); - UIService.displayEnvInfo( - devToolsVersion, - toolchainVersion, - this.options.targetDir, - this.options.version, - ); - } - - /** - * Main compilation method that orchestrates the entire compilation process. - * - * Process flow: - * 1. Validates environment and shows configuration - * 2. Discovers .compact files in target directory - * 3. Compiles each file with progress reporting - * 4. Handles errors and provides user feedback - * - * @throws {CompactCliNotFoundError} If Compact CLI is not available - * @throws {DirectoryNotFoundError} If target directory doesn't exist - * @throws {CompilationError} If any file compilation fails - * @example - * ```typescript - * const compiler = new CompactCompiler('--skip-zk', 'security'); - * - * try { - * await compiler.compile(); - * console.log('All files compiled successfully'); - * } catch (error) { - * if (error instanceof DirectoryNotFoundError) { - * console.error(`Directory not found: ${error.directory}`); - * } else if (error instanceof CompilationError) { - * console.error(`Failed to compile: ${error.file}`); - * } - * } - * ``` - */ - async compile(): Promise { - await this.validateEnvironment(); - - const searchDir = this.options.targetDir - ? join(this.options.srcDir, this.options.targetDir) - : this.options.srcDir; - - // Validate target directory exists - if (this.options.targetDir && !existsSync(searchDir)) { - throw new DirectoryNotFoundError( - `Target directory ${searchDir} does not exist`, - searchDir, - ); - } - - const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); - - if (compactFiles.length === 0) { - UIService.showNoFiles(this.options.targetDir); - return; - } - - UIService.showCompilationStart(compactFiles.length, this.options.targetDir); - - for (const [index, file] of compactFiles.entries()) { - await this.compileFile(file, index, compactFiles.length); - } - } - - /** - * Compiles a single file with progress reporting and error handling. - * Private method used internally by the main compile() method. - * - * @param file - Relative path to the .compact file - * @param index - Current file index (0-based) for progress tracking - * @param total - Total number of files being compiled - * @throws {CompilationError} If compilation fails - * @private - */ - private async compileFile( - file: string, - index: number, - total: number, - ): Promise { - const step = `[${index + 1}/${total}]`; - const spinner = ora( - chalk.blue(`[COMPILE] ${step} Compiling ${file}`), - ).start(); - - try { - const result = await this.compilerService.compileFile( - file, - this.options.flags, - this.options.version, - ); - - spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); - // Filter out compactc version output from compact compile - const filteredOutput = result.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(result.stderr, chalk.yellow); - } catch (error) { - spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); - - if ( - error instanceof CompilationError && - isPromisifiedChildProcessError(error) - ) { - const execError = error; - // Filter out compactc version output from compact compile - const filteredOutput = execError.stdout.split('\n').slice(1).join('\n'); - - if (filteredOutput) { - UIService.printOutput(filteredOutput, chalk.cyan); - } - UIService.printOutput(execError.stderr, chalk.red); - } - - throw error; - } - } - - /** - * For testing - returns the resolved options object - */ - get testOptions(): ResolvedCompilerOptions { - return this.options; - } -} diff --git a/packages/cli/src/runBuilder.ts b/packages/cli/src/runBuilder.ts index 09e589a..3719973 100644 --- a/packages/cli/src/runBuilder.ts +++ b/packages/cli/src/runBuilder.ts @@ -1,35 +1,32 @@ #!/usr/bin/env node +import { CompactBuilder } from '@openzeppelin/compact-builder'; import chalk from 'chalk'; import ora from 'ora'; -import { CompactBuilder } from './Builder.ts'; /** * Executes the Compact builder CLI. * Builds projects using the `CompactBuilder` class with provided options, including compilation and additional steps. * - * Supports compiler options: - * - `--dir ` - Compile specific subdirectory - * - `--src ` - Source directory (default: src) - * - `--out ` - Output directory (default: artifacts) - * - `--hierarchical` - Preserve directory structure in output - * - `+` - Use specific toolchain version + * Compiler options (forwarded to `compact-compiler`): + * - `--dir ` - Compile specific subdirectory within srcDir + * - `--src ` - Source directory (default: src) + * - `--out ` - Output directory for artifacts (default: artifacts) + * - `--hierarchical` - Preserve source directory structure in BOTH the + * compiler artifacts output AND the builder's + * .compact copy into dist/ (default off: flat in both) + * - `--exclude ` - Skip .compact files matching pattern, in BOTH the + * compiler's file discovery AND the builder's + * .compact copy (repeatable). When unset, the builder + * falls back to ['Mock*', '*.mock.compact']; the + * compiler defaults to no excludes. + * - `+` - Use specific toolchain version * - * @example - * ```bash - * npx compact-builder - * npx compact-builder --src contracts --out build - * ``` - * Expected output: - * ``` - * ℹ [BUILD] Compact builder started - * ℹ [COMPILE] Found 1 .compact file(s) to compile - * ✔ [COMPILE] [1/1] Compiled Foo.compact - * Compactc version: 0.26.0 - * ✔ [BUILD] [1/3] Compiling TypeScript - * ✔ [BUILD] [2/3] Copying artifacts - * ✔ [BUILD] [3/3] Copying and cleaning .compact files - * ``` + * Builder-only options (control dist/ layout): + * - `--clean-dist` - rm -rf dist before building (default off) + * - `--copy ` - copy an extra file into dist/ for distribution (repeatable; e.g. package.json) + * + * See `packages/cli/README.md` for usage examples. */ async function runBuilder(): Promise { const spinner = ora(chalk.blue('[BUILD] Compact Builder started')).info(); diff --git a/packages/cli/src/runCompiler.ts b/packages/cli/src/runCompiler.ts index fda1a77..83e4622 100644 --- a/packages/cli/src/runCompiler.ts +++ b/packages/cli/src/runCompiler.ts @@ -1,12 +1,12 @@ #!/usr/bin/env node -import chalk from 'chalk'; -import ora, { type Ora } from 'ora'; -import { CompactCompiler } from './Compiler.ts'; import { + CompactCompiler, type CompilationError, isPromisifiedChildProcessError, -} from './types/errors.ts'; +} from '@openzeppelin/compact-builder'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; /** * Executes the Compact compiler CLI with improved error handling and user feedback. @@ -40,7 +40,7 @@ import { * * @example Version specification * ```bash - * npx compact-compiler --dir security --skip-zk +0.26.0 + * npx compact-compiler --dir security --skip-zk + * ``` */ async function runCompiler(): Promise { @@ -130,12 +130,17 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // Arg parsing + // Arg parsing — recognize all parser-emitted "flag requires a value" errors, + // not just --dir, so users get usage help for any malformed invocation. const errorMessage = error instanceof Error ? error.message : String(error); - if (errorMessage.includes('--dir flag requires a directory name')) { - spinner.fail( - chalk.red('[COMPILE] Error: --dir flag requires a directory name'), - ); + const parserErrors = [ + '--dir flag requires a directory name', + '--src flag requires a directory path', + '--out flag requires a directory path', + '--exclude flag requires a pattern', + ]; + if (parserErrors.some((msg) => errorMessage.includes(msg))) { + spinner.fail(chalk.red(`[COMPILE] Error: ${errorMessage}`)); showUsageHelp(); return; } @@ -185,13 +190,16 @@ function showUsageHelp(): void { ' --hierarchical Preserve source directory structure in artifacts output', ), ); + console.log( + chalk.yellow( + ' --exclude Skip .compact files matching the glob (repeatable)', + ), + ); console.log( chalk.yellow(' --skip-zk Skip zero-knowledge proof generation'), ); console.log( - chalk.yellow( - ' + Use specific toolchain version (e.g., +0.26.0)', - ), + chalk.yellow(' + Pin the Compact toolchain version'), ); console.log(chalk.yellow('\nArtifact Output Structure:')); console.log(chalk.yellow(' Default (flattened): //')); @@ -231,7 +239,7 @@ function showUsageHelp(): void { ); console.log( chalk.yellow( - ' compact-compiler --skip-zk +0.26.0 # Use specific version', + ' compact-compiler --skip-zk + # Pin toolchain version', ), ); console.log(chalk.yellow('\nTurbo integration:')); diff --git a/packages/cli/test/runCompiler.test.ts b/packages/cli/test/runCompiler.test.ts index 58f064a..d07e5d1 100644 --- a/packages/cli/test/runCompiler.test.ts +++ b/packages/cli/test/runCompiler.test.ts @@ -1,25 +1,23 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CompactCompiler } from '../src/Compiler.js'; import { CompactCliNotFoundError, + CompactCompiler, CompilationError, DirectoryNotFoundError, isPromisifiedChildProcessError, type PromisifiedChildProcessError, -} from '../src/types/errors.js'; - -// Mock CompactCompiler -vi.mock('../src/Compiler.js', () => ({ - CompactCompiler: { - fromArgs: vi.fn(), - }, -})); +} from '@openzeppelin/compact-builder'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -// Mock error utilities -vi.mock('../src/types/errors.js', async () => { - const actual = await vi.importActual('../src/types/errors.js'); +// Mock the library so we can drive the CLI in isolation. +vi.mock('@openzeppelin/compact-builder', async () => { + const actual = await vi.importActual< + typeof import('@openzeppelin/compact-builder') + >('@openzeppelin/compact-builder'); return { ...actual, + CompactCompiler: { + fromArgs: vi.fn(), + }, isPromisifiedChildProcessError: vi.fn(), }; }); @@ -334,11 +332,14 @@ describe('runCompiler CLI', () => { expect(mockConsoleLog).toHaveBeenCalledWith( ' --hierarchical Preserve source directory structure in artifacts output', ); + expect(mockConsoleLog).toHaveBeenCalledWith( + ' --exclude Skip .compact files matching the glob (repeatable)', + ); expect(mockConsoleLog).toHaveBeenCalledWith( ' --skip-zk Skip zero-knowledge proof generation', ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' + Use specific toolchain version (e.g., +0.26.0)', + ' + Pin the Compact toolchain version', ); expect(mockConsoleLog).toHaveBeenCalledWith( '\nArtifact Output Structure:', @@ -369,7 +370,7 @@ describe('runCompiler CLI', () => { ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --skip-zk +0.26.0 # Use specific version', + ' compact-compiler --skip-zk + # Pin toolchain version', ); expect(mockConsoleLog).toHaveBeenCalledWith('\nTurbo integration:'); expect(mockConsoleLog).toHaveBeenCalledWith( diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 75c9772..f4bcef1 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -3,6 +3,7 @@ "compilerOptions": { "outDir": "dist", "rootDir": "src", + "types": ["node"], "declaration": true, "skipLibCheck": true, "sourceMap": true, diff --git a/packages/simulator/README.md b/packages/simulator/README.md index da41f19..c846d79 100644 --- a/packages/simulator/README.md +++ b/packages/simulator/README.md @@ -13,7 +13,7 @@ allowing you to simulate contract behavior locally without blockchain deployment ## Quick Start ```typescript -import { createSimulator } from '@openzeppelin-compact/contracts-simulator'; +import { createSimulator } from '@openzeppelin/compact-simulator'; import { Contract, ledger } from './artifacts/MyContract/contract/index.js'; // 1. Define your contract arguments type @@ -44,7 +44,7 @@ const sim = new MySimulator([ownerAddress, 100n], { coinPK: deployerPK }); The base simulator acts as a configuration class that the actual simulator will extend: ```typescript -import { createSimulator } from '@openzeppelin-compact/contracts-simulator'; +import { createSimulator } from '@openzeppelin/compact-simulator'; import { Contract as MyContract, ledger } from './artifacts/MyContract/contract/index.js'; import { MyContractWitnesses, MyContractPrivateState } from './MyContractWitnesses.js'; diff --git a/packages/simulator/package.json b/packages/simulator/package.json index ee59c16..e73b723 100644 --- a/packages/simulator/package.json +++ b/packages/simulator/package.json @@ -1,5 +1,5 @@ { - "name": "@openzeppelin/compact-tools-simulator", + "name": "@openzeppelin/compact-simulator", "description": "Simulator for testing Compact smart contracts", "version": "0.0.1", "keywords": [ @@ -8,7 +8,7 @@ "simulator", "testing" ], - "author": "", + "author": "OpenZeppelin Community ", "license": "MIT", "type": "module", "main": "./dist/index.js", @@ -20,6 +20,11 @@ "require": "./dist/index.js" } }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], "engines": { "node": ">=22" }, diff --git a/turbo.json b/turbo.json index 74b3c39..08dd369 100644 --- a/turbo.json +++ b/turbo.json @@ -26,8 +26,9 @@ "outputs": ["dist/**"] }, "types": { - "dependsOn": [], - "outputs": ["dist/**"] + "dependsOn": ["^build"], + "inputs": ["src/**/*.ts", "tsconfig.json"], + "outputs": [] }, "lint": {}, "lint:ci": {}, diff --git a/yarn.lock b/yarn.lock index 99a2cf5..9352729 100644 --- a/yarn.lock +++ b/yarn.lock @@ -381,46 +381,49 @@ __metadata: languageName: node linkType: hard -"@openzeppelin/compact-tools-cli@workspace:packages/cli": +"@openzeppelin/compact-builder@workspace:^, @openzeppelin/compact-builder@workspace:packages/builder": version: 0.0.0-use.local - resolution: "@openzeppelin/compact-tools-cli@workspace:packages/cli" + resolution: "@openzeppelin/compact-builder@workspace:packages/builder" dependencies: "@tsconfig/node24": "npm:^24.0.3" "@types/node": "npm:24.10.1" + "@types/shell-quote": "npm:^1.7.5" chalk: "npm:^5.6.2" log-symbols: "npm:^7.0.0" ora: "npm:^9.0.0" + shell-quote: "npm:^1.8.3" typescript: "npm:^5.9.3" vitest: "npm:^4.0.15" - bin: - compact-builder: dist/runBuilder.js - compact-compiler: dist/runCompiler.js languageName: unknown linkType: soft -"@openzeppelin/compact-tools-simulator@workspace:packages/simulator": +"@openzeppelin/compact-cli@workspace:packages/cli": version: 0.0.0-use.local - resolution: "@openzeppelin/compact-tools-simulator@workspace:packages/simulator" + resolution: "@openzeppelin/compact-cli@workspace:packages/cli" dependencies: - "@midnight-ntwrk/compact-runtime": "npm:0.14.0" - "@midnight-ntwrk/ledger-v7": "npm:^7.0.0" + "@openzeppelin/compact-builder": "workspace:^" "@tsconfig/node24": "npm:^24.0.3" "@types/node": "npm:24.10.1" - fast-check: "npm:^4.5.2" - typescript: "npm:^5.8.2" + chalk: "npm:^5.6.2" + ora: "npm:^9.0.0" + typescript: "npm:^5.9.3" vitest: "npm:^4.0.15" + bin: + compact-builder: dist/runBuilder.js + compact-compiler: dist/runCompiler.js languageName: unknown linkType: soft -"@openzeppelin/compact-tools@workspace:.": +"@openzeppelin/compact-simulator@workspace:packages/simulator": version: 0.0.0-use.local - resolution: "@openzeppelin/compact-tools@workspace:." + resolution: "@openzeppelin/compact-simulator@workspace:packages/simulator" dependencies: - "@biomejs/biome": "npm:2.3.8" + "@midnight-ntwrk/compact-runtime": "npm:0.14.0" + "@midnight-ntwrk/ledger-v7": "npm:^7.0.0" + "@tsconfig/node24": "npm:^24.0.3" "@types/node": "npm:24.10.1" - ts-node: "npm:^10.9.2" - turbo: "npm:^2.6.1" - typescript: "npm:^5.9.3" + fast-check: "npm:^4.5.2" + typescript: "npm:^5.8.2" vitest: "npm:^4.0.15" languageName: unknown linkType: soft @@ -689,6 +692,13 @@ __metadata: languageName: node linkType: hard +"@types/shell-quote@npm:^1.7.5": + version: 1.7.5 + resolution: "@types/shell-quote@npm:1.7.5" + checksum: 10/32b4d697c7d23dbadf40713692c47f1595f083a3b3deea76cb18e30a05d197aa9205d2b87f6d92edb60cda120b51e35d32bda96ed9b0a7e32921eed2deb4559e + languageName: node + linkType: hard + "@vitest/expect@npm:4.0.15": version: 4.0.15 resolution: "@vitest/expect@npm:4.0.15" @@ -934,6 +944,19 @@ __metadata: languageName: node linkType: hard +"compact-tools-monorepo@workspace:.": + version: 0.0.0-use.local + resolution: "compact-tools-monorepo@workspace:." + dependencies: + "@biomejs/biome": "npm:2.3.8" + "@types/node": "npm:24.10.1" + ts-node: "npm:^10.9.2" + turbo: "npm:^2.6.1" + typescript: "npm:^5.9.3" + vitest: "npm:^4.0.15" + languageName: unknown + linkType: soft + "create-require@npm:^1.1.0": version: 1.1.1 resolution: "create-require@npm:1.1.1" @@ -1785,6 +1808,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.3": + version: 1.8.3 + resolution: "shell-quote@npm:1.8.3" + checksum: 10/5473e354637c2bd698911224129c9a8961697486cff1fb221f234d71c153fc377674029b0223d1d3c953a68d451d79366abfe53d1a0b46ee1f28eb9ade928f4c + languageName: node + linkType: hard + "siginfo@npm:^2.0.0": version: 2.0.0 resolution: "siginfo@npm:2.0.0"