From 7ea77ed546d396f21635218f08e9b38dc2a20142 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 13:40:59 -0300 Subject: [PATCH 01/44] bump language and compact version --- .github/ISSUE_TEMPLATE/01_bug_report.yml | 2 +- .github/workflows/test.yml | 4 +-- README.md | 22 ++++++++-------- compact/src/Compiler.ts | 26 +++++++++---------- compact/src/runBuilder.ts | 2 +- compact/src/runCompiler.ts | 6 ++--- contracts/src/access/AccessControl.compact | 2 +- contracts/src/access/Ownable.compact | 2 +- contracts/src/access/ZOwnablePK.compact | 2 +- .../test/mocks/MockAccessControl.compact | 2 +- .../src/access/test/mocks/MockOwnable.compact | 2 +- .../access/test/mocks/MockZOwnablePK.compact | 2 +- contracts/src/archive/ShieldedToken.compact | 2 +- .../test/mocks/MockShieldedToken.compact | 2 +- contracts/src/security/Initializable.compact | 2 +- contracts/src/security/Pausable.compact | 2 +- .../test/mocks/MockInitializable.compact | 2 +- .../security/test/mocks/MockPausable.compact | 2 +- contracts/src/token/FungibleToken.compact | 2 +- contracts/src/token/MultiToken.compact | 2 +- contracts/src/token/NonFungibleToken.compact | 2 +- .../test/mocks/MockFungibleToken.compact | 2 +- .../token/test/mocks/MockMultiToken.compact | 2 +- .../test/mocks/MockNonFungibleToken.compact | 2 +- contracts/src/utils/Utils.compact | 2 +- .../src/utils/test/mocks/MockUtils.compact | 2 +- docs/modules/ROOT/pages/access.adoc | 12 ++++----- docs/modules/ROOT/pages/extensibility.adoc | 2 +- docs/modules/ROOT/pages/fungibleToken.adoc | 4 +-- docs/modules/ROOT/pages/index.adoc | 20 +++++++------- docs/modules/ROOT/pages/multitoken.adoc | 4 +-- docs/modules/ROOT/pages/nonFungibleToken.adoc | 4 +-- docs/modules/ROOT/pages/security.adoc | 4 +-- docs/modules/ROOT/pages/utils.adoc | 2 +- 34 files changed, 77 insertions(+), 77 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/01_bug_report.yml b/.github/ISSUE_TEMPLATE/01_bug_report.yml index ff3d6a1b..fd3dd963 100644 --- a/.github/ISSUE_TEMPLATE/01_bug_report.yml +++ b/.github/ISSUE_TEMPLATE/01_bug_report.yml @@ -93,7 +93,7 @@ body: label: Version description: What version of Compact are you running? options: - - 0.24.0 (Default) + - 0.25.0 (Default) default: 0 validations: required: true diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7a2d626..225d6014 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,8 +8,8 @@ on: env: TURBO_TELEMETRY_DISABLED: 1 - COMPILER_VERSION: "0.24.0" - LANGUAGE_VERSION: "0.16.0" + COMPILER_VERSION: "0.25.0" + LANGUAGE_VERSION: "0.17.0" jobs: run-suite: diff --git a/README.md b/README.md index f411b50c..6c669398 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.24.0-1abc9c.svg)](https://docs.midnight.network/relnotes/compact) +[![Generic badge](https://img.shields.io/badge/Compact%20Compiler-0.25.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) # OpenZeppelin Contracts for Compact @@ -20,8 +20,8 @@ Follow Midnight's [Compact Developer Tools installation guide](https://docs.midn ```bash $ compact compile --version -Compactc version: 0.24.0 -0.24.0 +Compactc version: 0.25.0 +0.25.0 ``` ## Set up the project @@ -56,29 +56,29 @@ $ turbo compact (...) ✔ [COMPILE] [1/2] Compiled FungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 +@openzeppelin-compact/fungible-token:compact: Compactc version: 0.25.0 @openzeppelin-compact/fungible-token:compact: ✔ [COMPILE] [1/6] Compiled Initializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [2/6] Compiled Pausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [3/6] Compiled Utils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [4/6] Compiled test/mocks/MockInitializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: Compiling 3 circuits: ✔ [COMPILE] [5/6] Compiled test/mocks/MockPausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: Compiling 5 circuits: ✔ [COMPILE] [6/6] Compiled test/mocks/MockUtils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [2/2] Compiled test/mocks/MockFungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 +@openzeppelin-compact/fungible-token:compact: Compactc version: 0.25.0 @openzeppelin-compact/fungible-token:compact: Compiling 15 circuits: diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index eaee0178..6f9b5f1a 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -39,7 +39,7 @@ export type ExecFunction = ( * @example * ```typescript * const validator = new EnvironmentValidator(); - * await validator.validate('0.24.0'); + * await validator.validate('0.25.0'); * const version = await validator.getDevToolsVersion(); * ``` */ @@ -100,7 +100,7 @@ export class EnvironmentValidator { * @throws {Error} If the CLI is not available or command fails * @example * ```typescript - * const toolchainVersion = await validator.getToolchainVersion('0.24.0'); + * const toolchainVersion = await validator.getToolchainVersion('0.25.0'); * console.log(`Toolchain: ${toolchainVersion}`); * ``` */ @@ -122,7 +122,7 @@ export class EnvironmentValidator { * @example * ```typescript * try { - * await validator.validate('0.24.0'); + * await validator.validate('0.25.0'); * console.log('Environment validated successfully'); * } catch (error) { * if (error instanceof CompactCliNotFoundError) { @@ -215,7 +215,7 @@ export class FileDiscovery { * const result = await compiler.compileFile( * 'contracts/Token.compact', * '--skip-zk --verbose', - * '0.24.0' + * '0.25.0' * ); * console.log('Compilation output:', result.stdout); * ``` @@ -247,7 +247,7 @@ export class CompilerService { * const result = await compiler.compileFile( * 'security/AccessControl.compact', * '--skip-zk', - * '0.24.0' + * '0.25.0' * ); * console.log('Success:', result.stdout); * } catch (error) { @@ -292,7 +292,7 @@ export class CompilerService { * @class UIService * @example * ```typescript - * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.24.0', 'security'); + * UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.25.0', 'security'); * UIService.printOutput('Compilation successful', chalk.green); * ``` */ @@ -329,9 +329,9 @@ export const UIService = { * ```typescript * UIService.displayEnvInfo( * 'compact 0.1.0', - * 'Compactc version: 0.24.0', + * 'Compactc version: 0.25.0', * 'security', - * '0.24.0' + * '0.25.0' * ); * ``` */ @@ -415,7 +415,7 @@ export const UIService = { * @example * ```typescript * // Basic usage - * const compiler = new CompactCompiler('--skip-zk', 'security', '0.24.0'); + * const compiler = new CompactCompiler('--skip-zk', 'security', '0.25.0'); * await compiler.compile(); * * // Factory method usage @@ -448,7 +448,7 @@ export class CompactCompiler { * * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') * @param targetDir - Optional subdirectory within src/ to compile (e.g., 'security', 'token') - * @param version - Optional toolchain version to use (e.g., '0.24.0') + * @param version - Optional toolchain version to use (e.g., '0.25.0') * @param execFn - Optional custom exec function for dependency injection * @example * ```typescript @@ -459,7 +459,7 @@ export class CompactCompiler { * const compiler = new CompactCompiler('', 'security'); * * // Compile with specific version - * const compiler = new CompactCompiler('--skip-zk', undefined, '0.24.0'); + * const compiler = new CompactCompiler('--skip-zk', undefined, '0.25.0'); * * // For testing with custom exec function * const mockExec = vi.fn(); @@ -496,11 +496,11 @@ export class CompactCompiler { * @throws {Error} If --dir flag is provided without a directory name * @example * ```typescript - * // Parse command line: compact-compiler --dir security --skip-zk +0.24.0 + * // Parse command line: compact-compiler --dir security --skip-zk +0.25.0 * const compiler = CompactCompiler.fromArgs([ * '--dir', 'security', * '--skip-zk', - * '+0.24.0' + * '+0.25.0' * ]); * * // With environment variable diff --git a/compact/src/runBuilder.ts b/compact/src/runBuilder.ts index ebcf5a8f..ac2659b7 100644 --- a/compact/src/runBuilder.ts +++ b/compact/src/runBuilder.ts @@ -19,7 +19,7 @@ import { CompactBuilder } from './Builder.js'; * ℹ [COMPILE] COMPACTC_PATH: /path/to/compactc/compactc * ℹ [COMPILE] Found 1 .compact file(s) to compile * ✔ [COMPILE] [1/1] Compiled Foo.compact - * Compactc version: 0.24.0 + * Compactc version: 0.25.0 * ✔ [BUILD] [1/3] Compiling TypeScript * ✔ [BUILD] [2/3] Copying artifacts * ✔ [BUILD] [3/3] Copying and cleaning .compact files diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index ce3cdc6c..93cbe1b2 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -40,7 +40,7 @@ import { * * @example Version specification * ```bash - * npx compact-compiler --dir security --skip-zk +0.24.0 + * npx compact-compiler --dir security --skip-zk +0.25.0 * ``` */ async function runCompiler(): Promise { @@ -179,7 +179,7 @@ function showUsageHelp(): void { ); console.log( chalk.yellow( - ' + Use specific toolchain version (e.g., +0.24.0)', + ' + Use specific toolchain version (e.g., +0.25.0)', ), ); console.log(chalk.yellow('\nExamples:')); @@ -205,7 +205,7 @@ function showUsageHelp(): void { ); console.log( chalk.yellow( - ' compact-compiler --skip-zk +0.24.0 # Use specific version', + ' compact-compiler --skip-zk +0.25.0 # Use specific version', ), ); console.log(chalk.yellow('\nTurbo integration:')); diff --git a/contracts/src/access/AccessControl.compact b/contracts/src/access/AccessControl.compact index 31717e13..d6fe9267 100644 --- a/contracts/src/access/AccessControl.compact +++ b/contracts/src/access/AccessControl.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (access/AccessControl.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module AccessControl diff --git a/contracts/src/access/Ownable.compact b/contracts/src/access/Ownable.compact index e2ee4848..bc7fa447 100644 --- a/contracts/src/access/Ownable.compact +++ b/contracts/src/access/Ownable.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (access/Ownable.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module Ownable diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 37e4616f..18918b99 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module ZOwnablePK diff --git a/contracts/src/access/test/mocks/MockAccessControl.compact b/contracts/src/access/test/mocks/MockAccessControl.compact index 0cc10a1c..48a6ea7a 100644 --- a/contracts/src/access/test/mocks/MockAccessControl.compact +++ b/contracts/src/access/test/mocks/MockAccessControl.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; diff --git a/contracts/src/access/test/mocks/MockOwnable.compact b/contracts/src/access/test/mocks/MockOwnable.compact index ac7ca266..051ea24d 100644 --- a/contracts/src/access/test/mocks/MockOwnable.compact +++ b/contracts/src/access/test/mocks/MockOwnable.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact index d769f30e..51de1633 100644 --- a/contracts/src/access/test/mocks/MockZOwnablePK.compact +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "../../ZOwnablePK" prefix ZOwnablePK_; diff --git a/contracts/src/archive/ShieldedToken.compact b/contracts/src/archive/ShieldedToken.compact index 4497a556..5ceb55ea 100644 --- a/contracts/src/archive/ShieldedToken.compact +++ b/contracts/src/archive/ShieldedToken.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (archive/ShieldedToken.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module ShieldedToken (archived until further notice, DO NOT USE IN PRODUCTION) diff --git a/contracts/src/archive/test/mocks/MockShieldedToken.compact b/contracts/src/archive/test/mocks/MockShieldedToken.compact index 766ded21..2f2ee0f0 100644 --- a/contracts/src/archive/test/mocks/MockShieldedToken.compact +++ b/contracts/src/archive/test/mocks/MockShieldedToken.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "../../ShieldedToken" prefix ShieldedToken_; diff --git a/contracts/src/security/Initializable.compact b/contracts/src/security/Initializable.compact index 30422377..714b4fee 100644 --- a/contracts/src/security/Initializable.compact +++ b/contracts/src/security/Initializable.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (security/Initializable.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module Initializable diff --git a/contracts/src/security/Pausable.compact b/contracts/src/security/Pausable.compact index 899b33af..cd06cf31 100644 --- a/contracts/src/security/Pausable.compact +++ b/contracts/src/security/Pausable.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (security/Pausable.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module Pausable diff --git a/contracts/src/security/test/mocks/MockInitializable.compact b/contracts/src/security/test/mocks/MockInitializable.compact index d4c9bdef..6ecca8e4 100644 --- a/contracts/src/security/test/mocks/MockInitializable.compact +++ b/contracts/src/security/test/mocks/MockInitializable.compact @@ -1,4 +1,4 @@ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "../../Initializable" prefix Initializable_; diff --git a/contracts/src/security/test/mocks/MockPausable.compact b/contracts/src/security/test/mocks/MockPausable.compact index a4695485..cf7a4197 100644 --- a/contracts/src/security/test/mocks/MockPausable.compact +++ b/contracts/src/security/test/mocks/MockPausable.compact @@ -1,4 +1,4 @@ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "../../Pausable" prefix Pausable_; diff --git a/contracts/src/token/FungibleToken.compact b/contracts/src/token/FungibleToken.compact index 5b11e7c1..aed8d7aa 100644 --- a/contracts/src/token/FungibleToken.compact +++ b/contracts/src/token/FungibleToken.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (token/FungibleToken.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module FungibleToken diff --git a/contracts/src/token/MultiToken.compact b/contracts/src/token/MultiToken.compact index d1080ce4..69feff98 100644 --- a/contracts/src/token/MultiToken.compact +++ b/contracts/src/token/MultiToken.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (token/MultiToken.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module MultiToken diff --git a/contracts/src/token/NonFungibleToken.compact b/contracts/src/token/NonFungibleToken.compact index f73e8258..f0f24a2d 100644 --- a/contracts/src/token/NonFungibleToken.compact +++ b/contracts/src/token/NonFungibleToken.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (token/NonFungibleToken.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module NonFungibleToken diff --git a/contracts/src/token/test/mocks/MockFungibleToken.compact b/contracts/src/token/test/mocks/MockFungibleToken.compact index ddd30abd..f85d6d76 100644 --- a/contracts/src/token/test/mocks/MockFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockFungibleToken.compact @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; diff --git a/contracts/src/token/test/mocks/MockMultiToken.compact b/contracts/src/token/test/mocks/MockMultiToken.compact index 00cb0d35..2549559c 100644 --- a/contracts/src/token/test/mocks/MockMultiToken.compact +++ b/contracts/src/token/test/mocks/MockMultiToken.compact @@ -1,4 +1,4 @@ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "../../MultiToken" prefix MultiToken_; diff --git a/contracts/src/token/test/mocks/MockNonFungibleToken.compact b/contracts/src/token/test/mocks/MockNonFungibleToken.compact index a883800d..4b6064cb 100644 --- a/contracts/src/token/test/mocks/MockNonFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockNonFungibleToken.compact @@ -1,4 +1,4 @@ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; diff --git a/contracts/src/utils/Utils.compact b/contracts/src/utils/Utils.compact index a00cd220..d58bf66d 100644 --- a/contracts/src/utils/Utils.compact +++ b/contracts/src/utils/Utils.compact @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT // OpenZeppelin Compact Contracts v0.0.1 (utils/Utils.compact) -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; /** * @module Utils. diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index 54fd45f3..3f3ee9d7 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -1,4 +1,4 @@ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; diff --git a/docs/modules/ROOT/pages/access.adoc b/docs/modules/ROOT/pages/access.adoc index 7978c1fa..98a4f865 100644 --- a/docs/modules/ROOT/pages/access.adoc +++ b/docs/modules/ROOT/pages/access.adoc @@ -26,7 +26,7 @@ It's recommended to prefix the module with `Ownable_` to avoid circuit signature ```ts // MyOwnableContract.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable" @@ -65,7 +65,7 @@ Here's a complete contract showcasing how to integrate the Ownable module and pr ```ts // SimpleOwnable.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/Ownable" @@ -308,7 +308,7 @@ It’s recommended to prefix the module with `ZOwnablePK_` to avoid circuit sign ```typescript // MyZOwnablePKContract.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/ZOwnablePK" @@ -405,7 +405,7 @@ Here’s a simple example of using `AccessControl` with xref:fungibleToken.adoc[ ```ts // AccessControlMinter.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" @@ -447,7 +447,7 @@ Let’s augment our FungibleToken example by also defining a 'burner' role, whic ```ts // AccessControlMinter.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" @@ -513,7 +513,7 @@ Let’s take a look at the FungibleToken example, this time taking advantage of ```ts // AccessControlMinter.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/access/AccessControl" diff --git a/docs/modules/ROOT/pages/extensibility.adoc b/docs/modules/ROOT/pages/extensibility.adoc index 3f3faaab..e39e0914 100644 --- a/docs/modules/ROOT/pages/extensibility.adoc +++ b/docs/modules/ROOT/pages/extensibility.adoc @@ -42,7 +42,7 @@ This pattern balances modularity with local control, avoids tight coupling, and ```ts /** FungibleTokenMintablePausableOwnableContract */ -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" diff --git a/docs/modules/ROOT/pages/fungibleToken.adoc b/docs/modules/ROOT/pages/fungibleToken.adoc index b56c42b2..de1b31cc 100644 --- a/docs/modules/ROOT/pages/fungibleToken.adoc +++ b/docs/modules/ROOT/pages/fungibleToken.adoc @@ -44,7 +44,7 @@ Import the FungibleToken module into the implementing contract. It's recommended to prefix the module with `FungibleToken_` to avoid circuit signature clashes. ```typescript -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" @@ -85,7 +85,7 @@ The following example is a simple token contract with a fixed supply that's mint ```typescript // FungibleTokenFixedSupply.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/FungibleToken" diff --git a/docs/modules/ROOT/pages/index.adoc b/docs/modules/ROOT/pages/index.adoc index 05a2a246..59a7b789 100644 --- a/docs/modules/ROOT/pages/index.adoc +++ b/docs/modules/ROOT/pages/index.adoc @@ -20,8 +20,8 @@ Follow Midnight's {compact-dev-tools} installation guide and confirm that `compa ```bash $ compact compile --version -Compactc version: 0.24.0 -0.24.0 +Compactc version: 0.25.0 +0.25.0 ``` === Set up the project @@ -49,29 +49,29 @@ $ turbo compact (...) ✔ [COMPILE] [1/2] Compiled FungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 +@openzeppelin-compact/fungible-token:compact: Compactc version: 0.25.0 @openzeppelin-compact/fungible-token:compact: ✔ [COMPILE] [1/6] Compiled Initializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [2/6] Compiled Pausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [3/6] Compiled Utils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [4/6] Compiled test/mocks/MockInitializable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: Compiling 3 circuits: ✔ [COMPILE] [5/6] Compiled test/mocks/MockPausable.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: Compiling 5 circuits: ✔ [COMPILE] [6/6] Compiled test/mocks/MockUtils.compact -@openzeppelin-compact/utils:compact: Compactc version: 0.24.0 +@openzeppelin-compact/utils:compact: Compactc version: 0.25.0 @openzeppelin-compact/utils:compact: ✔ [COMPILE] [2/2] Compiled test/mocks/MockFungibleToken.compact -@openzeppelin-compact/fungible-token:compact: Compactc version: 0.24.0 +@openzeppelin-compact/fungible-token:compact: Compactc version: 0.25.0 @openzeppelin-compact/fungible-token:compact: Compiling 15 circuits: diff --git a/docs/modules/ROOT/pages/multitoken.adoc b/docs/modules/ROOT/pages/multitoken.adoc index 49420db8..84957ac4 100644 --- a/docs/modules/ROOT/pages/multitoken.adoc +++ b/docs/modules/ROOT/pages/multitoken.adoc @@ -59,7 +59,7 @@ Import the MultiToken module into the implementing contract. It's recommended to prefix the module with `MultiToken_` to avoid circuit signature clashes. ```typescript -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken" @@ -96,7 +96,7 @@ The following example is a simple multi-token contract that creates both a fixed ```typescript // MultiTokenTwoTokenTypes.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/MultiToken" diff --git a/docs/modules/ROOT/pages/nonFungibleToken.adoc b/docs/modules/ROOT/pages/nonFungibleToken.adoc index c4286402..792b1098 100644 --- a/docs/modules/ROOT/pages/nonFungibleToken.adoc +++ b/docs/modules/ROOT/pages/nonFungibleToken.adoc @@ -49,7 +49,7 @@ Import the NonFungibleToken module into the implementing contract. It's recommended to prefix the module with `NonFungibleToken_` to avoid circuit signature clashes. ```typescript -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken" @@ -85,7 +85,7 @@ The following example is a simple non-fungible token contract that mints an NFT ```typescript // SimpleNonFungibleToken.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import "./node_modules/@openzeppelin-compact/contracts/src/token/NonFungibleToken" diff --git a/docs/modules/ROOT/pages/security.adoc b/docs/modules/ROOT/pages/security.adoc index 038e7bc2..568cd4cf 100644 --- a/docs/modules/ROOT/pages/security.adoc +++ b/docs/modules/ROOT/pages/security.adoc @@ -17,7 +17,7 @@ Many modules also use the initializable pattern which ensures that implementing ```typescript // CustomContractStateSetup.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import './node_modules/@openzeppelin-compact/contracts/src/security/Initializable'; @@ -63,7 +63,7 @@ For example (using the {ownable} module for access control): ```typescript // OwnablePausable.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import './node_modules/@openzeppelin-compact/contracts/src/security/Initializable' prefix Initializable_; diff --git a/docs/modules/ROOT/pages/utils.adoc b/docs/modules/ROOT/pages/utils.adoc index 5884664d..1a63c8dd 100644 --- a/docs/modules/ROOT/pages/utils.adoc +++ b/docs/modules/ROOT/pages/utils.adoc @@ -11,7 +11,7 @@ The Utils module provides miscellaneous circuits and common utilities for Compac ```typescript // UtilsExample.compact -pragma language_version >= 0.16.0; +pragma language_version >= 0.17.0; import CompactStandardLibrary; import './node_modules/@openzeppelin-compact/contracts/src/utils/Utils'; From d055cf0a88610e52bdbc53707cbe8621fef3ada9 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 13:42:31 -0300 Subject: [PATCH 02/44] use multi const declaration --- contracts/src/token/MultiToken.compact | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/contracts/src/token/MultiToken.compact b/contracts/src/token/MultiToken.compact index 69feff98..88b5bdbd 100644 --- a/contracts/src/token/MultiToken.compact +++ b/contracts/src/token/MultiToken.compact @@ -318,9 +318,7 @@ module MultiToken { _balances.insert(disclose(id), default, Uint<128>>>); _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); } else { - const toBalance = balanceOf(to, id); - // TODO: Replace with Max_Uint128() - const MAX_UINT128 = 340282366920938463463374607431768211455; + const toBalance = balanceOf(to, id), MAX_UINT128 = 340282366920938463463374607431768211455; assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); } From 78b412d0043fff8eece97b4575bf850cfdd20b3f Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 13:47:43 -0300 Subject: [PATCH 03/44] update tests --- compact/test/runCompiler.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index 11244f2d..b6d58b34 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -309,7 +309,7 @@ describe('runCompiler CLI', () => { ' --skip-zk Skip zero-knowledge proof generation', ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' + Use specific toolchain version (e.g., +0.24.0)', + ' + Use specific toolchain version (e.g., +0.25.0)', ); expect(mockConsoleLog).toHaveBeenCalledWith('\nExamples:'); expect(mockConsoleLog).toHaveBeenCalledWith( @@ -325,7 +325,7 @@ describe('runCompiler CLI', () => { ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --skip-zk +0.24.0 # Use specific version', + ' compact-compiler --skip-zk +0.25.0 # Use specific version', ); expect(mockConsoleLog).toHaveBeenCalledWith('\nTurbo integration:'); expect(mockConsoleLog).toHaveBeenCalledWith( From f0405a0cce11ee1c1d002bc43f7c8e9e13d59c42 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 16:57:20 -0300 Subject: [PATCH 04/44] fix comments in build --- compact/src/Builder.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compact/src/Builder.ts b/compact/src/Builder.ts index b8f05197..f97909f1 100755 --- a/compact/src/Builder.ts +++ b/compact/src/Builder.ts @@ -29,9 +29,9 @@ const execAsync = promisify(exec); * ``` * ℹ [COMPILE] Found 2 .compact file(s) to compile * ✔ [COMPILE] [1/2] Compiled AccessControl.compact - * Compactc version: 0.24.0 + * Compactc version: 0.25.0 * ✔ [COMPILE] [2/2] Compiled MockAccessControl.compact - * Compactc version: 0.24.0 + * Compactc version: 0.25.0 * ✔ [BUILD] [1/3] Compiling TypeScript * ✔ [BUILD] [2/3] Copying artifacts * ✔ [BUILD] [3/3] Copying and cleaning .compact files @@ -41,7 +41,7 @@ const execAsync = promisify(exec); * ``` * ℹ [COMPILE] Found 2 .compact file(s) to compile * ✖ [COMPILE] [1/2] Failed AccessControl.compact - * Compactc version: 0.24.0 + * Compactc version: 0.25.0 * Error: Expected ';' at line 5 in AccessControl.compact * ``` * From d44dfd893c5dc8a6fcf74623e3a518bc5f527a35 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 21:02:22 -0300 Subject: [PATCH 05/44] add formatter --- compact/package.json | 3 +- compact/src/Formatter.ts | 499 ++++++++++++++++++++++++++++++++++++ compact/src/runFormatter.ts | 230 +++++++++++++++++ compact/src/types/errors.ts | 36 ++- 4 files changed, 766 insertions(+), 2 deletions(-) create mode 100644 compact/src/Formatter.ts create mode 100644 compact/src/runFormatter.ts diff --git a/compact/package.json b/compact/package.json index bba8cb66..c6e1a617 100644 --- a/compact/package.json +++ b/compact/package.json @@ -17,7 +17,8 @@ }, "bin": { "compact-builder": "dist/runBuilder.js", - "compact-compiler": "dist/runCompiler.js" + "compact-compiler": "dist/runCompiler.js", + "compact-formatter": "./dist/runFormatter.js" }, "scripts": { "build": "tsc -p .", diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts new file mode 100644 index 00000000..ed40485f --- /dev/null +++ b/compact/src/Formatter.ts @@ -0,0 +1,499 @@ +#!/usr/bin/env node + +import { exec as execCallback } from 'node:child_process'; +import { existsSync } from 'node:fs'; +import { readdir } from 'node:fs/promises'; +import { join, relative } from 'node:path'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora from 'ora'; +import { + CompactCliNotFoundError, + FormatterError, + FormatterNotAvailableError, + DirectoryNotFoundError, + isPromisifiedChildProcessError, +} from './types/errors.ts'; + +/** Source directory containing .compact files */ +const SRC_DIR: string = 'src'; + +/** + * Function type for executing shell commands. + * Allows dependency injection for testing and customization. + */ +export type ExecFunction = ( + command: string, +) => Promise<{ stdout: string; stderr: string }>; + +/** + * Service responsible for validating the Compact CLI environment for formatting. + * Checks CLI availability, formatter availability, and ensures the toolchain + * supports formatting operations. + * + * @class FormatterEnvironmentValidator + */ +export class FormatterEnvironmentValidator { + private execFn: ExecFunction; + + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Checks if the Compact CLI is available in the system PATH. + */ + async checkCompactAvailable(): Promise { + try { + await this.execFn('compact --version'); + return true; + } catch { + return false; + } + } + + /** + * Retrieves the version of the Compact developer tools. + */ + async getDevToolsVersion(): Promise { + const { stdout } = await this.execFn('compact --version'); + return stdout.trim(); + } + + /** + * Checks if the formatter is available (requires compiler 0.25.0+). + * @throws {FormatterNotAvailableError} If formatter is not available + */ + async checkFormatterAvailable(): Promise { + try { + await this.execFn('compact help format'); + } catch (error) { + if (isPromisifiedChildProcessError(error) && + error.stderr?.includes('formatter not available')) { + throw new FormatterNotAvailableError( + 'Formatter not available. Please update your Compact compiler with: compact update' + ); + } + throw error; + } + } + + /** + * Validates the entire Compact environment for formatting operations. + * @throws {CompactCliNotFoundError} If the Compact CLI is not available + * @throws {FormatterNotAvailableError} If formatter is not available + */ + async validate(): Promise<{ devToolsVersion: string }> { + const isAvailable = await this.checkCompactAvailable(); + if (!isAvailable) { + throw new CompactCliNotFoundError( + "'compact' CLI not found in PATH. Please install the Compact developer tools." + ); + } + + await this.checkFormatterAvailable(); + const devToolsVersion = await this.getDevToolsVersion(); + + return { devToolsVersion }; + } +} + +/** + * Service responsible for discovering .compact files for formatting operations. + * Reuses the same file discovery logic as the compiler. + * + * @class FormatterFileDiscovery + */ +export class FormatterFileDiscovery { + /** + * Recursively discovers all .compact files in a directory. + */ + 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(SRC_DIR, fullPath)]; + } + return []; + } catch (err) { + console.warn(`Error accessing ${fullPath}:`, err); + return []; + } + }); + + const results = await Promise.all(filePromises); + return results.flat(); + } catch (err) { + console.error(`Failed to read dir: ${dir}`, err); + return []; + } + } +} + +/** + * Service responsible for executing format operations using the Compact CLI. + * Handles different formatting modes: format, check, and specific file/directory targeting. + * + * @class FormatterService + */ +export class FormatterService { + private execFn: ExecFunction; + + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Formats files in-place in the specified directory or current directory. + * @param targetPath - Optional directory or file path to format + */ + async formatInPlace(targetPath?: string): Promise<{ stdout: string; stderr: string }> { + const pathArg = targetPath ? ` "${targetPath}"` : ''; + const command = `compact format${pathArg}`; + + try { + return await this.execFn(command); + } catch (error: unknown) { + let message: string; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } + + throw new FormatterError(`Failed to format: ${message}`, targetPath); + } + } + + /** + * Checks if files are properly formatted without modifying them. + * @param targetPath - Optional directory or file path to check + * @returns Promise with check results including any formatting differences + */ + async checkFormatting(targetPath?: string): Promise<{ stdout: string; stderr: string; isFormatted: boolean }> { + const pathArg = targetPath ? ` "${targetPath}"` : ''; + const command = `compact format --check${pathArg}`; + + try { + const result = await this.execFn(command); + return { ...result, isFormatted: true }; + } catch (error: unknown) { + if (isPromisifiedChildProcessError(error)) { + // Exit code 1 with formatting differences is expected behavior + if (error.code === 1 && error.stdout) { + return { + stdout: error.stdout, + stderr: error.stderr || '', + isFormatted: false + }; + } + } + + let message: string; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } + + throw new FormatterError(`Failed to check formatting: ${message}`, targetPath); + } + } + + /** + * Formats a list of specific files. + * @param files - Array of file paths to format + */ + async formatFiles(files: string[]): Promise<{ stdout: string; stderr: string }> { + if (files.length === 0) { + return { stdout: '', stderr: '' }; + } + + const fileArgs = files.map(file => `"${join(SRC_DIR, file)}"`).join(' '); + const command = `compact format ${fileArgs}`; + + try { + return await this.execFn(command); + } catch (error: unknown) { + let message: string; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } + + throw new FormatterError(`Failed to format files: ${message}`, files.join(', ')); + } + } +} + +/** + * Utility service for handling formatter UI output and formatting. + * Provides consistent styling and formatting for formatter messages and output. + * + * @class FormatterUIService + */ +export const FormatterUIService = { + /** + * Prints formatted output with consistent indentation and coloring. + */ + 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 for formatting operations. + */ + displayEnvInfo(devToolsVersion: string, targetDir?: string): void { + const spinner = ora(); + + if (targetDir) { + spinner.info(chalk.blue(`[FORMAT] TARGET_DIR: ${targetDir}`)); + } + + spinner.info( + chalk.blue(`[FORMAT] Compact developer tools: ${devToolsVersion}`) + ); + }, + + /** + * Displays formatting start message with file count and optional location. + */ + showFormattingStart(fileCount: number, mode: 'format' | 'check', targetDir?: string): void { + const searchLocation = targetDir ? ` in ${targetDir}/` : ''; + const action = mode === 'check' ? 'check formatting for' : 'format'; + const spinner = ora(); + spinner.info( + chalk.blue( + `[FORMAT] Found ${fileCount} .compact file(s) to ${action}${searchLocation}` + ) + ); + }, + + /** + * Displays a warning message when no .compact files are found. + */ + showNoFiles(targetDir?: string): void { + const searchLocation = targetDir ? `${targetDir}/` : ''; + const spinner = ora(); + spinner.warn( + chalk.yellow(`[FORMAT] No .compact files found in ${searchLocation}.`) + ); + }, + + /** + * Displays formatting check results. + */ + showCheckResults(isFormatted: boolean, differences?: string): void { + const spinner = ora(); + + if (isFormatted) { + spinner.succeed(chalk.green('[FORMAT] All files are properly formatted')); + } else { + spinner.fail(chalk.red('[FORMAT] Some files are not properly formatted')); + if (differences) { + console.log(chalk.yellow('\nFormatting differences:')); + this.printOutput(differences, chalk.white); + } + } + }, +}; + +/** + * Main formatter class that orchestrates the formatting process. + * Coordinates environment validation, file discovery, and formatting services + * to provide a complete .compact file formatting solution. + * + * @class CompactFormatter + */ +export class CompactFormatter { + /** Environment validation service */ + private readonly environmentValidator: FormatterEnvironmentValidator; + /** File discovery service */ + private readonly fileDiscovery: FormatterFileDiscovery; + /** Formatting execution service */ + private readonly formatterService: FormatterService; + + /** Whether to check formatting instead of formatting in-place */ + private readonly checkMode: boolean; + /** Optional target directory or files to limit formatting scope */ + private readonly targets: string[]; + + /** + * Creates a new CompactFormatter instance with specified configuration. + * + * @param checkMode - Whether to check formatting instead of formatting in-place + * @param targets - Optional array of directories or files to format + * @param execFn - Optional custom exec function for dependency injection + */ + constructor( + checkMode = false, + targets: string[] = [], + execFn?: ExecFunction, + ) { + this.checkMode = checkMode; + this.targets = targets; + this.environmentValidator = new FormatterEnvironmentValidator(execFn); + this.fileDiscovery = new FormatterFileDiscovery(); + this.formatterService = new FormatterService(execFn); + } + + /** + * Factory method to create a CompactFormatter from command-line arguments. + * + * Supported argument patterns: + * - `--check` - Check formatting without modifying files + * - `--dir ` - Target specific directory + * - ` ...` - Target specific files + * + * @param args - Array of command-line arguments + * @returns New CompactFormatter instance configured from arguments + */ + static fromArgs(args: string[]): CompactFormatter { + let checkMode = false; + const targets: string[] = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--check') { + checkMode = true; + } else if (args[i] === '--dir') { + const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); + if (dirNameExists) { + targets.push(args[i + 1]); + i++; + } else { + throw new Error('--dir flag requires a directory name'); + } + } else if (!args[i].startsWith('--')) { + targets.push(args[i]); + } + } + + return new CompactFormatter(checkMode, targets, undefined); + } + + /** + * Validates the formatting environment and displays version information. + */ + async validateEnvironment(): Promise { + const { devToolsVersion } = await this.environmentValidator.validate(); + const targetDir = this.targets.length === 1 ? this.targets[0] : undefined; + FormatterUIService.displayEnvInfo(devToolsVersion, targetDir); + } + + /** + * Main formatting method that orchestrates the entire formatting process. + */ + async format(): Promise { + await this.validateEnvironment(); + + // Handle specific file targets + if (this.targets.length > 0 && this.targets.every(target => target.endsWith('.compact'))) { + return this.formatSpecificFiles(); + } + + // Handle directory target or current directory + const targetDir = this.targets.length === 1 ? this.targets[0] : undefined; + return this.formatDirectory(targetDir); + } + + /** + * Formats specific files provided as arguments. + */ + private async formatSpecificFiles(): Promise { + if (this.checkMode) { + // For check mode with specific files, we need to check each file + for (const file of this.targets) { + await this.checkFile(file); + } + } else { + // Format the specific files + const result = await this.formatterService.formatFiles(this.targets); + FormatterUIService.printOutput(result.stdout, chalk.cyan); + FormatterUIService.printOutput(result.stderr, chalk.yellow); + } + } + + /** + * Formats all files in a directory or current directory. + */ + private async formatDirectory(targetDir?: string): Promise { + const searchDir = targetDir ? join(SRC_DIR, targetDir) : SRC_DIR; + + // Validate target directory exists + if (targetDir && !existsSync(searchDir)) { + throw new DirectoryNotFoundError( + `Target directory ${searchDir} does not exist`, + searchDir, + ); + } + + const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); + + if (compactFiles.length === 0) { + FormatterUIService.showNoFiles(targetDir); + return; + } + + const mode = this.checkMode ? 'check' : 'format'; + FormatterUIService.showFormattingStart(compactFiles.length, mode, targetDir); + + if (this.checkMode) { + const result = await this.formatterService.checkFormatting(searchDir); + FormatterUIService.showCheckResults(result.isFormatted, result.stdout); + } else { + const result = await this.formatterService.formatInPlace(searchDir); + + // Successful formatting typically produces no output + if (result.stdout.trim()) { + FormatterUIService.printOutput(result.stdout, chalk.cyan); + } + if (result.stderr.trim()) { + FormatterUIService.printOutput(result.stderr, chalk.yellow); + } + + const spinner = ora(); + spinner.succeed(chalk.green(`[FORMAT] Successfully formatted ${compactFiles.length} file(s)`)); + } + } + + /** + * Checks formatting for a specific file. + */ + private async checkFile(file: string): Promise { + const result = await this.formatterService.checkFormatting(file); + + if (result.isFormatted) { + const spinner = ora(); + spinner.succeed(chalk.green(`[FORMAT] ${file} is properly formatted`)); + } else { + const spinner = ora(); + spinner.fail(chalk.red(`[FORMAT] ${file} is not properly formatted`)); + if (result.stdout) { + FormatterUIService.printOutput(result.stdout, chalk.white); + } + } + } + + /** + * For testing - expose internal state + */ + get testCheckMode(): boolean { + return this.checkMode; + } + + get testTargets(): string[] { + return this.targets; + } +} diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts new file mode 100644 index 00000000..7ac80dd5 --- /dev/null +++ b/compact/src/runFormatter.ts @@ -0,0 +1,230 @@ +#!/usr/bin/env node + +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { CompactFormatter } from './Formatter.js'; +import { + type FormatterError, + isPromisifiedChildProcessError, +} from './types/errors.js'; + +/** + * Executes the Compact formatter CLI with improved error handling and user feedback. + * + * This CLI provides formatting capabilities for .compact files using the Compact developer tools. + * It supports both formatting in-place and checking formatting without modifications. + * + * @example Directory formatting + * ```bash + * npx compact-formatter --dir security + * npx compact-formatter --dir token --check + * ``` + * + * @example Specific file formatting + * ```bash + * npx compact-formatter src/contracts/Token.compact src/utils/Helper.compact + * npx compact-formatter --check src/contracts/Token.compact + * ``` + * + * @example Full project formatting + * ```bash + * npx compact-formatter + * npx compact-formatter --check + * ``` + */ +async function runFormatter(): Promise { + const spinner = ora(chalk.blue('[FORMAT] Compact formatter started')).info(); + + try { + const args = process.argv.slice(2); + const formatter = CompactFormatter.fromArgs(args); + await formatter.format(); + } catch (error) { + handleError(error, spinner); + process.exit(1); + } +} + +/** + * Centralized error handling with specific error types and user-friendly messages. + * + * Handles different error types with appropriate user feedback: + * + * - `CompactCliNotFoundError`: Shows installation instructions. + * - `FormatterNotAvailableError`: Shows update instructions for formatter support. + * - `DirectoryNotFoundError`: Shows available directories. + * - `FormatterError`: Shows formatting-specific error details. + * - Environment validation errors: Shows troubleshooting tips. + * - Argument parsing errors: Shows usage help. + * - Generic errors: Shows general troubleshooting guidance. + * + * @param error - The error that occurred during formatting + * @param spinner - Ora spinner instance for consistent UI messaging + */ +function handleError(error: unknown, spinner: Ora): void { + // CompactCliNotFoundError + if (error instanceof Error && error.name === 'CompactCliNotFoundError') { + spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); + spinner.info( + chalk.blue( + `[FORMAT] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, + ), + ); + return; + } + + // FormatterNotAvailableError + if (error instanceof Error && error.name === 'FormatterNotAvailableError') { + spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); + spinner.info( + chalk.blue('[FORMAT] Update compiler with: compact update'), + ); + spinner.info( + chalk.blue('[FORMAT] Update dev tools with: compact self update'), + ); + return; + } + + // DirectoryNotFoundError + if (error instanceof Error && error.name === 'DirectoryNotFoundError') { + spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); + showAvailableDirectories(); + return; + } + + // FormatterError + if (error instanceof Error && error.name === 'FormatterError') { + const formatterError = error as FormatterError; + spinner.fail( + chalk.red( + `[FORMAT] Formatting failed${formatterError.target ? ` for: ${formatterError.target}` : ''}`, + ), + ); + + if (isPromisifiedChildProcessError(formatterError.cause)) { + const execError = formatterError.cause; + if (execError.stderr && !execError.stderr.includes('stdout')) { + console.log( + chalk.red(` Additional error details: ${execError.stderr}`), + ); + } + if (execError.stdout) { + console.log(chalk.yellow(' Output:')); + console.log(chalk.yellow(` ${execError.stdout}`)); + } + } + return; + } + + // Environment validation errors (non-CLI errors) + if (isPromisifiedChildProcessError(error)) { + spinner.fail( + chalk.red(`[FORMAT] Environment validation failed: ${error.message}`), + ); + console.log(chalk.gray('\nTroubleshooting:')); + console.log( + chalk.gray(' • Check that Compact CLI is installed and in PATH'), + ); + console.log( + chalk.gray(' • Update compiler with: compact update'), + ); + console.log( + chalk.gray(' • Update dev tools with: compact self update'), + ); + console.log(chalk.gray(' • Ensure you have proper permissions')); + return; + } + + // Argument parsing + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('--dir flag requires a directory name')) { + spinner.fail( + chalk.red('[FORMAT] Error: --dir flag requires a directory name'), + ); + showUsageHelp(); + return; + } + + // Unexpected errors + spinner.fail(chalk.red(`[FORMAT] Unexpected error: ${errorMessage}`)); + console.log(chalk.gray('\nIf this error persists, please check:')); + console.log(chalk.gray(' • Compact CLI is installed and in PATH')); + console.log(chalk.gray(' • Compact compiler is updated (compact update)')); + console.log(chalk.gray(' • Source files exist and are readable')); + console.log(chalk.gray(' • File system permissions are correct')); +} + +/** + * Shows available directories when `DirectoryNotFoundError` occurs. + */ +function showAvailableDirectories(): void { + console.log(chalk.yellow('\nAvailable directories:')); + console.log( + chalk.yellow(' --dir access # Format access control contracts'), + ); + console.log(chalk.yellow(' --dir archive # Format archive contracts')); + console.log(chalk.yellow(' --dir security # Format security contracts')); + console.log(chalk.yellow(' --dir token # Format token contracts')); + console.log(chalk.yellow(' --dir utils # Format utility contracts')); +} + +/** + * Shows usage help with examples for different scenarios. + */ +function showUsageHelp(): void { + console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); + console.log(chalk.yellow('\nOptions:')); + console.log( + chalk.yellow( + ' --check Check if files are properly formatted (no modifications)', + ), + ); + console.log( + chalk.yellow( + ' --dir Format specific directory (access, archive, security, token, utils)', + ), + ); + console.log(chalk.yellow('\nExamples:')); + console.log( + chalk.yellow( + ' compact-formatter # Format all files', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --check # Check all files', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --dir security # Format security directory', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --dir access --check # Check access directory', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter file1.compact file2.compact # Format specific files', + ), + ); + console.log( + chalk.yellow( + ' compact-formatter --check file1.compact # Check specific file', + ), + ); + console.log(chalk.yellow('\nIntegration examples:')); + console.log( + chalk.yellow(' turbo format # Full formatting'), + ); + console.log( + chalk.yellow(' turbo format:security # Directory formatting'), + ); + console.log( + chalk.yellow(' turbo format:check # Check formatting'), + ); +} + +runFormatter(); diff --git a/compact/src/types/errors.ts b/compact/src/types/errors.ts index 8fcf6da3..70a50caa 100644 --- a/compact/src/types/errors.ts +++ b/compact/src/types/errors.ts @@ -10,6 +10,7 @@ * @prop {string} stderr stderr of a child process */ export interface PromisifiedChildProcessError extends Error { + code?: number; stdout: string; stderr: string; } @@ -24,7 +25,11 @@ export interface PromisifiedChildProcessError extends Error { export function isPromisifiedChildProcessError( error: unknown, ): error is PromisifiedChildProcessError { - return error instanceof Error && 'stdout' in error && 'stderr' in error; + return ( + error instanceof Error && + typeof (error as any).stdout === 'string' && + typeof (error as any).stderr === 'string' + ); } /** @@ -94,3 +99,32 @@ export class DirectoryNotFoundError extends Error { this.name = 'DirectoryNotFoundError'; } } + +/** + * Error thrown when the formatter is not available. + * This typically occurs when the Compact compiler version is too old (< 0.25.0). + */ +export class FormatterNotAvailableError extends Error { + constructor(message: string) { + super(message); + this.name = 'FormatterNotAvailableError'; + } +} + +/** + * Error thrown when formatting operations fail. + * Includes the specific target (file or directory) that failed for context. + */ +export class FormatterError extends Error { + /** The target file or directory that failed to format */ + public readonly target?: string; + /** The underlying cause of the formatting failure */ + public readonly cause?: unknown; + + constructor(message: string, target?: string, cause?: unknown) { + super(message); + this.name = 'FormatterError'; + this.target = target; + this.cause = cause; + } +} From d51afd80b2f0bb39ba93f00174b174beaacab4fa Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 21:03:40 -0300 Subject: [PATCH 06/44] update turbo and yarn.lock --- turbo.json | 7 +++++++ yarn.lock | 1 + 2 files changed, 8 insertions(+) diff --git a/turbo.json b/turbo.json index b52c9c45..b1fea330 100644 --- a/turbo.json +++ b/turbo.json @@ -73,6 +73,13 @@ ], "outputs": ["dist/**"] }, + "format": { + "dependsOn": ["^build"] + }, + "format:check": { + "dependsOn": ["^build"], + "cache": false + }, "types": { "dependsOn": [ "@openzeppelin-compact/compact#build", diff --git a/yarn.lock b/yarn.lock index b628db43..a0e3e118 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,6 +409,7 @@ __metadata: bin: compact-builder: dist/runBuilder.js compact-compiler: dist/runCompiler.js + compact-formatter: ./dist/runFormatter.js languageName: unknown linkType: soft From f84e86134f6feb17cb1f43852baecca7ed2f552d Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 21:03:52 -0300 Subject: [PATCH 07/44] add formatter cmds --- contracts/package.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/package.json b/contracts/package.json index acd41414..8cddb107 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -22,6 +22,8 @@ "compact:utils": "compact-compiler --dir utils", "build": "compact-builder && tsc", "test": "compact-compiler --skip-zk && vitest run", + "format": "compact-formatter", + "format:check": "compact-formatter --check", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, From 7fc8fb45f52dbe7d689c6c795a37e5bc344e31d5 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 5 Sep 2025 21:04:08 -0300 Subject: [PATCH 08/44] fmt all .compact files --- contracts/src/access/AccessControl.compact | 102 ++++++------ contracts/src/access/Ownable.compact | 3 +- contracts/src/access/ZOwnablePK.compact | 26 +-- .../test/mocks/MockAccessControl.compact | 6 +- .../src/access/test/mocks/MockOwnable.compact | 3 +- .../access/test/mocks/MockZOwnablePK.compact | 5 +- contracts/src/archive/ShieldedToken.compact | 10 +- .../test/mocks/MockShieldedToken.compact | 30 ++-- .../test/mocks/MockInitializable.compact | 7 +- .../security/test/mocks/MockPausable.compact | 1 + contracts/src/token/FungibleToken.compact | 116 ++++++-------- contracts/src/token/MultiToken.compact | 111 +++++++------ contracts/src/token/NonFungibleToken.compact | 150 ++++++++---------- .../test/mocks/MockFungibleToken.compact | 77 ++++----- .../token/test/mocks/MockMultiToken.compact | 76 +++++---- .../test/mocks/MockNonFungibleToken.compact | 96 +++++------ contracts/src/utils/Utils.compact | 23 +-- .../src/utils/test/mocks/MockUtils.compact | 6 +- 18 files changed, 390 insertions(+), 458 deletions(-) diff --git a/contracts/src/access/AccessControl.compact b/contracts/src/access/AccessControl.compact index 31717e13..55e15000 100644 --- a/contracts/src/access/AccessControl.compact +++ b/contracts/src/access/AccessControl.compact @@ -81,7 +81,8 @@ module AccessControl { * @type {Map} * @type {Map, Map, Boolean>} _operatorRoles  */ - export ledger _operatorRoles: Map, Map, Boolean>>; + export ledger _operatorRoles: Map, + Map, Boolean>>; /** * @description Mapping from a role identifier to an admin role identifier. @@ -94,27 +95,22 @@ module AccessControl { export ledger DEFAULT_ADMIN_ROLE: Bytes<32>; - /** - * @description Returns `true` if `account` has been granted `roleId`. - * - * @circuitInfo k=10, rows=487 - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} account - The account to query. - * @return {Boolean} - Whether the account has the specified role. + /** + * @description Returns `true` if `account` has been granted `roleId`. + * + * @circuitInfo k=10, rows=487 + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} account - The account to query. + * @return {Boolean} - Whether the account has the specified role.   */ export circuit hasRole(roleId: Bytes<32>, account: Either): Boolean { - if ( - _operatorRoles.member(disclose(roleId)) && - _operatorRoles - .lookup(roleId) - .member(disclose(account)) - ) { - return _operatorRoles - .lookup(roleId) - .lookup(disclose(account)); - } else { - return false; + if (_operatorRoles.member(disclose(roleId)) && + _operatorRoles.lookup(roleId).member(disclose(account))) { + return _operatorRoles.lookup(roleId).lookup(disclose(account)); + } + else { + return false; } } @@ -132,7 +128,7 @@ module AccessControl { * @return {[]} - Empty tuple. */ export circuit assertOnlyRole(roleId: Bytes<32>): [] { - _checkRole(roleId, left(ownPublicKey())); + _checkRole(roleId, left(ownPublicKey())); } /** @@ -207,26 +203,29 @@ module AccessControl { _revokeRole(roleId, account); } - /** - * @description Revokes `roleId` from the calling account. - * - * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's - * purpose is to provide a mechanism for accounts to lose their privileges - * if they are compromised (such as when a trusted device is misplaced). - * - * @circuitInfo k=10, rows=640 - * - * Requirements: - * - * - The caller must be `callerConfirmation`. - * - The caller must not be a `ContractAddress`. - * - * @param {Bytes<32>} roleId - The role identifier. - * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. - * @return {[]} - Empty tuple. - */ - export circuit renounceRole(roleId: Bytes<32>, callerConfirmation: Either): [] { - assert(callerConfirmation == left(ownPublicKey()), "AccessControl: bad confirmation"); + /** + * @description Revokes `roleId` from the calling account. + * + * @notice Roles are often managed via {grantRole} and {revokeRole}: this circuit's + * purpose is to provide a mechanism for accounts to lose their privileges + * if they are compromised (such as when a trusted device is misplaced). + * + * @circuitInfo k=10, rows=640 + * + * Requirements: + * + * - The caller must be `callerConfirmation`. + * - The caller must not be a `ContractAddress`. + * + * @param {Bytes<32>} roleId - The role identifier. + * @param {Either} callerConfirmation - A ZswapCoinPublicKey or ContractAddress. + * @return {[]} - Empty tuple. + */ + export circuit renounceRole( + roleId: Bytes<32>, + callerConfirmation: Either + ): [] { + assert(callerConfirmation == left(ownPublicKey()), "AccessControl: bad confirmation"); _revokeRole(roleId, callerConfirmation); } @@ -276,22 +275,16 @@ module AccessControl { * @param {Either} account - A ZswapCoinPublicKey or ContractAddress. * @return {Boolean} roleGranted - A boolean indicating if `role` was granted. */ - export circuit _unsafeGrantRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit _unsafeGrantRole( + roleId: Bytes<32>, account: Either): Boolean { if (hasRole(roleId, account)) { return false; } if (!_operatorRoles.member(disclose(roleId))) { _operatorRoles.insert( - disclose(roleId), - default, - Boolean - >> - ); - _operatorRoles - .lookup(roleId) - .insert(disclose(account), true); + disclose(roleId), default, Boolean>>); + _operatorRoles.lookup(roleId).insert(disclose(account), true); return true; } @@ -309,14 +302,13 @@ module AccessControl { * @param {Bytes<32>} adminRole - The admin role identifier. * @return {Boolean} roleRevoked - A boolean indicating if `roleId` was revoked. */ - export circuit _revokeRole(roleId: Bytes<32>, account: Either): Boolean { + export circuit _revokeRole( + roleId: Bytes<32>, account: Either): Boolean { if (!hasRole(roleId, account)) { return false; } - _operatorRoles - .lookup(roleId) - .insert(disclose(account), false); + _operatorRoles.lookup(roleId).insert(disclose(account), false); return true; } } diff --git a/contracts/src/access/Ownable.compact b/contracts/src/access/Ownable.compact index e2ee4848..263a6cdb 100644 --- a/contracts/src/access/Ownable.compact +++ b/contracts/src/access/Ownable.compact @@ -206,7 +206,8 @@ module Ownable { * @param {Either} newOwner - The new owner. * @returns {[]} Empty tuple. */ - export circuit _unsafeUncheckedTransferOwnership(newOwner: Either): [] { + export circuit _unsafeUncheckedTransferOwnership( + newOwner: Either): [] { Initializable_assertInitialized(); _owner = disclose(newOwner); } diff --git a/contracts/src/access/ZOwnablePK.compact b/contracts/src/access/ZOwnablePK.compact index 37e4616f..dbed42ce 100644 --- a/contracts/src/access/ZOwnablePK.compact +++ b/contracts/src/access/ZOwnablePK.compact @@ -190,11 +190,10 @@ module ZOwnablePK { Initializable_assertInitialized(); const nonce = wit_secretNonce(); - const callerAsEither = Either { - is_left: true, - left: ownPublicKey(), - right: ContractAddress { bytes: pad(32, "") } - }; + const callerAsEither = + Either { is_left: true, + left: ownPublicKey(), + right: ContractAddress { bytes: pad(32, "") } }; const id = _computeOwnerId(callerAsEither, nonce); assert(_ownerCommitment == _computeOwnerCommitment(id, _counter), "ZOwnablePK: caller is not the owner"); } @@ -231,19 +230,10 @@ module ZOwnablePK { * after every transfer to prevent duplicate commitments given the same `id`. * @returns {Bytes<32>} The commitment derived from `id` and `counter`. */ - export circuit _computeOwnerCommitment( - id: Bytes<32>, - counter: Uint<64>, - ): Bytes<32> { + export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>,): Bytes<32> { Initializable_assertInitialized(); return persistentHash>>( - [ - id, - _instanceSalt, - counter as Field as Bytes<32>, - pad(32, "ZOwnablePK:shield:") - ] - ); + [id, _instanceSalt, counter as Field as Bytes<32>, pad(32, "ZOwnablePK:shield:")]); } /** @@ -279,9 +269,7 @@ module ZOwnablePK { * @returns {Bytes<32>} The computed owner ID. */ export pure circuit _computeOwnerId( - pk: Either, - nonce: Bytes<32> - ): Bytes<32> { + pk: Either, nonce: Bytes<32>): Bytes<32> { assert(pk.is_left, "ZOwnablePK: contract address owners are not yet supported"); return persistentHash>>([pk.left.bytes, nonce]); diff --git a/contracts/src/access/test/mocks/MockAccessControl.compact b/contracts/src/access/test/mocks/MockAccessControl.compact index 0cc10a1c..95e92a8a 100644 --- a/contracts/src/access/test/mocks/MockAccessControl.compact +++ b/contracts/src/access/test/mocks/MockAccessControl.compact @@ -32,7 +32,8 @@ export circuit revokeRole(roleId: Bytes<32>, account: Either, callerConfirmation: Either): [] { +export circuit renounceRole( + roleId: Bytes<32>, callerConfirmation: Either): [] { AccessControl_renounceRole(roleId, callerConfirmation); } @@ -44,7 +45,8 @@ export circuit _grantRole(roleId: Bytes<32>, account: Either, account: Either): Boolean { +export circuit _unsafeGrantRole( + roleId: Bytes<32>, account: Either): Boolean { return AccessControl__unsafeGrantRole(roleId, account); } diff --git a/contracts/src/access/test/mocks/MockOwnable.compact b/contracts/src/access/test/mocks/MockOwnable.compact index ac7ca266..a23136a0 100644 --- a/contracts/src/access/test/mocks/MockOwnable.compact +++ b/contracts/src/access/test/mocks/MockOwnable.compact @@ -45,6 +45,7 @@ export circuit _transferOwnership(newOwner: Either): [] { +export circuit _unsafeUncheckedTransferOwnership( + newOwner: Either): [] { return Ownable__unsafeUncheckedTransferOwnership(newOwner); } diff --git a/contracts/src/access/test/mocks/MockZOwnablePK.compact b/contracts/src/access/test/mocks/MockZOwnablePK.compact index d769f30e..855ec463 100644 --- a/contracts/src/access/test/mocks/MockZOwnablePK.compact +++ b/contracts/src/access/test/mocks/MockZOwnablePK.compact @@ -3,9 +3,11 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; + import "../../ZOwnablePK" prefix ZOwnablePK_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + export { ZOwnablePK__ownerCommitment, ZOwnablePK__counter }; /** @@ -41,7 +43,8 @@ export circuit _computeOwnerCommitment(id: Bytes<32>, counter: Uint<64>): Bytes< return ZOwnablePK__computeOwnerCommitment(id, counter); } -export pure circuit _computeOwnerId(pk: Either, nonce: Bytes<32>): Bytes<32> { +export pure circuit _computeOwnerId( + pk: Either, nonce: Bytes<32>): Bytes<32> { return ZOwnablePK__computeOwnerId(pk, nonce); } diff --git a/contracts/src/archive/ShieldedToken.compact b/contracts/src/archive/ShieldedToken.compact index 4497a556..9ab1b60b 100644 --- a/contracts/src/archive/ShieldedToken.compact +++ b/contracts/src/archive/ShieldedToken.compact @@ -51,11 +51,11 @@ module ShieldedToken { // DO NOT USE IN PRODUCTION! * @return {[]} - Empty tuple. */ export circuit initializer( - initNonce: Bytes<32>, - name_: Maybe>, - symbol_: Maybe>, - decimals_ :Uint<8> - ): [] { + initNonce: Bytes<32>, + name_: Maybe>, + symbol_: Maybe>, + decimals_: Uint<8> + ): [] { _nonce = disclose(initNonce); _domain = pad(32, "ShieldedToken"); _name = disclose(name_); diff --git a/contracts/src/archive/test/mocks/MockShieldedToken.compact b/contracts/src/archive/test/mocks/MockShieldedToken.compact index 766ded21..63b0abe4 100644 --- a/contracts/src/archive/test/mocks/MockShieldedToken.compact +++ b/contracts/src/archive/test/mocks/MockShieldedToken.compact @@ -3,29 +3,17 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; + import "../../ShieldedToken" prefix ShieldedToken_; -export { - ZswapCoinPublicKey, - ContractAddress, - Either, - Maybe, - CoinInfo, - SendResult -}; - -export { - ShieldedToken__counter, - ShieldedToken__nonce, - ShieldedToken__domain -}; - -constructor( - _nonce: Bytes<32>, - _name: Maybe>, - _symbol: Maybe>, - _decimals:Uint<8> -) { +export { ZswapCoinPublicKey, ContractAddress, Either, Maybe, CoinInfo, SendResult }; + +export { ShieldedToken__counter, ShieldedToken__nonce, ShieldedToken__domain }; + +constructor(_nonce: Bytes<32>, + _name: Maybe>, + _symbol: Maybe>, + _decimals: Uint<8>) { ShieldedToken_initializer(_nonce, _name, _symbol, _decimals); } diff --git a/contracts/src/security/test/mocks/MockInitializable.compact b/contracts/src/security/test/mocks/MockInitializable.compact index d4c9bdef..896c7622 100644 --- a/contracts/src/security/test/mocks/MockInitializable.compact +++ b/contracts/src/security/test/mocks/MockInitializable.compact @@ -1,18 +1,19 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; + import "../../Initializable" prefix Initializable_; export { Initializable__isInitialized }; export circuit initialize(): [] { - return Initializable_initialize(); + return Initializable_initialize(); } export circuit assertInitialized(): [] { - return Initializable_assertInitialized(); + return Initializable_assertInitialized(); } export circuit assertNotInitialized(): [] { - return Initializable_assertNotInitialized(); + return Initializable_assertNotInitialized(); } diff --git a/contracts/src/security/test/mocks/MockPausable.compact b/contracts/src/security/test/mocks/MockPausable.compact index a4695485..a87b6bd1 100644 --- a/contracts/src/security/test/mocks/MockPausable.compact +++ b/contracts/src/security/test/mocks/MockPausable.compact @@ -1,6 +1,7 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; + import "../../Pausable" prefix Pausable_; export { Pausable__isPaused }; diff --git a/contracts/src/token/FungibleToken.compact b/contracts/src/token/FungibleToken.compact index 5b11e7c1..37c5474b 100644 --- a/contracts/src/token/FungibleToken.compact +++ b/contracts/src/token/FungibleToken.compact @@ -60,7 +60,8 @@ module FungibleToken { * @type {Map>} * @type {Map, Map, Uint<128>>>} _allowances */ - export ledger _allowances: Map, Map, Uint<128>>>; + export ledger _allowances: Map, + Map, Uint<128>>>; export ledger _totalSupply: Uint<128>; @@ -80,11 +81,7 @@ module FungibleToken { * @param {Uint<8>} decimals_ - The number of decimals used to get the user representation. * @return {[]} - Empty tuple. */ - export circuit initialize( - name_: Opaque<"string">, - symbol_: Opaque<"string">, - decimals_:Uint<8> - ): [] { + export circuit initialize(name_: Opaque<"string">, symbol_: Opaque<"string">, decimals_: Uint<8>): [] { Initializable_initialize(); _name = disclose(name_); _symbol = disclose(symbol_); @@ -247,9 +244,9 @@ module FungibleToken { * @return {Uint<128>} - The `spender`'s allowance over `owner`'s tokens. */ export circuit allowance( - owner: Either, - spender: Either - ): Uint<128> { + owner: Either, + spender: Either + ): Uint<128> { Initializable_assertInitialized(); if (!_allowances.member(disclose(owner)) || !_allowances.lookup(owner).member(disclose(spender))) { return 0; @@ -305,10 +302,10 @@ module FungibleToken { * @return {Boolean} - As per the IERC20 spec, this MUST return true. */ export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128> - ): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "FungibleToken: Unsafe Transfer"); return _unsafeTransferFrom(from, to, value); @@ -337,10 +334,10 @@ module FungibleToken { * @return {Boolean} - As per the IERC20 spec, this MUST return true. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - value: Uint<128> - ): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { Initializable_assertInitialized(); const spender = left(ownPublicKey()); @@ -374,10 +371,10 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - value: Uint<128> - ): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "FungibleToken: Unsafe Transfer"); _unsafeUncheckedTransfer(from, to, value); @@ -404,10 +401,10 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeUncheckedTransfer( - from: Either, - to: Either, - value: Uint<128> - ): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "FungibleToken: invalid sender"); assert(!Utils_isKeyOrAddressZero(to), "FungibleToken: invalid receiver"); @@ -431,30 +428,29 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens moved from `from` to `to`. * @return {[]} - Empty tuple. */ - circuit _update( - from: Either, - to: Either, - value: Uint<128> - ): [] { + circuit _update(from: Either, + to: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); if (Utils_isKeyOrAddressZero(disclose(from))) { - // Mint - const MAX_UINT128 = 340282366920938463463374607431768211455; - assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow"); + // Mint + const MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - _totalSupply >= value, "FungibleToken: arithmetic overflow"); - _totalSupply = disclose(_totalSupply + value as Uint<128>); + _totalSupply = disclose(_totalSupply + value as Uint<128>); } else { - const fromBal = balanceOf(from); - assert(fromBal >= value, "FungibleToken: insufficient balance"); - _balances.insert(disclose(from), disclose(fromBal - value as Uint<128>)); + const fromBal = balanceOf(from); + assert(fromBal >= value, "FungibleToken: insufficient balance"); + _balances.insert(disclose(from), disclose(fromBal - value as Uint<128>)); } if (Utils_isKeyOrAddressZero(disclose(to))) { - // Burn - _totalSupply = disclose(_totalSupply - value as Uint<128>); + // Burn + _totalSupply = disclose(_totalSupply - value as Uint<128>); } else { - const toBal = balanceOf(to); - _balances.insert(disclose(to), disclose(toBal + value as Uint<128>)); + const toBal = balanceOf(to); + _balances.insert(disclose(to), disclose(toBal + value as Uint<128>)); } } @@ -478,10 +474,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens minted. * @return {[]} - Empty tuple. */ - export circuit _mint( - account: Either, - value: Uint<128> - ): [] { + export circuit _mint(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(account), "FungibleToken: Unsafe Transfer"); _unsafeMint(account, value); @@ -505,10 +498,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens minted. * @return {[]} - Empty tuple. */ - export circuit _unsafeMint( - account: Either, - value: Uint<128> - ): [] { + export circuit _unsafeMint(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(account), "FungibleToken: invalid receiver"); _update(burnAddress(), account, value); @@ -530,10 +520,7 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens to burn. * @return {[]} - Empty tuple. */ - export circuit _burn( - account: Either, - value: Uint<128> - ): [] { + export circuit _burn(account: Either, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(account), "FungibleToken: invalid sender"); _update(account, burnAddress(), value); @@ -558,17 +545,17 @@ module FungibleToken { * @param {Uint<128>} value - The amount of tokens `spender` may spend on behalf of `owner`. * @return {[]} - Empty tuple. */ - export circuit _approve( - owner: Either, - spender: Either, - value: Uint<128> - ): [] { + export circuit _approve(owner: Either, + spender: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(owner), "FungibleToken: invalid owner"); assert(!Utils_isKeyOrAddressZero(spender), "FungibleToken: invalid spender"); if (!_allowances.member(disclose(owner))) { // If owner doesn't exist, create and insert a new sub-map directly - _allowances.insert(disclose(owner), default, Uint<128>>>); + _allowances.insert( + disclose(owner), default, Uint<128>>>); } _allowances.lookup(owner).insert(disclose(spender), disclose(value)); } @@ -590,12 +577,13 @@ module FungibleToken { * @return {[]} - Empty tuple. */ export circuit _spendAllowance( - owner: Either, - spender: Either, - value: Uint<128> - ): [] { + owner: Either, + spender: Either, + value: Uint<128> + ): [] { Initializable_assertInitialized(); - assert((_allowances.member(disclose(owner)) && _allowances.lookup(owner).member(disclose(spender))), "FungibleToken: insufficient allowance"); + assert((_allowances.member(disclose(owner)) && + _allowances.lookup(owner).member(disclose(spender))), "FungibleToken: insufficient allowance"); const currentAllowance = _allowances.lookup(owner).lookup(disclose(spender)); const MAX_UINT128 = 340282366920938463463374607431768211455; diff --git a/contracts/src/token/MultiToken.compact b/contracts/src/token/MultiToken.compact index d1080ce4..b2d42d2b 100644 --- a/contracts/src/token/MultiToken.compact +++ b/contracts/src/token/MultiToken.compact @@ -76,7 +76,8 @@ module MultiToken { * @type {Map>} * @type {Map, Map, Uint<128>>>} _balances */ - export ledger _balances: Map, Map, Uint<128>>>; + export ledger _balances: Map, + Map, Uint<128>>>; /** * @description Mapping from account to operator approvals. @@ -86,7 +87,8 @@ module MultiToken { * @type {Map>} * @type {Map, Map, Boolean>>} */ - export ledger _operatorApprovals: Map, Map, Boolean>>; + export ledger _operatorApprovals: Map, + Map, Boolean>>; /** * @description Base URI for computing token URIs. @@ -172,7 +174,8 @@ module MultiToken { * caller's assets. * @return {[]} - Empty tuple. */ - export circuit setApprovalForAll(operator: Either, approved: Boolean): [] { + export circuit setApprovalForAll( + operator: Either, approved: Boolean): [] { Initializable_assertInitialized(); // TODO: Contract-to-contract calls not yet supported. @@ -194,12 +197,13 @@ module MultiToken { * @return {Boolean} - Whether or not `operator` has permission to handle `account`'s assets. */ export circuit isApprovedForAll( - account: Either, - operator: Either - ): Boolean { + account: Either, + operator: Either + ): Boolean { Initializable_assertInitialized(); - if (!_operatorApprovals.member(disclose(account)) || !_operatorApprovals.lookup(account).member(disclose(operator))) { + if (!_operatorApprovals.member(disclose(account)) || + !_operatorApprovals.lookup(account).member(disclose(operator))) { return false; } @@ -235,11 +239,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit transferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeTransferFrom(from, to, id, value); } @@ -270,11 +274,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeTransfer(from, to, id, value); } @@ -296,12 +300,11 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens to transfer. * @return {[]} - Empty tuple. */ - circuit _update( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + circuit _update(from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); if (!Utils_isKeyOrAddressZero(disclose(from))) { @@ -315,14 +318,16 @@ module MultiToken { if (!Utils_isKeyOrAddressZero(disclose(to))) { // id not initialized if (!_balances.member(disclose(id))) { - _balances.insert(disclose(id), default, Uint<128>>>); - _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); - } else { - const toBalance = balanceOf(to, id); - // TODO: Replace with Max_Uint128() - const MAX_UINT128 = 340282366920938463463374607431768211455; - assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); - _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); + _balances.insert( + disclose(id), default, Uint<128>>>); + _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); + } + else { + const toBalance = balanceOf(to, id); + // TODO: Replace with Max_Uint128() + const MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); + _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); } } } @@ -352,11 +357,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); // TODO: Contract-to-contract calls not yet supported. @@ -394,11 +399,11 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> - ): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "MultiToken: invalid sender"); @@ -455,7 +460,10 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`. * @return {[]} - Empty tuple. */ - export circuit _mint(to: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _mint(to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { assert(!Utils_isContractAddress(to), "MultiToken: unsafe transfer"); _unsafeMint(to, id, value); } @@ -479,7 +487,8 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that are minted to `to`. * @return {[]} - Empty tuple. */ - export circuit _unsafeMint(to: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _unsafeMint( + to: Either, id: Uint<128>, value: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "MultiToken: invalid receiver"); @@ -502,7 +511,10 @@ module MultiToken { * @param {Uint<128>} value - The quantity of `id` tokens that will be destroyed from `from`. * @return {[]} - Empty tuple. */ - export circuit _burn(from: Either, id: Uint<128>, value: Uint<128>): [] { + export circuit _burn(from: Either, + id: Uint<128>, + value: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(from), "MultiToken: invalid sender"); @@ -530,15 +542,16 @@ module MultiToken { * @return {[]} - Empty tuple. */ export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean - ): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(operator), "MultiToken: invalid operator"); if (!_operatorApprovals.member(disclose(owner))) { - _operatorApprovals.insert(disclose(owner), default, Boolean>>); + _operatorApprovals.insert( + disclose(owner), default, Boolean>>); } _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(approved)); diff --git a/contracts/src/token/NonFungibleToken.compact b/contracts/src/token/NonFungibleToken.compact index f73e8258..672b273c 100644 --- a/contracts/src/token/NonFungibleToken.compact +++ b/contracts/src/token/NonFungibleToken.compact @@ -83,7 +83,8 @@ module NonFungibleToken { * @type {Map>} * @type {Map, Map, Boolean>>} _operatorApprovals */ - export ledger _operatorApprovals: Map, Map, Boolean>>; + export ledger _operatorApprovals: Map, + Map, Boolean>>; /** * @description Mapping from token IDs to their metadata URIs. @@ -258,17 +259,10 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token `to` may be permitted to transfer * @return {[]} - Empty tuple. */ - export circuit approve( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit approve(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); - const auth = left(ownPublicKey()); - _approve( - to, - tokenId, - auth - ); + const auth = left(ownPublicKey()); + _approve(to, tokenId, auth); } /** @@ -307,16 +301,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit setApprovalForAll( - operator: Either, - approved: Boolean - ): [] { + operator: Either, approved: Boolean): [] { Initializable_assertInitialized(); - const owner = left(ownPublicKey()); - _setApprovalForAll( - owner, - operator, - approved - ); + const owner = left(ownPublicKey()); + _setApprovalForAll(owner, operator, approved); } /** @@ -333,14 +321,16 @@ module NonFungibleToken { * @return {Boolean} - A boolean determining if `operator` is allowed to manage all of the tokens of `owner` */ export circuit isApprovedForAll( - owner: Either, - operator: Either - ): Boolean { + owner: Either, + operator: Either + ): Boolean { Initializable_assertInitialized(); - if (_operatorApprovals.member(disclose(owner)) && _operatorApprovals.lookup(owner).member(disclose(operator))) { - return _operatorApprovals.lookup(owner).lookup(disclose(operator)); - } else { - return false; + if (_operatorApprovals.member(disclose(owner)) && + _operatorApprovals.lookup(owner).member(disclose(operator))) { + return _operatorApprovals.lookup(owner).lookup(disclose(operator)); + } + else { + return false; } } @@ -368,10 +358,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit transferFrom( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -401,20 +391,16 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransferFrom( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); // Setting an "auth" arguments enables the `_isAuthorized` check which verifies that the token exists // (from != 0). Therefore, it is not needed to verify that the return value is not 0 here. - const auth = left(ownPublicKey()); - const previousOwner = _update( - to, - tokenId, - auth - ); + const auth = left(ownPublicKey()); + const previousOwner = _update(to, tokenId, auth); assert(previousOwner == from, "NonFungibleToken: Incorrect Owner"); } @@ -478,15 +464,15 @@ module NonFungibleToken { * @return {Boolean} - A boolean determining if `spender` may manage `tokenId` */ export circuit _isAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> - ): Boolean { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): Boolean { Initializable_assertInitialized(); - return ( - !Utils_isKeyOrAddressZero(disclose(spender)) && - (disclose(owner) == disclose(spender) || isApprovedForAll(owner, spender) || _getApproved(tokenId) == disclose(spender)) - ); + return (!Utils_isKeyOrAddressZero(disclose(spender)) && + (disclose(owner) == disclose(spender) || + isApprovedForAll(owner, spender) || + _getApproved(tokenId) == disclose(spender))); } /** @@ -508,10 +494,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _checkAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> - ): [] { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); if (!_isAuthorized(owner, spender, tokenId)) { assert(!Utils_isKeyOrAddressZero(owner), "NonFungibleToken: Nonexistent Token"); @@ -536,17 +522,16 @@ module NonFungibleToken { * @param {Either} auth - An account authorized to transfer the token * @return {Either} - Owner of the token before it was transfered */ - circuit _update( - to: Either, - tokenId: Uint<128>, - auth: Either - ): Either { + circuit _update(to: Either, + tokenId: Uint<128>, + auth: Either + ): Either { Initializable_assertInitialized(); const from = _ownerOf(tokenId); // Perform (optional) operator check if (!Utils_isKeyOrAddressZero(disclose(auth))) { - _checkAuthorized(from, auth, tokenId); + _checkAuthorized(from, auth, tokenId); } // Execute the update @@ -586,10 +571,7 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ - export circuit _mint( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit _mint(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -615,10 +597,7 @@ module NonFungibleToken { * @param {Uint<128>} tokenId - The token to transfer * @return {[]} - Empty tuple. */ - export circuit _unsafeMint( - to: Either, - tokenId: Uint<128> - ): [] { + export circuit _unsafeMint(to: Either, tokenId: Uint<128>): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); @@ -671,10 +650,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _transfer( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isContractAddress(to), "NonFungibleToken: Unsafe Transfer"); @@ -704,10 +683,10 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _unsafeTransfer( - from: Either, - to: Either, - tokenId: Uint<128> - ): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(to), "NonFungibleToken: Invalid Receiver"); @@ -733,11 +712,10 @@ module NonFungibleToken { * @param {Either} auth - An account authorized to operate on all tokens held by the owner the token * @return {[]} - Empty tuple. */ - export circuit _approve( - to: Either, - tokenId: Uint<128>, - auth: Either - ): [] { + export circuit _approve(to: Either, + tokenId: Uint<128>, + auth: Either + ): [] { Initializable_assertInitialized(); if (!Utils_isKeyOrAddressZero(disclose(auth))) { const owner = _requireOwned(tokenId); @@ -765,18 +743,16 @@ module NonFungibleToken { * @return {[]} - Empty tuple. */ export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean - ): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { Initializable_assertInitialized(); assert(!Utils_isKeyOrAddressZero(operator), "NonFungibleToken: Invalid Operator"); if (!_operatorApprovals.member(disclose(owner))) { _operatorApprovals.insert( - disclose(owner), - default, Boolean>> - ); + disclose(owner), default, Boolean>>); } _operatorApprovals.lookup(owner).insert(disclose(operator), disclose(approved)); diff --git a/contracts/src/token/test/mocks/MockFungibleToken.compact b/contracts/src/token/test/mocks/MockFungibleToken.compact index ddd30abd..8e21ae34 100644 --- a/contracts/src/token/test/mocks/MockFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockFungibleToken.compact @@ -15,12 +15,7 @@ export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; * Otherwise, the contract will not initialize and we can test * the contract when it is not initialized properly. */ -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - _decimals:Uint<8>, - init: Boolean -) { +constructor(_name: Opaque<"string">, _symbol: Opaque<"string">, _decimals: Uint<8>, init: Boolean) { if (disclose(init)) { FungibleToken_initialize(_name, _symbol, _decimals); } @@ -47,9 +42,9 @@ export circuit balanceOf(account: Either): } export circuit allowance( - owner: Either, - spender: Either -): Uint<128> { + owner: Either, + spender: Either + ): Uint<128> { return FungibleToken_allowance(owner, spender); } @@ -62,18 +57,18 @@ export circuit _unsafeTransfer(to: Either, } export circuit transferFrom( - from: Either, - to: Either, - value: Uint<128> -): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { return FungibleToken_transferFrom(from, to, value); } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - value: Uint<128> -): Boolean { + from: Either, + to: Either, + value: Uint<128> + ): Boolean { return FungibleToken__unsafeTransferFrom(from, to, value); } @@ -81,55 +76,45 @@ export circuit approve(spender: Either, val return FungibleToken_approve(spender, value); } -export circuit _approve( - owner: Either, - spender: Either, - value: Uint<128> -): [] { +export circuit _approve(owner: Either, + spender: Either, + value: Uint<128> + ): [] { return FungibleToken__approve(owner, spender, value); } export circuit _transfer( - from: Either, - to: Either, - value: Uint<128> -): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { return FungibleToken__transfer(from, to, value); } export circuit _unsafeUncheckedTransfer( - from: Either, - to: Either, - value: Uint<128> -): [] { + from: Either, + to: Either, + value: Uint<128> + ): [] { return FungibleToken__unsafeUncheckedTransfer(from, to, value); } -export circuit _mint( - account: Either, - value: Uint<128> -): [] { +export circuit _mint(account: Either, value: Uint<128>): [] { return FungibleToken__mint(account, value); } -export circuit _unsafeMint( - account: Either, - value: Uint<128> -): [] { +export circuit _unsafeMint(account: Either, value: Uint<128>): [] { return FungibleToken__unsafeMint(account, value); } -export circuit _burn( - account: Either, - value: Uint<128> -): [] { +export circuit _burn(account: Either, value: Uint<128>): [] { return FungibleToken__burn(account, value); } export circuit _spendAllowance( - owner: Either, - spender: Either, - value: Uint<128> -): [] { + owner: Either, + spender: Either, + value: Uint<128> + ): [] { return FungibleToken__spendAllowance(owner, spender, value); } diff --git a/contracts/src/token/test/mocks/MockMultiToken.compact b/contracts/src/token/test/mocks/MockMultiToken.compact index 00cb0d35..06125f72 100644 --- a/contracts/src/token/test/mocks/MockMultiToken.compact +++ b/contracts/src/token/test/mocks/MockMultiToken.compact @@ -1,9 +1,11 @@ pragma language_version >= 0.16.0; import CompactStandardLibrary; + import "../../MultiToken" prefix MultiToken_; export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; + export { MultiToken__balances, MultiToken__operatorApprovals, MultiToken__uri }; /** @@ -12,9 +14,7 @@ export { MultiToken__balances, MultiToken__operatorApprovals, MultiToken__uri }; * Otherwise, the contract will not initialize and we can test * the contract when it is not initialized properly. */ -constructor( - _uri: Maybe> -) { +constructor(_uri: Maybe>) { if (disclose(_uri.is_some)) { MultiToken_initialize(_uri.value); } @@ -32,50 +32,51 @@ export circuit balanceOf(account: Either, i return MultiToken_balanceOf(account, id); } -export circuit setApprovalForAll(operator: Either, approved: Boolean): [] { +export circuit setApprovalForAll( + operator: Either, approved: Boolean): [] { return MultiToken_setApprovalForAll(operator, approved); } export circuit isApprovedForAll( - account: Either, - operator: Either -): Boolean { + account: Either, + operator: Either + ): Boolean { return MultiToken_isApprovedForAll(account, operator); } export circuit transferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken_transferFrom(from, to, id, value); } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__unsafeTransferFrom(from, to, id, value); } export circuit _transfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__transfer(from, to, id, value); } export circuit _unsafeTransfer( - from: Either, - to: Either, - id: Uint<128>, - value: Uint<128> -): [] { + from: Either, + to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__unsafeTransfer(from, to, id, value); } @@ -83,22 +84,29 @@ export circuit _setURI(newURI: Opaque<"string">): [] { return MultiToken__setURI(newURI); } -export circuit _mint(to: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _mint(to: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__mint(to, id, value); } -export circuit _unsafeMint(to: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _unsafeMint( + to: Either, id: Uint<128>, value: Uint<128>): [] { return MultiToken__unsafeMint(to, id, value); } -export circuit _burn(from: Either, id: Uint<128>, value: Uint<128>): [] { +export circuit _burn(from: Either, + id: Uint<128>, + value: Uint<128> + ): [] { return MultiToken__burn(from, id, value); } export circuit _setApprovalForAll( - owner: Either, - operator: Either, - approved: Boolean -): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { return MultiToken__setApprovalForAll(owner, operator, approved); } diff --git a/contracts/src/token/test/mocks/MockNonFungibleToken.compact b/contracts/src/token/test/mocks/MockNonFungibleToken.compact index a883800d..278cfd9d 100644 --- a/contracts/src/token/test/mocks/MockNonFungibleToken.compact +++ b/contracts/src/token/test/mocks/MockNonFungibleToken.compact @@ -12,11 +12,7 @@ export { ZswapCoinPublicKey, ContractAddress, Either, Maybe }; * Otherwise, the contract will not initialize and we can test the * contract when it is not initialized properly. */ -constructor( - _name: Opaque<"string">, - _symbol: Opaque<"string">, - init: Boolean -) { +constructor(_name: Opaque<"string">, _symbol: Opaque<"string">, init: Boolean) { if (disclose(init)) { NonFungibleToken_initialize(_name, _symbol); } @@ -42,10 +38,7 @@ export circuit tokenURI(tokenId: Uint<128>): Opaque<"string"> { return NonFungibleToken_tokenURI(tokenId); } -export circuit approve( - to: Either, - tokenId: Uint<128> -): [] { +export circuit approve(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken_approve(to, tokenId); } @@ -54,24 +47,22 @@ export circuit getApproved(tokenId: Uint<128>): Either, - approved: Boolean -): [] { + operator: Either, approved: Boolean): [] { return NonFungibleToken_setApprovalForAll(operator, approved); } export circuit isApprovedForAll( - owner: Either, - operator: Either -): Boolean { + owner: Either, + operator: Either + ): Boolean { return NonFungibleToken_isApprovedForAll(owner, operator); } export circuit transferFrom( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken_transferFrom(from, to, tokenId); } @@ -83,27 +74,26 @@ export circuit _ownerOf(tokenId: Uint<128>): Either, - tokenId: Uint<128>, - auth: Either -): [] { +export circuit _approve(to: Either, + tokenId: Uint<128>, + auth: Either + ): [] { return NonFungibleToken__approve(to, tokenId, auth); } export circuit _checkAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> -): [] { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__checkAuthorized(owner, spender, tokenId); } export circuit _isAuthorized( - owner: Either, - spender: Either, - tokenId: Uint<128> -): Boolean { + owner: Either, + spender: Either, + tokenId: Uint<128> + ): Boolean { return NonFungibleToken__isAuthorized(owner, spender, tokenId); } @@ -112,17 +102,14 @@ export circuit _getApproved(tokenId: Uint<128>): Either, - operator: Either, - approved: Boolean -): [] { + owner: Either, + operator: Either, + approved: Boolean + ): [] { return NonFungibleToken__setApprovalForAll(owner, operator, approved); } -export circuit _mint( - to: Either, - tokenId: Uint<128> -): [] { +export circuit _mint(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken__mint(to, tokenId); } @@ -131,10 +118,10 @@ export circuit _burn(tokenId: Uint<128>): [] { } export circuit _transfer( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__transfer(from, to, tokenId); } @@ -143,24 +130,21 @@ export circuit _setTokenURI(tokenId: Uint<128>, tokenURI: Opaque<"string">): [] } export circuit _unsafeTransferFrom( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__unsafeTransferFrom(from, to, tokenId); } export circuit _unsafeTransfer( - from: Either, - to: Either, - tokenId: Uint<128> -): [] { + from: Either, + to: Either, + tokenId: Uint<128> + ): [] { return NonFungibleToken__unsafeTransfer(from, to, tokenId); } -export circuit _unsafeMint( - to: Either, - tokenId: Uint<128> -): [] { +export circuit _unsafeMint(to: Either, tokenId: Uint<128>): [] { return NonFungibleToken__unsafeMint(to, tokenId); } diff --git a/contracts/src/utils/Utils.compact b/contracts/src/utils/Utils.compact index a00cd220..17a72ef3 100644 --- a/contracts/src/utils/Utils.compact +++ b/contracts/src/utils/Utils.compact @@ -21,8 +21,8 @@ module Utils { */ export pure circuit isKeyOrAddressZero(keyOrAddress: Either): Boolean { return isContractAddress(keyOrAddress) - ? default == keyOrAddress.right - : default == keyOrAddress.left; + ? default == keyOrAddress.right + : default == keyOrAddress.left; } /** @@ -45,16 +45,17 @@ module Utils { * @return {Boolean} - Returns true if `keyOrAddress` is is equal to `other`. */ export pure circuit isKeyOrAddressEqual( - keyOrAddress: Either, - other: Either - ): Boolean { + keyOrAddress: Either, + other: Either + ): Boolean { if (keyOrAddress.is_left && other.is_left) { - return keyOrAddress.left == other.left; - } else if (!keyOrAddress.is_left && !other.is_left) { - return keyOrAddress.right == other.right; - } else { - return false; - } + return keyOrAddress.left == other.left; + } else + if (!keyOrAddress.is_left && !other.is_left) { + return keyOrAddress.right == other.right; + } else { + return false; + } } /** diff --git a/contracts/src/utils/test/mocks/MockUtils.compact b/contracts/src/utils/test/mocks/MockUtils.compact index 54fd45f3..720c4d13 100644 --- a/contracts/src/utils/test/mocks/MockUtils.compact +++ b/contracts/src/utils/test/mocks/MockUtils.compact @@ -11,9 +11,9 @@ export pure circuit isKeyOrAddressZero(keyOrAddress: Either, - other: Either -): Boolean { + keyOrAddress: Either, + other: Either + ): Boolean { return Utils_isKeyOrAddressEqual(keyOrAddress, other); } From e5ecac765433ddf35492b8738705621bd4612f29 Mon Sep 17 00:00:00 2001 From: andrew Date: Sat, 6 Sep 2025 23:14:55 -0300 Subject: [PATCH 09/44] improve design --- compact/src/BaseServices.ts | 390 ++++++++++++++++++++++++ compact/src/Compiler.ts | 589 +++++------------------------------- compact/src/Formatter.ts | 358 ++++++---------------- compact/src/runCompiler.ts | 110 +------ compact/src/runFormatter.ts | 105 +------ compact/src/types/errors.ts | 7 +- 6 files changed, 587 insertions(+), 972 deletions(-) create mode 100644 compact/src/BaseServices.ts diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts new file mode 100644 index 00000000..59cf4365 --- /dev/null +++ b/compact/src/BaseServices.ts @@ -0,0 +1,390 @@ +#!/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, join, relative } from 'node:path'; +import { promisify } from 'node:util'; +import chalk from 'chalk'; +import ora, { type Ora } from 'ora'; +import { + CompactCliNotFoundError, + DirectoryNotFoundError, + isPromisifiedChildProcessError, +} from './types/errors.ts'; + +/** Source directory containing .compact files */ +export const SRC_DIR: string = 'src'; +/** Output directory for compiled artifacts */ +export const ARTIFACTS_DIR: string = 'artifacts'; + +/** + * Function type for executing shell commands. + * Allows dependency injection for testing and customization. + */ +export type ExecFunction = ( + command: string, +) => Promise<{ stdout: string; stderr: string }>; + +/** + * Base environment validator that handles common CLI validation. + * Extended by specific validators for compilation and formatting. + */ +export abstract class BaseEnvironmentValidator { + protected execFn: ExecFunction; + + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Checks if the Compact CLI is available in the system PATH. + */ + async checkCompactAvailable(): Promise { + try { + await this.execFn('compact --version'); + return true; + } catch { + return false; + } + } + + /** + * Retrieves the version of the Compact developer tools. + */ + async getDevToolsVersion(): Promise { + const { stdout } = await this.execFn('compact --version'); + return stdout.trim(); + } + + /** + * Base validation that checks CLI availability. + * Override in subclasses for specific validation requirements. + */ + async validateBase(): Promise<{ devToolsVersion: 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(); + return { devToolsVersion }; + } + + /** + * Abstract method for specific validation logic. + * Must be implemented by subclasses. + */ + abstract validate(...args: any[]): Promise; +} + +/** + * Shared file discovery service for both compilation and formatting. + * Recursively scans directories and filters for .compact file extensions. + */ +export class FileDiscovery { + /** + * Recursively discovers all .compact files in a directory. + * Returns relative paths from the SRC_DIR for consistent processing. + */ + 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(SRC_DIR, fullPath)]; + } + return []; + } catch (err) { + console.warn(`Error accessing ${fullPath}:`, err); + return []; + } + }); + + const results = await Promise.all(filePromises); + return results.flat(); + } catch (err) { + console.error(`Failed to read dir: ${dir}`, err); + return []; + } + } +} + +/** + * Base service for executing Compact CLI commands. + * Provides common command execution patterns with error handling. + */ +export abstract class BaseCompactService { + protected execFn: ExecFunction; + + constructor(execFn: ExecFunction = promisify(execCallback)) { + this.execFn = execFn; + } + + /** + * Executes a compact command and handles common error patterns. + */ + protected async executeCompactCommand( + command: string, + errorContext: string, + ): Promise<{ stdout: string; stderr: string }> { + try { + return await this.execFn(command); + } catch (error: unknown) { + let message: string; + if (error instanceof Error) { + message = error.message; + } else { + message = String(error); + } + + throw this.createError(`${errorContext}: ${message}`, error); + } + } + + /** + * Abstract method for creating operation-specific errors. + * Must be implemented by subclasses. + */ + protected abstract createError(message: string, cause?: unknown): Error; +} + +/** + * Shared UI service for consistent styling across compiler and formatter. + * Provides common output formatting and user feedback patterns. + */ +export class SharedUIService { + /** + * Prints formatted output with consistent indentation and coloring. + */ + static 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 base environment information. + */ + static displayBaseEnvInfo( + operation: string, + devToolsVersion: string, + targetDir?: string, + ): void { + const spinner = ora(); + + if (targetDir) { + spinner.info(chalk.blue(`[${operation}] TARGET_DIR: ${targetDir}`)); + } + + spinner.info( + chalk.blue(`[${operation}] Compact developer tools: ${devToolsVersion}`) + ); + } + + /** + * Displays operation start message with file count. + */ + static showOperationStart( + operation: string, + action: string, + fileCount: number, + targetDir?: string, + ): void { + const searchLocation = targetDir ? ` in ${targetDir}/` : ''; + const spinner = ora(); + spinner.info( + chalk.blue( + `[${operation}] Found ${fileCount} .compact file(s) to ${action}${searchLocation}` + ) + ); + } + + /** + * Displays a warning when no .compact files are found. + */ + static showNoFiles(operation: string, targetDir?: string): void { + const searchLocation = targetDir ? `${targetDir}/` : ''; + const spinner = ora(); + spinner.warn( + chalk.yellow(`[${operation}] No .compact files found in ${searchLocation}.`) + ); + } + + /** + * Shows available directories when DirectoryNotFoundError occurs. + */ + static showAvailableDirectories(operation: string): void { + console.log(chalk.yellow('\nAvailable directories:')); + console.log( + chalk.yellow(` --dir access # ${operation} access control contracts`), + ); + console.log(chalk.yellow(` --dir archive # ${operation} archive contracts`)); + console.log(chalk.yellow(` --dir security # ${operation} security contracts`)); + console.log(chalk.yellow(` --dir token # ${operation} token contracts`)); + console.log(chalk.yellow(` --dir utils # ${operation} utility contracts`)); + } +} + +/** + * Base class for Compact operations (compilation, formatting). + * Provides common patterns for argument parsing, validation, and execution. + */ +export abstract class BaseCompactOperation { + protected readonly fileDiscovery: FileDiscovery; + protected readonly targetDir?: string; + + constructor(targetDir?: string) { + this.targetDir = targetDir; + this.fileDiscovery = new FileDiscovery(); + } + + /** + * Validates the target directory exists if specified. + */ + protected validateTargetDirectory(searchDir: string): void { + if (this.targetDir && !existsSync(searchDir)) { + throw new DirectoryNotFoundError( + `Target directory ${searchDir} does not exist`, + searchDir, + ); + } + } + + /** + * Gets the search directory based on target directory. + */ + protected getSearchDirectory(): string { + return this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + } + + /** + * Discovers files and handles empty results. + */ + protected async discoverFiles(): Promise<{ files: string[]; searchDir: string }> { + const searchDir = this.getSearchDirectory(); + this.validateTargetDirectory(searchDir); + + const files = await this.fileDiscovery.getCompactFiles(searchDir); + + if (files.length === 0) { + this.showNoFiles(); + return { files: [], searchDir }; + } + + return { files, searchDir }; + } + + /** + * Abstract methods that must be implemented by subclasses. + */ + abstract validateEnvironment(): Promise; + abstract execute(): Promise; + abstract showNoFiles(): void; + + /** + * Common argument parsing patterns. + */ + protected static parseBaseArgs(args: string[]): { + targetDir?: string; + remainingArgs: string[]; + } { + let targetDir: string | undefined; + const remainingArgs: string[] = []; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--dir') { + const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); + if (dirNameExists) { + targetDir = args[i + 1]; + i++; + } else { + throw new Error('--dir flag requires a directory name'); + } + } else { + remainingArgs.push(args[i]); + } + } + + return { targetDir, remainingArgs }; + } +} + +/** + * Base error handler for both compiler and formatter CLIs. + * Handles common error types with operation-specific context. + */ +export class BaseErrorHandler { + static handleCommonErrors( + error: unknown, + spinner: Ora, + operation: string, + ): boolean { + // CompactCliNotFoundError + if (error instanceof Error && error.name === 'CompactCliNotFoundError') { + spinner.fail(chalk.red(`[${operation}] Error: ${error.message}`)); + spinner.info( + chalk.blue( + `[${operation}] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, + ), + ); + return true; + } + + // DirectoryNotFoundError + if (error instanceof Error && error.name === 'DirectoryNotFoundError') { + spinner.fail(chalk.red(`[${operation}] Error: ${error.message}`)); + SharedUIService.showAvailableDirectories(operation); + return true; + } + + // Environment validation errors + if (isPromisifiedChildProcessError(error)) { + spinner.fail( + chalk.red(`[${operation}] Environment validation failed: ${error.message}`), + ); + console.log(chalk.gray('\nTroubleshooting:')); + console.log( + chalk.gray(' • Check that Compact CLI is installed and in PATH'), + ); + console.log(chalk.gray(' • Verify the specified Compact version exists')); + console.log(chalk.gray(' • Ensure you have proper permissions')); + return true; + } + + // Argument parsing errors + const errorMessage = error instanceof Error ? error.message : String(error); + if (errorMessage.includes('--dir flag requires a directory name')) { + spinner.fail( + chalk.red(`[${operation}] Error: --dir flag requires a directory name`), + ); + return false; // Let specific handler show usage + } + + return false; // Not handled, let specific handler deal with it + } + + static handleUnexpectedError( + error: unknown, + spinner: Ora, + operation: string, + ): void { + const errorMessage = error instanceof Error ? error.message : String(error); + spinner.fail(chalk.red(`[${operation}] Unexpected error: ${errorMessage}`)); + + console.log(chalk.gray('\nIf this error persists, please check:')); + console.log(chalk.gray(' • Compact CLI is installed and in PATH')); + console.log(chalk.gray(' • Source files exist and are readable')); + console.log(chalk.gray(' • File system permissions are correct')); + } +} diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index eaee0178..3b7c111d 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -1,108 +1,29 @@ #!/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, join, relative } from 'node:path'; -import { promisify } from 'node:util'; +import { basename, join } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import { - CompactCliNotFoundError, + BaseEnvironmentValidator, + BaseCompactService, + BaseCompactOperation, + SharedUIService, + SRC_DIR, + ARTIFACTS_DIR, + type ExecFunction, +} from './BaseServices.js'; +import { CompilationError, - DirectoryNotFoundError, isPromisifiedChildProcessError, } from './types/errors.ts'; -/** Source directory containing .compact files */ -const SRC_DIR: string = 'src'; -/** Output directory for compiled artifacts */ -const ARTIFACTS_DIR: string = '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 }>; - /** - * 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.24.0'); - * const version = await validator.getDevToolsVersion(); - * ``` + * Environment validator specific to compilation operations. + * Extends base validator with compilation-specific version checking. */ -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(); - } - +export class CompilerEnvironmentValidator extends BaseEnvironmentValidator { /** * 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.24.0'); - * console.log(`Toolchain: ${toolchainVersion}`); - * ``` */ async getToolchainVersion(version?: string): Promise { const versionFlag = version ? `+${version}` : ''; @@ -113,35 +34,13 @@ export class EnvironmentValidator { } /** - * 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.24.0'); - * console.log('Environment validated successfully'); - * } catch (error) { - * if (error instanceof CompactCliNotFoundError) { - * console.error('Please install Compact CLI'); - * } - * } - * ``` + * Validates environment for compilation with optional version. */ - 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(); + async validate(version?: string): Promise<{ + devToolsVersion: string; + toolchainVersion: string; + }> { + const { devToolsVersion } = await this.validateBase(); const toolchainVersion = await this.getToolchainVersion(version); return { devToolsVersion, toolchainVersion }; @@ -149,113 +48,12 @@ export class EnvironmentValidator { } /** - * 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(); - * const files = await discovery.getCompactFiles('src/security'); - * console.log(`Found ${files.length} .compact files`); - * ``` + * Service for executing compilation commands. + * Extends base service with compilation-specific command construction. */ -export class FileDiscovery { - /** - * Recursively discovers all .compact files in a directory. - * Returns relative paths from the SRC_DIR 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(SRC_DIR, 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 []; - } - } -} - -/** - * 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.24.0' - * ); - * console.log('Compilation output:', result.stdout); - * ``` - */ -export class CompilerService { - private execFn: ExecFunction; - - /** - * Creates a new CompilerService instance. - * - * @param execFn - Function to execute shell commands (defaults to promisified child_process.exec) - */ - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - +export class CompilerService extends BaseCompactService { /** * Compiles a single .compact file using the Compact CLI. - * Constructs the appropriate command with flags and version, then executes it. - * - * @param file - Relative path to the .compact file from SRC_DIR - * @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.24.0' - * ); - * console.log('Success:', result.stdout); - * } catch (error) { - * if (error instanceof CompilationError) { - * console.error('Compilation failed for', error.file); - * } - * } - * ``` */ async compileFile( file: string, @@ -269,71 +67,26 @@ export class CompilerService { 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. - } + return this.executeCompactCommand(command, `Failed to compile ${file}`); + } - throw new CompilationError(`Failed to compile ${file}: ${message}`, file); - } + protected createError(message: string, cause?: unknown): Error { + // Extract file name from error message for CompilationError + const match = message.match(/Failed to compile (.+?):/); + const file = match ? match[1] : 'unknown'; + return new CompilationError(message, file, cause); } } /** - * 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.24.0', 'security'); - * UIService.printOutput('Compilation successful', chalk.green); - * ``` + * UI service specific to compilation operations. + * Extends shared UI with compilation-specific formatting. */ -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'))); - }, +export const CompilerUIService = { + ...SharedUIService, /** - * 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.24.0', - * 'security', - * '0.24.0' - * ); - * ``` + * Displays compilation environment information. */ displayEnvInfo( devToolsVersion: string, @@ -341,17 +94,11 @@ export const UIService = { targetDir?: string, version?: string, ): void { - const spinner = ora(); - - if (targetDir) { - spinner.info(chalk.blue(`[COMPILE] TARGET_DIR: ${targetDir}`)); - } + SharedUIService.displayBaseEnvInfo('COMPILE', devToolsVersion, targetDir); + const spinner = ora(); spinner.info( - chalk.blue(`[COMPILE] Compact developer tools: ${devToolsVersion}`), - ); - spinner.info( - chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), + chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`) ); if (version) { @@ -360,111 +107,32 @@ export const UIService = { }, /** - * 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/" - * ``` + * Displays compilation start message. */ 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}`, - ), - ); + SharedUIService.showOperationStart('COMPILE', 'compile', fileCount, targetDir); }, /** - * 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/." - * ``` + * Displays no files warning for compilation. */ showNoFiles(targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; - const spinner = ora(); - spinner.warn( - chalk.yellow(`[COMPILE] No .compact files found in ${searchLocation}.`), - ); + SharedUIService.showNoFiles('COMPILE', targetDir); }, }; /** * 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 - * - * @class CompactCompiler - * @example - * ```typescript - * // Basic usage - * const compiler = new CompactCompiler('--skip-zk', 'security', '0.24.0'); - * await compiler.compile(); - * - * // Factory method usage - * const compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - * await compiler.compile(); - * - * // With environment variables - * process.env.SKIP_ZK = 'true'; - * const compiler = CompactCompiler.fromArgs(['--dir', 'token']); - * await compiler.compile(); - * ``` + * Extends base operation with compilation-specific logic. */ -export class CompactCompiler { - /** Environment validation service */ - private readonly environmentValidator: EnvironmentValidator; - /** File discovery service */ - private readonly fileDiscovery: FileDiscovery; - /** Compilation execution service */ +export class CompactCompiler extends BaseCompactOperation { + private readonly environmentValidator: CompilerEnvironmentValidator; private readonly compilerService: CompilerService; - - /** Compiler flags to pass to the Compact CLI */ private readonly flags: string; - /** Optional target directory to limit compilation scope */ - private readonly targetDir?: string; - /** Optional specific toolchain version to use */ private readonly version?: string; /** - * Creates a new CompactCompiler instance with specified configuration. - * - * @param flags - Space-separated compiler flags (e.g., '--skip-zk --verbose') - * @param targetDir - Optional subdirectory within src/ to compile (e.g., 'security', 'token') - * @param version - Optional toolchain version to use (e.g., '0.24.0') - * @param execFn - Optional custom exec function for dependency injection - * @example - * ```typescript - * // Compile all files with flags - * const compiler = new CompactCompiler('--skip-zk --verbose'); - * - * // Compile specific directory - * const compiler = new CompactCompiler('', 'security'); - * - * // Compile with specific version - * const compiler = new CompactCompiler('--skip-zk', undefined, '0.24.0'); - * - * // For testing with custom exec function - * const mockExec = vi.fn(); - * const compiler = new CompactCompiler('', undefined, undefined, mockExec); - * ``` + * Creates a new CompactCompiler instance. */ constructor( flags = '', @@ -472,52 +140,22 @@ export class CompactCompiler { version?: string, execFn?: ExecFunction, ) { + super(targetDir); this.flags = flags.trim(); - this.targetDir = targetDir; this.version = version; - this.environmentValidator = new EnvironmentValidator(execFn); - this.fileDiscovery = new FileDiscovery(); + this.environmentValidator = new CompilerEnvironmentValidator(execFn); this.compilerService = new CompilerService(execFn); } /** * 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 directory - * - `+` - 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 flag is provided without a directory name - * @example - * ```typescript - * // Parse command line: compact-compiler --dir security --skip-zk +0.24.0 - * const compiler = CompactCompiler.fromArgs([ - * '--dir', 'security', - * '--skip-zk', - * '+0.24.0' - * ]); - * - * // 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 { - let targetDir: string | undefined; + const { targetDir, remainingArgs } = this.parseBaseArgs(args); + const flags: string[] = []; let version: string | undefined; @@ -525,22 +163,13 @@ export class CompactCompiler { flags.push('--skip-zk'); } - for (let i = 0; i < args.length; i++) { - if (args[i] === '--dir') { - const dirNameExists = - i + 1 < args.length && !args[i + 1].startsWith('--'); - if (dirNameExists) { - targetDir = args[i + 1]; - i++; - } else { - throw new Error('--dir flag requires a directory name'); - } - } else if (args[i].startsWith('+')) { - version = args[i].slice(1); + for (const arg of remainingArgs) { + if (arg.startsWith('+')) { + version = arg.slice(1); } else { // Only add flag if it's not already present - if (!flags.includes(args[i])) { - flags.push(args[i]); + if (!flags.includes(arg)) { + flags.push(arg); } } } @@ -549,33 +178,13 @@ export class CompactCompiler { } /** - * 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'); - * } - * } - * ``` + * Validates the compilation environment. */ async validateEnvironment(): Promise { const { devToolsVersion, toolchainVersion } = await this.environmentValidator.validate(this.version); - UIService.displayEnvInfo( + + CompilerUIService.displayEnvInfo( devToolsVersion, toolchainVersion, this.targetDir, @@ -584,69 +193,37 @@ export class CompactCompiler { } /** - * 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}`); - * } - * } - * ``` + * Shows no files warning for compilation. */ - async compile(): Promise { - await this.validateEnvironment(); + showNoFiles(): void { + CompilerUIService.showNoFiles(this.targetDir); + } - const searchDir = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + /** + * Main compilation execution method. + */ + async execute(): Promise { + await this.validateEnvironment(); - // Validate target directory exists - if (this.targetDir && !existsSync(searchDir)) { - throw new DirectoryNotFoundError( - `Target directory ${searchDir} does not exist`, - searchDir, - ); - } + const { files } = await this.discoverFiles(); + if (files.length === 0) return; - const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); + CompilerUIService.showCompilationStart(files.length, this.targetDir); - if (compactFiles.length === 0) { - UIService.showNoFiles(this.targetDir); - return; + for (const [index, file] of files.entries()) { + await this.compileFile(file, index, files.length); } + } - UIService.showCompilationStart(compactFiles.length, this.targetDir); - - for (const [index, file] of compactFiles.entries()) { - await this.compileFile(file, index, compactFiles.length); - } + /** + * Legacy method name for backwards compatibility. + */ + async compile(): Promise { + return this.execute(); } /** - * 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 + * Compiles a single file with progress reporting. */ private async compileFile( file: string, @@ -666,8 +243,8 @@ export class CompactCompiler { ); spinner.succeed(chalk.green(`[COMPILE] ${step} Compiled ${file}`)); - UIService.printOutput(result.stdout, chalk.cyan); - UIService.printOutput(result.stderr, chalk.yellow); + SharedUIService.printOutput(result.stdout, chalk.cyan); + SharedUIService.printOutput(result.stderr, chalk.yellow); } catch (error) { spinner.fail(chalk.red(`[COMPILE] ${step} Failed ${file}`)); @@ -676,8 +253,8 @@ export class CompactCompiler { isPromisifiedChildProcessError(error.cause) ) { const execError = error.cause; - UIService.printOutput(execError.stdout, chalk.cyan); - UIService.printOutput(execError.stderr, chalk.red); + SharedUIService.printOutput(execError.stdout, chalk.cyan); + SharedUIService.printOutput(execError.stderr, chalk.red); } throw error; @@ -685,7 +262,7 @@ export class CompactCompiler { } /** - * For testing + * For testing - expose internal state */ get testFlags(): string { return this.flags; diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index ed40485f..7d620b87 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -1,68 +1,29 @@ #!/usr/bin/env node -import { exec as execCallback } from 'node:child_process'; -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; -import { join, relative } from 'node:path'; -import { promisify } from 'node:util'; +import { join } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import { - CompactCliNotFoundError, + BaseEnvironmentValidator, + BaseCompactService, + BaseCompactOperation, + SharedUIService, + SRC_DIR, + type ExecFunction, +} from './BaseServices.js'; +import { FormatterError, FormatterNotAvailableError, - DirectoryNotFoundError, isPromisifiedChildProcessError, } from './types/errors.ts'; -/** Source directory containing .compact files */ -const SRC_DIR: string = 'src'; - -/** - * Function type for executing shell commands. - * Allows dependency injection for testing and customization. - */ -export type ExecFunction = ( - command: string, -) => Promise<{ stdout: string; stderr: string }>; - /** - * Service responsible for validating the Compact CLI environment for formatting. - * Checks CLI availability, formatter availability, and ensures the toolchain - * supports formatting operations. - * - * @class FormatterEnvironmentValidator + * Environment validator specific to formatting operations. + * Extends base validator with formatter availability checking. */ -export class FormatterEnvironmentValidator { - private execFn: ExecFunction; - - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - - /** - * Checks if the Compact CLI is available in the system PATH. - */ - async checkCompactAvailable(): Promise { - try { - await this.execFn('compact --version'); - return true; - } catch { - return false; - } - } - - /** - * Retrieves the version of the Compact developer tools. - */ - async getDevToolsVersion(): Promise { - const { stdout } = await this.execFn('compact --version'); - return stdout.trim(); - } - +export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { /** * Checks if the formatter is available (requires compiler 0.25.0+). - * @throws {FormatterNotAvailableError} If formatter is not available */ async checkFormatterAvailable(): Promise { try { @@ -79,110 +40,42 @@ export class FormatterEnvironmentValidator { } /** - * Validates the entire Compact environment for formatting operations. - * @throws {CompactCliNotFoundError} If the Compact CLI is not available - * @throws {FormatterNotAvailableError} If formatter is not available + * Validates environment for formatting operations. */ async validate(): Promise<{ devToolsVersion: 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.validateBase(); await this.checkFormatterAvailable(); - const devToolsVersion = await this.getDevToolsVersion(); - return { devToolsVersion }; } } /** - * Service responsible for discovering .compact files for formatting operations. - * Reuses the same file discovery logic as the compiler. - * - * @class FormatterFileDiscovery + * Service for executing formatting commands. + * Extends base service with format-specific command construction. */ -export class FormatterFileDiscovery { - /** - * Recursively discovers all .compact files in a directory. - */ - 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(SRC_DIR, fullPath)]; - } - return []; - } catch (err) { - console.warn(`Error accessing ${fullPath}:`, err); - return []; - } - }); - - const results = await Promise.all(filePromises); - return results.flat(); - } catch (err) { - console.error(`Failed to read dir: ${dir}`, err); - return []; - } - } -} - -/** - * Service responsible for executing format operations using the Compact CLI. - * Handles different formatting modes: format, check, and specific file/directory targeting. - * - * @class FormatterService - */ -export class FormatterService { - private execFn: ExecFunction; - - constructor(execFn: ExecFunction = promisify(execCallback)) { - this.execFn = execFn; - } - +export class FormatterService extends BaseCompactService { /** * Formats files in-place in the specified directory or current directory. - * @param targetPath - Optional directory or file path to format */ async formatInPlace(targetPath?: string): Promise<{ stdout: string; stderr: string }> { const pathArg = targetPath ? ` "${targetPath}"` : ''; const command = `compact format${pathArg}`; - - try { - return await this.execFn(command); - } catch (error: unknown) { - let message: string; - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); - } - - throw new FormatterError(`Failed to format: ${message}`, targetPath); - } + return this.executeCompactCommand(command, 'Failed to format'); } /** * Checks if files are properly formatted without modifying them. - * @param targetPath - Optional directory or file path to check - * @returns Promise with check results including any formatting differences */ - async checkFormatting(targetPath?: string): Promise<{ stdout: string; stderr: string; isFormatted: boolean }> { + async checkFormatting(targetPath?: string): Promise<{ + stdout: string; + stderr: string; + isFormatted: boolean; + }> { const pathArg = targetPath ? ` "${targetPath}"` : ''; const command = `compact format --check${pathArg}`; try { - const result = await this.execFn(command); + const result = await this.executeCompactCommand(command, 'Failed to check formatting'); return { ...result, isFormatted: true }; } catch (error: unknown) { if (isPromisifiedChildProcessError(error)) { @@ -195,21 +88,12 @@ export class FormatterService { }; } } - - let message: string; - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); - } - - throw new FormatterError(`Failed to check formatting: ${message}`, targetPath); + throw error; } } /** * Formats a list of specific files. - * @param files - Array of file paths to format */ async formatFiles(files: string[]): Promise<{ stdout: string; stderr: string }> { if (files.length === 0) { @@ -218,78 +102,44 @@ export class FormatterService { const fileArgs = files.map(file => `"${join(SRC_DIR, file)}"`).join(' '); const command = `compact format ${fileArgs}`; + return this.executeCompactCommand(command, `Failed to format files: ${files.join(', ')}`); + } - try { - return await this.execFn(command); - } catch (error: unknown) { - let message: string; - if (error instanceof Error) { - message = error.message; - } else { - message = String(error); - } - - throw new FormatterError(`Failed to format files: ${message}`, files.join(', ')); - } + protected createError(message: string, cause?: unknown): Error { + // Extract target from error message for FormatterError + const match = message.match(/Failed to format(?: files:)? (.+)/); + const target = match ? match[1] : undefined; + return new FormatterError(message, target, cause); } } /** - * Utility service for handling formatter UI output and formatting. - * Provides consistent styling and formatting for formatter messages and output. - * - * @class FormatterUIService + * UI service specific to formatting operations. + * Extends shared UI with format-specific messaging. */ export const FormatterUIService = { - /** - * Prints formatted output with consistent indentation and coloring. - */ - 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'))); - }, + ...SharedUIService, /** - * Displays environment information for formatting operations. + * Displays formatting environment information. */ displayEnvInfo(devToolsVersion: string, targetDir?: string): void { - const spinner = ora(); - - if (targetDir) { - spinner.info(chalk.blue(`[FORMAT] TARGET_DIR: ${targetDir}`)); - } - - spinner.info( - chalk.blue(`[FORMAT] Compact developer tools: ${devToolsVersion}`) - ); + SharedUIService.displayBaseEnvInfo('FORMAT', devToolsVersion, targetDir); }, /** - * Displays formatting start message with file count and optional location. + * Displays formatting start message. */ showFormattingStart(fileCount: number, mode: 'format' | 'check', targetDir?: string): void { - const searchLocation = targetDir ? ` in ${targetDir}/` : ''; const action = mode === 'check' ? 'check formatting for' : 'format'; - const spinner = ora(); - spinner.info( - chalk.blue( - `[FORMAT] Found ${fileCount} .compact file(s) to ${action}${searchLocation}` - ) - ); + SharedUIService.showOperationStart('FORMAT', action, fileCount, targetDir); }, /** - * Displays a warning message when no .compact files are found. + * Displays no files warning for formatting. */ showNoFiles(targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; - const spinner = ora(); - spinner.warn( - chalk.yellow(`[FORMAT] No .compact files found in ${searchLocation}.`) - ); + SharedUIService.showNoFiles('FORMAT', targetDir); }, /** @@ -304,7 +154,7 @@ export const FormatterUIService = { spinner.fail(chalk.red('[FORMAT] Some files are not properly formatted')); if (differences) { console.log(chalk.yellow('\nFormatting differences:')); - this.printOutput(differences, chalk.white); + SharedUIService.printOutput(differences, chalk.white); } } }, @@ -312,90 +162,78 @@ export const FormatterUIService = { /** * Main formatter class that orchestrates the formatting process. - * Coordinates environment validation, file discovery, and formatting services - * to provide a complete .compact file formatting solution. - * - * @class CompactFormatter + * Extends base operation with formatting-specific logic. */ -export class CompactFormatter { - /** Environment validation service */ +export class CompactFormatter extends BaseCompactOperation { private readonly environmentValidator: FormatterEnvironmentValidator; - /** File discovery service */ - private readonly fileDiscovery: FormatterFileDiscovery; - /** Formatting execution service */ private readonly formatterService: FormatterService; - - /** Whether to check formatting instead of formatting in-place */ private readonly checkMode: boolean; - /** Optional target directory or files to limit formatting scope */ private readonly targets: string[]; /** - * Creates a new CompactFormatter instance with specified configuration. - * - * @param checkMode - Whether to check formatting instead of formatting in-place - * @param targets - Optional array of directories or files to format - * @param execFn - Optional custom exec function for dependency injection + * Creates a new CompactFormatter instance. */ constructor( checkMode = false, targets: string[] = [], execFn?: ExecFunction, ) { + // For single directory target, use it as targetDir + const targetDir = targets.length === 1 && !targets[0].endsWith('.compact') + ? targets[0] + : undefined; + + super(targetDir); this.checkMode = checkMode; this.targets = targets; this.environmentValidator = new FormatterEnvironmentValidator(execFn); - this.fileDiscovery = new FormatterFileDiscovery(); this.formatterService = new FormatterService(execFn); } /** * Factory method to create a CompactFormatter from command-line arguments. - * - * Supported argument patterns: - * - `--check` - Check formatting without modifying files - * - `--dir ` - Target specific directory - * - ` ...` - Target specific files - * - * @param args - Array of command-line arguments - * @returns New CompactFormatter instance configured from arguments */ static fromArgs(args: string[]): CompactFormatter { + const { targetDir, remainingArgs } = this.parseBaseArgs(args); + let checkMode = false; const targets: string[] = []; - for (let i = 0; i < args.length; i++) { - if (args[i] === '--check') { + // Add targetDir to targets if specified + if (targetDir) { + targets.push(targetDir); + } + + for (const arg of remainingArgs) { + if (arg === '--check') { checkMode = true; - } else if (args[i] === '--dir') { - const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); - if (dirNameExists) { - targets.push(args[i + 1]); - i++; - } else { - throw new Error('--dir flag requires a directory name'); - } - } else if (!args[i].startsWith('--')) { - targets.push(args[i]); + } else if (!arg.startsWith('--')) { + targets.push(arg); } } - return new CompactFormatter(checkMode, targets, undefined); + return new CompactFormatter(checkMode, targets); } /** - * Validates the formatting environment and displays version information. + * Validates the formatting environment. */ async validateEnvironment(): Promise { const { devToolsVersion } = await this.environmentValidator.validate(); - const targetDir = this.targets.length === 1 ? this.targets[0] : undefined; - FormatterUIService.displayEnvInfo(devToolsVersion, targetDir); + FormatterUIService.displayEnvInfo(devToolsVersion, this.targetDir); } /** - * Main formatting method that orchestrates the entire formatting process. + * Shows no files warning for formatting. */ - async format(): Promise { + showNoFiles(): void { + FormatterUIService.showNoFiles(this.targetDir); + } + + /** + * Main formatting execution method. + */ + async execute(): Promise { await this.validateEnvironment(); // Handle specific file targets @@ -404,8 +242,14 @@ export class CompactFormatter { } // Handle directory target or current directory - const targetDir = this.targets.length === 1 ? this.targets[0] : undefined; - return this.formatDirectory(targetDir); + return this.formatDirectory(); + } + + /** + * Legacy method name for backwards compatibility. + */ + async format(): Promise { + return this.execute(); } /** @@ -413,41 +257,25 @@ export class CompactFormatter { */ private async formatSpecificFiles(): Promise { if (this.checkMode) { - // For check mode with specific files, we need to check each file for (const file of this.targets) { await this.checkFile(file); } } else { - // Format the specific files const result = await this.formatterService.formatFiles(this.targets); - FormatterUIService.printOutput(result.stdout, chalk.cyan); - FormatterUIService.printOutput(result.stderr, chalk.yellow); + SharedUIService.printOutput(result.stdout, chalk.cyan); + SharedUIService.printOutput(result.stderr, chalk.yellow); } } /** * Formats all files in a directory or current directory. */ - private async formatDirectory(targetDir?: string): Promise { - const searchDir = targetDir ? join(SRC_DIR, targetDir) : SRC_DIR; - - // Validate target directory exists - if (targetDir && !existsSync(searchDir)) { - throw new DirectoryNotFoundError( - `Target directory ${searchDir} does not exist`, - searchDir, - ); - } - - const compactFiles = await this.fileDiscovery.getCompactFiles(searchDir); - - if (compactFiles.length === 0) { - FormatterUIService.showNoFiles(targetDir); - return; - } + private async formatDirectory(): Promise { + const { files, searchDir } = await this.discoverFiles(); + if (files.length === 0) return; const mode = this.checkMode ? 'check' : 'format'; - FormatterUIService.showFormattingStart(compactFiles.length, mode, targetDir); + FormatterUIService.showFormattingStart(files.length, mode, this.targetDir); if (this.checkMode) { const result = await this.formatterService.checkFormatting(searchDir); @@ -457,14 +285,14 @@ export class CompactFormatter { // Successful formatting typically produces no output if (result.stdout.trim()) { - FormatterUIService.printOutput(result.stdout, chalk.cyan); + SharedUIService.printOutput(result.stdout, chalk.cyan); } if (result.stderr.trim()) { - FormatterUIService.printOutput(result.stderr, chalk.yellow); + SharedUIService.printOutput(result.stderr, chalk.yellow); } const spinner = ora(); - spinner.succeed(chalk.green(`[FORMAT] Successfully formatted ${compactFiles.length} file(s)`)); + spinner.succeed(chalk.green(`[FORMAT] Successfully formatted ${files.length} file(s)`)); } } @@ -473,7 +301,7 @@ export class CompactFormatter { */ private async checkFile(file: string): Promise { const result = await this.formatterService.checkFormatting(file); - + if (result.isFormatted) { const spinner = ora(); spinner.succeed(chalk.green(`[FORMAT] ${file} is properly formatted`)); @@ -481,7 +309,7 @@ export class CompactFormatter { const spinner = ora(); spinner.fail(chalk.red(`[FORMAT] ${file} is not properly formatted`)); if (result.stdout) { - FormatterUIService.printOutput(result.stdout, chalk.white); + SharedUIService.printOutput(result.stdout, chalk.white); } } } diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index ce3cdc6c..1b07b158 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import ora, { type Ora } from 'ora'; import { CompactCompiler } from './Compiler.js'; +import { BaseErrorHandler } from './BaseServices.js'; import { type CompilationError, isPromisifiedChildProcessError, @@ -10,38 +11,6 @@ import { /** * Executes the Compact compiler CLI with improved error handling and user feedback. - * - * Error Handling Architecture: - * - * This CLI follows a layered error handling approach: - * - * - Business logic (Compiler.ts) throws structured errors with context. - * - CLI layer (runCompiler.ts) handles all user-facing error presentation. - * - Custom error types (types/errors.ts) provide semantic meaning and context. - * - * Benefits: Better testability, consistent UI, separation of concerns. - * - * Note: This compiler uses fail-fast error handling. - * Compilation stops on the first error encountered. - * This provides immediate feedback but doesn't attempt to compile remaining files after a failure. - * - * @example Individual module compilation - * ```bash - * npx compact-compiler --dir security --skip-zk - * turbo compact:access -- --skip-zk - * turbo compact:security -- --skip-zk --other-flag - * ``` - * - * @example Full compilation with environment variables - * ```bash - * SKIP_ZK=true turbo compact - * turbo compact - * ``` - * - * @example Version specification - * ```bash - * npx compact-compiler --dir security --skip-zk +0.24.0 - * ``` */ async function runCompiler(): Promise { const spinner = ora(chalk.blue('[COMPILE] Compact compiler started')).info(); @@ -57,43 +26,16 @@ async function runCompiler(): Promise { } /** - * Centralized error handling with specific error types and user-friendly messages. - * - * Handles different error types with appropriate user feedback: - * - * - `CompactCliNotFoundError`: Shows installation instructions. - * - `DirectoryNotFoundError`: Shows available directories. - * - `CompilationError`: Shows file-specific error details with context. - * - Environment validation errors: Shows troubleshooting tips. - * - Argument parsing errors: Shows usage help. - * - Generic errors: Shows general troubleshooting guidance. - * - * @param error - The error that occurred during compilation - * @param spinner - Ora spinner instance for consistent UI messaging + * Centralized error handling with compiler-specific error types. */ function handleError(error: unknown, spinner: Ora): void { - // CompactCliNotFoundError - if (error instanceof Error && error.name === 'CompactCliNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - spinner.info( - chalk.blue( - `[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, - ), - ); - return; - } - - // DirectoryNotFoundError - if (error instanceof Error && error.name === 'DirectoryNotFoundError') { - spinner.fail(chalk.red(`[COMPILE] Error: ${error.message}`)); - showAvailableDirectories(); + // Try common error handling first + if (BaseErrorHandler.handleCommonErrors(error, spinner, 'COMPILE')) { return; } - // CompilationError + // CompilationError - specific to compilation if (error instanceof Error && error.name === 'CompilationError') { - // The compilation error details (file name, stdout/stderr) are already displayed - // by `compileFile`; therefore, this just handles the final err state const compilationError = error as CompilationError; spinner.fail( chalk.red( @@ -116,55 +58,19 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // Env validation errors (non-CLI errors) - if (isPromisifiedChildProcessError(error)) { - spinner.fail( - chalk.red(`[COMPILE] Environment validation failed: ${error.message}`), - ); - console.log(chalk.gray('\nTroubleshooting:')); - console.log( - chalk.gray(' • Check that Compact CLI is installed and in PATH'), - ); - console.log(chalk.gray(' • Verify the specified Compact version exists')); - console.log(chalk.gray(' • Ensure you have proper permissions')); - return; - } - - // Arg parsing + // Argument parsing specific to compilation 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'), - ); showUsageHelp(); return; } // Unexpected errors - spinner.fail(chalk.red(`[COMPILE] Unexpected error: ${errorMessage}`)); - console.log(chalk.gray('\nIf this error persists, please check:')); - console.log(chalk.gray(' • Compact CLI is installed and in PATH')); - console.log(chalk.gray(' • Source files exist and are readable')); - console.log(chalk.gray(' • Specified Compact version exists')); - console.log(chalk.gray(' • File system permissions are correct')); -} - -/** - * Shows available directories when `DirectoryNotFoundError` occurs. - */ -function showAvailableDirectories(): void { - console.log(chalk.yellow('\nAvailable directories:')); - console.log( - chalk.yellow(' --dir access # Compile access control contracts'), - ); - console.log(chalk.yellow(' --dir archive # Compile archive contracts')); - console.log(chalk.yellow(' --dir security # Compile security contracts')); - console.log(chalk.yellow(' --dir token # Compile token contracts')); - console.log(chalk.yellow(' --dir utils # Compile utility contracts')); + BaseErrorHandler.handleUnexpectedError(error, spinner, 'COMPILE'); } /** - * Shows usage help with examples for different scenarios. + * Shows usage help with examples for compilation scenarios. */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-compiler [options]')); diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts index 7ac80dd5..1302a320 100644 --- a/compact/src/runFormatter.ts +++ b/compact/src/runFormatter.ts @@ -3,6 +3,7 @@ import chalk from 'chalk'; import ora, { type Ora } from 'ora'; import { CompactFormatter } from './Formatter.js'; +import { BaseErrorHandler } from './BaseServices.js'; import { type FormatterError, isPromisifiedChildProcessError, @@ -10,27 +11,6 @@ import { /** * Executes the Compact formatter CLI with improved error handling and user feedback. - * - * This CLI provides formatting capabilities for .compact files using the Compact developer tools. - * It supports both formatting in-place and checking formatting without modifications. - * - * @example Directory formatting - * ```bash - * npx compact-formatter --dir security - * npx compact-formatter --dir token --check - * ``` - * - * @example Specific file formatting - * ```bash - * npx compact-formatter src/contracts/Token.compact src/utils/Helper.compact - * npx compact-formatter --check src/contracts/Token.compact - * ``` - * - * @example Full project formatting - * ```bash - * npx compact-formatter - * npx compact-formatter --check - * ``` */ async function runFormatter(): Promise { const spinner = ora(chalk.blue('[FORMAT] Compact formatter started')).info(); @@ -46,34 +26,15 @@ async function runFormatter(): Promise { } /** - * Centralized error handling with specific error types and user-friendly messages. - * - * Handles different error types with appropriate user feedback: - * - * - `CompactCliNotFoundError`: Shows installation instructions. - * - `FormatterNotAvailableError`: Shows update instructions for formatter support. - * - `DirectoryNotFoundError`: Shows available directories. - * - `FormatterError`: Shows formatting-specific error details. - * - Environment validation errors: Shows troubleshooting tips. - * - Argument parsing errors: Shows usage help. - * - Generic errors: Shows general troubleshooting guidance. - * - * @param error - The error that occurred during formatting - * @param spinner - Ora spinner instance for consistent UI messaging + * Centralized error handling with formatter-specific error types. */ function handleError(error: unknown, spinner: Ora): void { - // CompactCliNotFoundError - if (error instanceof Error && error.name === 'CompactCliNotFoundError') { - spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); - spinner.info( - chalk.blue( - `[FORMAT] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh`, - ), - ); + // Try common error handling first + if (BaseErrorHandler.handleCommonErrors(error, spinner, 'FORMAT')) { return; } - // FormatterNotAvailableError + // FormatterNotAvailableError - specific to formatting if (error instanceof Error && error.name === 'FormatterNotAvailableError') { spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); spinner.info( @@ -85,14 +46,7 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // DirectoryNotFoundError - if (error instanceof Error && error.name === 'DirectoryNotFoundError') { - spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); - showAvailableDirectories(); - return; - } - - // FormatterError + // FormatterError - specific to formatting if (error instanceof Error && error.name === 'FormatterError') { const formatterError = error as FormatterError; spinner.fail( @@ -116,60 +70,19 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // Environment validation errors (non-CLI errors) - if (isPromisifiedChildProcessError(error)) { - spinner.fail( - chalk.red(`[FORMAT] Environment validation failed: ${error.message}`), - ); - console.log(chalk.gray('\nTroubleshooting:')); - console.log( - chalk.gray(' • Check that Compact CLI is installed and in PATH'), - ); - console.log( - chalk.gray(' • Update compiler with: compact update'), - ); - console.log( - chalk.gray(' • Update dev tools with: compact self update'), - ); - console.log(chalk.gray(' • Ensure you have proper permissions')); - return; - } - - // Argument parsing + // Argument parsing specific to formatting const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('--dir flag requires a directory name')) { - spinner.fail( - chalk.red('[FORMAT] Error: --dir flag requires a directory name'), - ); showUsageHelp(); return; } // Unexpected errors - spinner.fail(chalk.red(`[FORMAT] Unexpected error: ${errorMessage}`)); - console.log(chalk.gray('\nIf this error persists, please check:')); - console.log(chalk.gray(' • Compact CLI is installed and in PATH')); - console.log(chalk.gray(' • Compact compiler is updated (compact update)')); - console.log(chalk.gray(' • Source files exist and are readable')); - console.log(chalk.gray(' • File system permissions are correct')); -} - -/** - * Shows available directories when `DirectoryNotFoundError` occurs. - */ -function showAvailableDirectories(): void { - console.log(chalk.yellow('\nAvailable directories:')); - console.log( - chalk.yellow(' --dir access # Format access control contracts'), - ); - console.log(chalk.yellow(' --dir archive # Format archive contracts')); - console.log(chalk.yellow(' --dir security # Format security contracts')); - console.log(chalk.yellow(' --dir token # Format token contracts')); - console.log(chalk.yellow(' --dir utils # Format utility contracts')); + BaseErrorHandler.handleUnexpectedError(error, spinner, 'FORMAT'); } /** - * Shows usage help with examples for different scenarios. + * Shows usage help with examples for formatting scenarios. */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); diff --git a/compact/src/types/errors.ts b/compact/src/types/errors.ts index 70a50caa..3b1577c8 100644 --- a/compact/src/types/errors.ts +++ b/compact/src/types/errors.ts @@ -62,17 +62,18 @@ export class CompactCliNotFoundError extends Error { */ export class CompilationError extends Error { public readonly file?: string; - + public readonly cause?: unknown; /** * Creates a new CompilationError instance. * * @param message - Error message describing the compilation failure * @param file - Optional relative path to the file that failed to compile */ - constructor(message: string, file?: string) { + constructor(message: string, file: string, cause?: unknown) { super(message); - this.file = file; this.name = 'CompilationError'; + this.file = file; + this.cause = cause; } } From 6074183703a53c9088f44d0391d2ad5fbbf000e9 Mon Sep 17 00:00:00 2001 From: andrew Date: Mon, 8 Sep 2025 02:07:15 -0300 Subject: [PATCH 10/44] fix import, remove traling space --- compact/src/BaseServices.ts | 2 +- compact/src/Formatter.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts index 59cf4365..1d06160a 100644 --- a/compact/src/BaseServices.ts +++ b/compact/src/BaseServices.ts @@ -3,7 +3,7 @@ import { exec as execCallback } from 'node:child_process'; import { existsSync } from 'node:fs'; import { readdir } from 'node:fs/promises'; -import { basename, join, relative } from 'node:path'; +import { join, relative } from 'node:path'; import { promisify } from 'node:util'; import chalk from 'chalk'; import ora, { type Ora } from 'ora'; diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 7d620b87..891ed326 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -301,7 +301,7 @@ export class CompactFormatter extends BaseCompactOperation { */ private async checkFile(file: string): Promise { const result = await this.formatterService.checkFormatting(file); - + if (result.isFormatted) { const spinner = ora(); spinner.succeed(chalk.green(`[FORMAT] ${file} is properly formatted`)); From f8641534a58e8db59688f8ea6e22db8d26595374 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 18:55:12 -0300 Subject: [PATCH 11/44] fix output bug --- compact/src/BaseServices.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts index 1d06160a..27b875fc 100644 --- a/compact/src/BaseServices.ts +++ b/compact/src/BaseServices.ts @@ -214,7 +214,7 @@ export class SharedUIService { * Displays a warning when no .compact files are found. */ static showNoFiles(operation: string, targetDir?: string): void { - const searchLocation = targetDir ? `${targetDir}/` : ''; + const searchLocation = targetDir ? `${targetDir}/` : 'src/'; const spinner = ora(); spinner.warn( chalk.yellow(`[${operation}] No .compact files found in ${searchLocation}.`) From de1f426c06c5bf66457795200b5affd4fce3c7d2 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 20:30:02 -0300 Subject: [PATCH 12/44] add BaseServices tests --- compact/test/BaseServices.test.ts | 555 ++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 compact/test/BaseServices.test.ts diff --git a/compact/test/BaseServices.test.ts b/compact/test/BaseServices.test.ts new file mode 100644 index 00000000..414052a4 --- /dev/null +++ b/compact/test/BaseServices.test.ts @@ -0,0 +1,555 @@ +import { describe, test, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { join } from 'node:path'; +import chalk from 'chalk'; +import ora from 'ora'; +import { + BaseEnvironmentValidator, + FileDiscovery, + BaseCompactService, + SharedUIService, + BaseCompactOperation, + BaseErrorHandler, + SRC_DIR, + type ExecFunction, +} from '../src/BaseServices.js'; +import { + CompactCliNotFoundError, + DirectoryNotFoundError, +} from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('node:fs', () => ({ + existsSync: vi.fn(), +})); + +vi.mock('node:fs/promises', () => ({ + readdir: vi.fn(), +})); + +vi.mock('chalk', () => ({ + default: { + blue: vi.fn((text) => text), + yellow: vi.fn((text) => text), + red: vi.fn((text) => text), + gray: vi.fn((text) => text), + }, +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + info: vi.fn(), + warn: vi.fn(), + fail: vi.fn(), + })), +})); + +// Mock spinner +const mockSpinner = { + start: () => ({ succeed: vi.fn(), fail: vi.fn(), text: '' }), + info: vi.fn(), + warn: vi.fn(), + fail: vi.fn(), + succeed: vi.fn(), +}; + +// Concrete implementations for testing abstract classes +class TestEnvironmentValidator extends BaseEnvironmentValidator { + async validate(): Promise<{ devToolsVersion: string }> { + return this.validateBase(); + } +} + +class TestCompactService extends BaseCompactService { + async testCommand(command: string): Promise<{ stdout: string; stderr: string }> { + return this.executeCompactCommand(command, 'Test operation failed'); + } + + protected createError(message: string, cause?: unknown): Error { + return new Error(message); + } +} + +class TestCompactOperation extends BaseCompactOperation { + async validateEnvironment(): Promise { + // Test implementation + } + + async execute(): Promise { + const { files } = await this.discoverFiles(); + return Promise.resolve(); + } + + showNoFiles(): void { + SharedUIService.showNoFiles('TEST', this.targetDir); + } +} + +describe('BaseEnvironmentValidator', () => { + let validator: TestEnvironmentValidator; + let mockExec: Mock; + + beforeEach(() => { + mockExec = vi.fn(); + validator = new TestEnvironmentValidator(mockExec); + }); + + describe('checkCompactAvailable', () => { + it('returns true when compact CLI is available', async () => { + mockExec.mockResolvedValue({ stdout: 'compact 0.2.0', stderr: '' }); + + const result = await validator.checkCompactAvailable(); + + expect(result).toBe(true); + expect(mockExec).toHaveBeenCalledWith('compact --version'); + }); + + it('returns false when compact CLI is not available', async () => { + mockExec.mockRejectedValue(new Error('Command not found')); + + const result = await validator.checkCompactAvailable(); + + expect(result).toBe(false); + }); + }); + + describe('getDevToolsVersion', () => { + it('returns trimmed version string', async () => { + mockExec.mockResolvedValue({ stdout: ' compact 0.2.0 \n', stderr: '' }); + + const version = await validator.getDevToolsVersion(); + + expect(version).toBe('compact 0.2.0'); + }); + }); + + describe('validateBase', () => { + it('returns version when CLI is available', async () => { + mockExec.mockResolvedValue({ stdout: 'compact 0.2.0', stderr: '' }); + + const result = await validator.validateBase(); + + expect(result).toEqual({ devToolsVersion: 'compact 0.2.0' }); + }); + + it('throws CompactCliNotFoundError when CLI is not available', async () => { + mockExec.mockRejectedValue(new Error('Command not found')); + + await expect(validator.validateBase()).rejects.toThrow(CompactCliNotFoundError); + }); + }); +}); + +describe('FileDiscovery', () => { + let fileDiscovery: FileDiscovery; + let mockReaddir: Mock; + + beforeEach( async() => { + fileDiscovery = new FileDiscovery(); + mockReaddir = vi.mocked(await import('node:fs/promises')).readdir; + }); + + it('discovers .compact files recursively', async () => { + mockReaddir + .mockResolvedValueOnce([ + { name: 'MyToken.compact', isFile: () => true, isDirectory: () => false }, + { name: 'access', isFile: () => false, isDirectory: () => true }, + ] as any) + .mockResolvedValueOnce([ + { name: 'AccessControl.compact', isFile: () => true, isDirectory: () => false }, + ] as any); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['MyToken.compact', 'access/AccessControl.compact']); + }); + + it('filters out non-compact files', async () => { + mockReaddir.mockResolvedValue([ + { name: 'MyToken.compact', isFile: () => true, isDirectory: () => false }, + { name: 'README.md', isFile: () => true, isDirectory: () => false }, + { name: 'package.json', isFile: () => true, isDirectory: () => false }, + ] as any); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual(['MyToken.compact']); + }); + + it('handles empty directories', async () => { + mockReaddir.mockResolvedValue([]); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual([]); + }); + + it('handles readdir errors gracefully', async () => { + mockReaddir.mockRejectedValue(new Error('You shall not pass!')); + + const files = await fileDiscovery.getCompactFiles('src'); + + expect(files).toEqual([]); + }); +}); + +describe('BaseCompactService', () => { + let service: TestCompactService; + let mockExec: Mock; + + beforeEach(() => { + mockExec = vi.fn(); + service = new TestCompactService(mockExec); + }); + + it('executes command successfully', async () => { + const expectedResult = { stdout: 'Success', stderr: '' }; + mockExec.mockResolvedValue(expectedResult); + + const result = await service.testCommand('compact test'); + + expect(result).toEqual(expectedResult); + expect(mockExec).toHaveBeenCalledWith('compact test'); + }); + + it('handles command execution errors', async () => { + const errMsg = 'Command failed' + mockExec.mockRejectedValue(new Error(errMsg)); + + await expect(service.testCommand('compact test')).rejects.toThrow( + `Test operation failed: ${errMsg}` + ); + }); + + it('handles non-Error rejections', async () => { + const otherMsg = 'String error' + mockExec.mockRejectedValue(otherMsg); + + await expect(service.testCommand('compact test')).rejects.toThrow( + `Test operation failed: ${otherMsg}` + ); + }); +}); + +describe('SharedUIService', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + describe('printOutput', () => { + it('formats output with indentation', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const colorFn = (text: string) => `colored: ${text}`; + + // split, filter, map + SharedUIService.printOutput('line1\nline2\n\nline3', colorFn); + + expect(consoleSpy).toHaveBeenCalledWith('colored: line1\n line2\n line3'); + consoleSpy.mockRestore(); + }); + + it('filters empty lines', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const colorFn = (text: string) => text; + + SharedUIService.printOutput('line1\n\n\nline2', colorFn); + + expect(consoleSpy).toHaveBeenCalledWith(' line1\n line2'); + consoleSpy.mockRestore(); + }); + }); + + describe('displayBaseEnvInfo', () => { + const testData = { + operation: 'TEST', + version: 'compact 0.2.0', + targetDir: 'security' + }; + const { operation, version, targetDir } = testData; + + it('displays environment info with target directory', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.displayBaseEnvInfo(operation, version, targetDir); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledTimes(2); + expect(mockSpinner.info).toHaveBeenNthCalledWith(1, `[${operation}] TARGET_DIR: ${targetDir}`); + expect(mockSpinner.info).toHaveBeenNthCalledWith(2, `[${operation}] Compact developer tools: ${version}`); + }); + + it('displays environment info without target directory', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.displayBaseEnvInfo(operation, version); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Compact developer tools: ${version}`); + }); + }); + + describe('showOperationStart', () => { + const testData = { + operation: 'TEST', + action: 'compact 0.2.0', + fileCount: 5, + targetDir: 'security' + }; + const { operation, action, fileCount, targetDir } = testData; + + it('displays operation start message with targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showOperationStart(operation, action, fileCount, targetDir); + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Found ${fileCount} .compact file(s) to ${action} in ${targetDir}/`); + }); + + it('displays operation start message without targetDir', () => { + const mockSpinner = { info: vi.fn() }; + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showOperationStart(operation, action, fileCount); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Found ${fileCount} .compact file(s) to ${action}`); + }); + }); + + describe('showNoFiles', () => { + const testData = { + operation: 'TEST', + targetDir: 'security' + }; + const { operation, targetDir } = testData; + + it('shows no files warning with targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showNoFiles(operation, targetDir); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith(`[${operation}] No .compact files found in ${targetDir}/.`); + }); + + it('shows no files warning without targetDir', () => { + vi.mocked(ora).mockReturnValue(mockSpinner as any); + + SharedUIService.showNoFiles(operation); + + expect(ora).toHaveBeenCalled(); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith(`[${operation}] No .compact files found in src/.`); + }); + }); + + describe('showAvailableDirectories', () => { + it('shows available directories', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const operation = 'TEST'; + + SharedUIService.showAvailableDirectories(operation); + + expect(consoleSpy).toHaveBeenNthCalledWith(1, '\nAvailable directories:') + expect(consoleSpy).toHaveBeenNthCalledWith(2, ` --dir access # ${operation} access control contracts`) + expect(consoleSpy).toHaveBeenNthCalledWith(3, ` --dir archive # ${operation} archive contracts`) + expect(consoleSpy).toHaveBeenNthCalledWith(4, ` --dir security # ${operation} security contracts`) + expect(consoleSpy).toHaveBeenNthCalledWith(5, ` --dir token # ${operation} token contracts`) + expect(consoleSpy).toHaveBeenNthCalledWith(6, ` --dir utils # ${operation} utility contracts`) + consoleSpy.mockRestore(); + }); + }); +}); + +describe('BaseCompactOperation', () => { + let operation: TestCompactOperation; + let mockExistsSync: Mock; + + beforeEach(async () => { + operation = new TestCompactOperation('security'); + mockExistsSync = vi.mocked(await import('node:fs')).existsSync; + }); + + describe('validateTargetDirectory', () => { + it('passes when target directory exists', () => { + mockExistsSync.mockReturnValue(true); + + expect(() => operation['validateTargetDirectory']('src/security')).not.toThrow(); + }); + + it('throws when target directory does not exist', () => { + mockExistsSync.mockReturnValue(false); + const missingDir = 'src/missingDir'; + const expErr = new DirectoryNotFoundError(`Target directory ${missingDir} does not exist`, missingDir); + + expect(() => operation['validateTargetDirectory'](missingDir)).toThrow(expErr); + }); + + it('does not validate when no target directory is set', () => { + const noTargetOperation = new TestCompactOperation(); + mockExistsSync.mockReturnValue(false); + + expect(() => noTargetOperation['validateTargetDirectory']('src')).not.toThrow(); + }); + }); + + describe('getSearchDirectory', () => { + it('returns targetDir path when set', () => { + const result = operation['getSearchDirectory'](); + expect(result).toBe(join(SRC_DIR, 'security')); + }); + + it('returns SRC_DIR when no targetDir', () => { + const noTargetOperation = new TestCompactOperation(); + const result = noTargetOperation['getSearchDirectory'](); + expect(result).toBe(SRC_DIR); + }); + }); + + describe('parseBaseArgs', () => { + it('parses --dir argument correctly', () => { + const args = ['--dir', 'security', '--other-flag']; + + const result = BaseCompactOperation['parseBaseArgs'](args); + + expect(result).toEqual({ + targetDir: 'security', + remainingArgs: ['--other-flag'], + }); + }); + + it('throws error when --dir has no value', () => { + const args = ['--dir']; + + expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( + '--dir flag requires a directory name' + ); + }); + + it('throws error when --dir value starts with --', () => { + const args = ['--dir', '--other-flag']; + + expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( + '--dir flag requires a directory name' + ); + }); + + it('handles arguments without --dir', () => { + const args = ['--flag1', 'value1', '--flag2']; + + const result = BaseCompactOperation['parseBaseArgs'](args); + + expect(result).toEqual({ + targetDir: undefined, + remainingArgs: ['--flag1', 'value1', '--flag2'], + }); + }); + }); +}); + +describe('BaseErrorHandler', () => { + let mockSpinner: any; + + beforeEach(() => { + mockSpinner = { + fail: vi.fn(), + info: vi.fn(), + }; + }); + + describe('handleCommonErrors', () => { + const operation = 'TEST'; + + it('handles CompactCliNotFoundError', () => { + const error = new CompactCliNotFoundError('CLI not found'); + const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Error: CLI not found`) + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Install with:`) + ); + }); + + it('handles DirectoryNotFoundError', () => { + const error = new DirectoryNotFoundError('Directory not found', '/path'); + + const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Error: Directory not found`) + ); + }); + + it('handles promisified child process errors', () => { + const error = Object.assign(new Error('Command failed'), { + stdout: 'stdout', + stderr: 'stderr', + code: 1, + }); + + const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + + expect(result).toBe(true); + expect(mockSpinner.fail).toHaveBeenCalledExactlyOnceWith( + expect.stringContaining(`[${operation}] Environment validation failed`) + ); + }); + + it('handles --dir argument parsing errors', () => { + const error = new Error('--dir flag requires a directory name'); + + const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + + expect(result).toBe(false); // Should let specific handler show usage + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Error: --dir flag requires a directory name`) + ); + }); + + it('returns false for unhandled errors', () => { + const error = new Error('Some other error'); + + const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + + expect(result).toBe(false); + }); + }); + + describe('handleUnexpectedError', () => { + const operation = 'TEST'; + + it('handles Error objects', () => { + const error = new Error('Unexpected error'); + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Unexpected error: Unexpected error`) + ); + }); + + it('handles non-Error values', () => { + const error = 'String error'; + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + expect.stringContaining(`[${operation}] Unexpected error: String error`) + ); + }); + + it('displays troubleshooting information', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const error = new Error('Test error'); + + BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); + + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('If this error persists') + ); + consoleSpy.mockRestore(); + }); + }); +}); From 8fc511890143255812bbc9e2f790e7e6544648f0 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 20:30:26 -0300 Subject: [PATCH 13/44] refactor compiler tests --- compact/test/Compiler.test.ts | 967 ++++++++++------------------------ 1 file changed, 278 insertions(+), 689 deletions(-) diff --git a/compact/test/Compiler.test.ts b/compact/test/Compiler.test.ts index 514a82ea..7d89343d 100644 --- a/compact/test/Compiler.test.ts +++ b/compact/test/Compiler.test.ts @@ -1,468 +1,335 @@ -import { existsSync } from 'node:fs'; -import { readdir } from 'node:fs/promises'; +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { join } from 'node:path'; +import ora from 'ora'; import { - beforeEach, - describe, - expect, - it, - type MockedFunction, - vi, -} from 'vitest'; -import { - CompactCompiler, + CompilerEnvironmentValidator, CompilerService, - EnvironmentValidator, - type ExecFunction, - FileDiscovery, - UIService, + CompilerUIService, + CompactCompiler, } from '../src/Compiler.js'; -import { - CompactCliNotFoundError, - CompilationError, - DirectoryNotFoundError, -} from '../src/types/errors.js'; - -// Mock Node.js modules -vi.mock('node:fs'); -vi.mock('node:fs/promises'); +import { SRC_DIR, ARTIFACTS_DIR } from '../src/BaseServices.js'; +import { CompilationError } from '../src/types/errors.js'; + +// Mock dependencies vi.mock('chalk', () => ({ default: { - blue: (text: string) => text, - green: (text: string) => text, - red: (text: string) => text, - yellow: (text: string) => text, - cyan: (text: string) => text, - gray: (text: string) => text, + blue: vi.fn((text) => text), + green: vi.fn((text) => text), + red: vi.fn((text) => text), + yellow: vi.fn((text) => text), + cyan: vi.fn((text) => text), }, })); -// Mock spinner -const mockSpinner = { - start: () => ({ succeed: vi.fn(), fail: vi.fn(), text: '' }), - info: vi.fn(), - warn: vi.fn(), - fail: vi.fn(), - succeed: vi.fn(), -}; - vi.mock('ora', () => ({ - default: () => mockSpinner, + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + succeed: vi.fn(), + fail: vi.fn(), + info: vi.fn(), + })), })); -const mockExistsSync = vi.mocked(existsSync); -const mockReaddir = vi.mocked(readdir); - -describe('EnvironmentValidator', () => { - let mockExec: MockedFunction; - let validator: EnvironmentValidator; +describe('CompilerEnvironmentValidator', () => { + let validator: CompilerEnvironmentValidator; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); mockExec = vi.fn(); - validator = new EnvironmentValidator(mockExec); - }); - - describe('checkCompactAvailable', () => { - it('should return true when compact CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(true); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should return false when compact CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - const result = await validator.checkCompactAvailable(); - - expect(result).toBe(false); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - }); - - describe('getDevToolsVersion', () => { - it('should return trimmed version string', async () => { - mockExec.mockResolvedValue({ stdout: ' compact 0.1.0 \n', stderr: '' }); - - const version = await validator.getDevToolsVersion(); - - expect(version).toBe('compact 0.1.0'); - expect(mockExec).toHaveBeenCalledWith('compact --version'); - }); - - it('should throw error when command fails', async () => { - mockExec.mockRejectedValue(new Error('Command failed')); - - await expect(validator.getDevToolsVersion()).rejects.toThrow( - 'Command failed', - ); - }); + validator = new CompilerEnvironmentValidator(mockExec); }); describe('getToolchainVersion', () => { - it('should get version without specific version flag', async () => { + it('returns default toolchain version', async () => { + const testData = { + expectedOutput: 'Compactc version: 0.24.0', + expectedCommand: 'compact compile --version' + }; + mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.24.0', - stderr: '', + stdout: ` ${testData.expectedOutput} \n`, + stderr: '' }); const version = await validator.getToolchainVersion(); - expect(version).toBe('Compactc version: 0.24.0'); - expect(mockExec).toHaveBeenCalledWith('compact compile --version'); + expect(version).toBe(testData.expectedOutput); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should get version with specific version flag', async () => { + it('returns toolchain version with specific version', async () => { + const testData = { + version: '0.25.0', + expectedOutput: 'Compactc version: 0.25.0', + expectedCommand: 'compact compile +0.25.0 --version' + }; + mockExec.mockResolvedValue({ - stdout: 'Compactc version: 0.24.0', - stderr: '', + stdout: testData.expectedOutput, + stderr: '' }); - const version = await validator.getToolchainVersion('0.24.0'); + const version = await validator.getToolchainVersion(testData.version); - expect(version).toBe('Compactc version: 0.24.0'); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.24.0 --version', - ); + expect(version).toBe(testData.expectedOutput); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); }); describe('validate', () => { - it('should validate successfully when CLI is available', async () => { - mockExec.mockResolvedValue({ stdout: 'compact 0.1.0', stderr: '' }); - - await expect(validator.validate()).resolves.not.toThrow(); - }); - - it('should throw CompactCliNotFoundError when CLI is not available', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - - await expect(validator.validate()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - }); -}); - -describe('FileDiscovery', () => { - let discovery: FileDiscovery; - - beforeEach(() => { - vi.clearAllMocks(); - discovery = new FileDiscovery(); - }); - - describe('getCompactFiles', () => { - it('should find .compact files in directory', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - { name: 'README.md', isFile: () => true, isDirectory: () => false }, - { name: 'utils', isFile: () => false, isDirectory: () => true }, - ]; - - mockReaddir - .mockResolvedValueOnce(mockDirents as any) - .mockResolvedValueOnce([ - { - name: 'Utils.compact', - isFile: () => true, - isDirectory: () => false, - }, - ] as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual([ - 'MyToken.compact', - 'Ownable.compact', - 'utils/Utils.compact', - ]); - }); + it('returns both dev tools and toolchain versions', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.24.0' + }; - it('should handle empty directories', async () => { - mockReaddir.mockResolvedValue([]); + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); - const files = await discovery.getCompactFiles('src'); + const result = await validator.validate(); - expect(files).toEqual([]); + expect(result).toEqual({ + devToolsVersion: testData.devToolsVersion, + toolchainVersion: testData.toolchainVersion + }); }); - it('should handle directory read errors gracefully', async () => { - const consoleSpy = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - - mockReaddir.mockRejectedValueOnce(new Error('Permission denied')); + it('passes version parameter to getToolchainVersion', async () => { + const testData = { + version: '0.25.0', + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.25.0' + }; - const files = await discovery.getCompactFiles('src'); + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); - expect(files).toEqual([]); - expect(consoleSpy).toHaveBeenCalledWith( - 'Failed to read dir: src', - expect.any(Error), - ); - consoleSpy.mockRestore(); - }); + await validator.validate(testData.version); - it('should handle file access errors gracefully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => { - throw new Error('Access denied'); - }, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - - mockReaddir.mockResolvedValue(mockDirents as any); - - const files = await discovery.getCompactFiles('src'); - - expect(files).toEqual(['Ownable.compact']); + expect(mockExec).toHaveBeenCalledWith('compact compile +0.25.0 --version'); }); }); }); describe('CompilerService', () => { - let mockExec: MockedFunction; let service: CompilerService; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); mockExec = vi.fn(); service = new CompilerService(mockExec); }); describe('compileFile', () => { - it('should compile file successfully with basic flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs correct command with all parameters', async () => { + const testData = { + file: 'contracts/MyToken.compact', + flags: '--skip-zk --verbose', + version: '0.24.0', + expectedInputPath: join(SRC_DIR, 'contracts/MyToken.compact'), + expectedOutputDir: join(ARTIFACTS_DIR, 'MyToken'), + expectedCommand: 'compact compile +0.24.0 --skip-zk --verbose "src/contracts/MyToken.compact" "artifacts/MyToken"' + }; - const result = await service.compileFile('MyToken.compact', '--skip-zk'); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + await service.compileFile(testData.file, testData.flags, testData.version); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should compile file with version flag', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs command without version flag', async () => { + const testData = { + file: 'MyToken.compact', + flags: '--skip-zk', + expectedCommand: 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"' + }; - const result = await service.compileFile( - 'MyToken.compact', - '--skip-zk', - '0.24.0', - ); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile +0.24.0 --skip-zk "src/MyToken.compact" "artifacts/MyToken"', - ); + await service.compileFile(testData.file, testData.flags); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should handle empty flags', async () => { - mockExec.mockResolvedValue({ - stdout: 'Compilation successful', - stderr: '', - }); + it('constructs command without flags', async () => { + const testData = { + file: 'MyToken.compact', + flags: '', + expectedCommand: 'compact compile "src/MyToken.compact" "artifacts/MyToken"' + }; - const result = await service.compileFile('MyToken.compact', ''); + mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - expect(result).toEqual({ stdout: 'Compilation successful', stderr: '' }); - expect(mockExec).toHaveBeenCalledWith( - 'compact compile "src/MyToken.compact" "artifacts/MyToken"', - ); + await service.compileFile(testData.file, testData.flags); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); - it('should throw CompilationError when compilation fails', async () => { - mockExec.mockRejectedValue(new Error('Syntax error on line 10')); + it('throws CompilationError on failure', async () => { + const testData = { + file: 'MyToken.compact', + flags: '--skip-zk', + errorMessage: 'Syntax error on line 10' + }; + + mockExec.mockRejectedValue(new Error(testData.errorMessage)); await expect( - service.compileFile('MyToken.compact', '--skip-zk'), + service.compileFile(testData.file, testData.flags) ).rejects.toThrow(CompilationError); }); - it('should include file path in CompilationError', async () => { - mockExec.mockRejectedValue(new Error('Syntax error')); + it('CompilationError includes file name', async () => { + const testData = { + file: 'contracts/MyToken.compact', + flags: '--skip-zk' + }; + + mockExec.mockRejectedValue(new Error('Compilation failed')); try { - await service.compileFile('MyToken.compact', '--skip-zk'); + await service.compileFile(testData.file, testData.flags); } catch (error) { expect(error).toBeInstanceOf(CompilationError); - expect((error as CompilationError).file).toBe('MyToken.compact'); + expect((error as CompilationError).file).toBe(testData.file); } }); }); -}); -describe('UIService', () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.spyOn(console, 'log').mockImplementation(() => {}); - }); - - describe('printOutput', () => { - it('should format output with indentation', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); + describe('createError', () => { + it('extracts file name from error message', () => { + const testData = { + message: 'Failed to compile contracts/MyToken.compact: Syntax error', + expectedFile: 'contracts/MyToken.compact' + }; - UIService.printOutput('line 1\nline 2\n\nline 3', mockColorFn); + const error = service['createError'](testData.message); - expect(mockColorFn).toHaveBeenCalledWith( - ' line 1\n line 2\n line 3', - ); - expect(console.log).toHaveBeenCalledWith( - 'colored( line 1\n line 2\n line 3)', - ); + expect(error).toBeInstanceOf(CompilationError); + expect((error as CompilationError).file).toBe(testData.expectedFile); }); - it('should handle empty output', () => { - const mockColorFn = vi.fn((text: string) => `colored(${text})`); + it('uses "unknown" when file name cannot be extracted', () => { + const testData = { + message: 'Some generic error message', + expectedFile: 'unknown' + }; - UIService.printOutput('', mockColorFn); + const error = service['createError'](testData.message); - expect(mockColorFn).toHaveBeenCalledWith(''); - expect(console.log).toHaveBeenCalledWith('colored()'); + expect(error).toBeInstanceOf(CompilationError); + expect((error as CompilationError).file).toBe(testData.expectedFile); }); }); +}); - describe('displayEnvInfo', () => { - it('should display environment information with all parameters', () => { - UIService.displayEnvInfo( - 'compact 0.1.0', - 'Compactc 0.24.0', - 'security', - '0.24.0', - ); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] TARGET_DIR: security', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.24.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Using toolchain version: 0.24.0', - ); - }); - - it('should display environment information without optional parameters', () => { - UIService.displayEnvInfo('compact 0.1.0', 'Compactc 0.24.0'); +describe('CompilerUIService', () => { + let mockSpinner: any; - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact developer tools: compact 0.1.0', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Compact toolchain: Compactc 0.24.0', - ); - expect(mockSpinner.info).not.toHaveBeenCalledWith( - expect.stringContaining('TARGET_DIR'), - ); - expect(mockSpinner.info).not.toHaveBeenCalledWith( - expect.stringContaining('Using toolchain version'), - ); - }); + beforeEach(() => { + mockSpinner = { + info: vi.fn() + }; + vi.mocked(ora).mockReturnValue(mockSpinner); }); - describe('showCompilationStart', () => { - it('should show file count without target directory', () => { - UIService.showCompilationStart(5); + describe('displayEnvInfo', () => { + it('displays all environment information', () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.24.0', + targetDir: 'security', + version: '0.24.0' + }; - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 5 .compact file(s) to compile', + CompilerUIService.displayEnvInfo( + testData.devToolsVersion, + testData.toolchainVersion, + testData.targetDir, + testData.version ); - }); - it('should show file count with target directory', () => { - UIService.showCompilationStart(3, 'security'); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[COMPILE] Found 3 .compact file(s) to compile in security/', - ); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] TARGET_DIR: security'); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact developer tools: compact 0.2.0'); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact toolchain: Compactc version: 0.24.0'); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Using toolchain version: 0.24.0'); }); - }); - describe('showNoFiles', () => { - it('should show no files message with target directory', () => { - UIService.showNoFiles('security'); + it('displays minimal environment information', () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.24.0' + }; - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in security/.', + CompilerUIService.displayEnvInfo( + testData.devToolsVersion, + testData.toolchainVersion ); - }); - it('should show no files message without target directory', () => { - UIService.showNoFiles(); - - expect(mockSpinner.warn).toHaveBeenCalledWith( - '[COMPILE] No .compact files found in .', - ); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact developer tools: compact 0.2.0'); + expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact toolchain: Compactc version: 0.24.0'); + expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('TARGET_DIR')); + expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('Using toolchain version')); }); }); }); describe('CompactCompiler', () => { - let mockExec: MockedFunction; let compiler: CompactCompiler; + let mockExec: Mock; beforeEach(() => { - vi.clearAllMocks(); - mockExec = vi.fn().mockResolvedValue({ stdout: 'success', stderr: '' }); - mockExistsSync.mockReturnValue(true); - mockReaddir.mockResolvedValue([]); + mockExec = vi.fn(); }); describe('constructor', () => { - it('should create instance with default parameters', () => { + it('creates instance with default parameters', () => { compiler = new CompactCompiler(); expect(compiler).toBeInstanceOf(CompactCompiler); + expect(compiler.testFlags).toBe(''); + expect(compiler.testTargetDir).toBeUndefined(); + expect(compiler.testVersion).toBeUndefined(); }); - it('should create instance with all parameters', () => { + it('creates instance with all parameters', () => { + const testData = { + flags: '--skip-zk --verbose', + targetDir: 'security', + version: '0.24.0' + }; + compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.24.0', - mockExec, + testData.flags, + testData.targetDir, + testData.version, + mockExec ); - expect(compiler).toBeInstanceOf(CompactCompiler); + expect(compiler.testFlags).toBe(testData.flags); + expect(compiler.testTargetDir).toBe(testData.targetDir); + expect(compiler.testVersion).toBe(testData.version); }); - it('should trim flags', () => { - compiler = new CompactCompiler(' --skip-zk --verbose '); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + it('trims flags parameter', () => { + const testData = { + inputFlags: ' --skip-zk --verbose ', + expectedFlags: '--skip-zk --verbose' + }; + + compiler = new CompactCompiler(testData.inputFlags); + + expect(compiler.testFlags).toBe(testData.expectedFlags); }); }); describe('fromArgs', () => { - it('should parse empty arguments', () => { + it('parses empty arguments', () => { compiler = CompactCompiler.fromArgs([]); expect(compiler.testFlags).toBe(''); @@ -470,399 +337,121 @@ describe('CompactCompiler', () => { expect(compiler.testVersion).toBeUndefined(); }); - it('should handle SKIP_ZK environment variable', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); + it('parses SKIP_ZK environment variable', () => { + const testData = { + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk' + }; - it('should ignore SKIP_ZK when not "true"', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'false' }); + compiler = CompactCompiler.fromArgs([], testData.env); - expect(compiler.testFlags).toBe(''); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should parse --dir flag', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security']); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe(''); - }); + it('ignores SKIP_ZK when not "true"', () => { + const testData = { + env: { SKIP_ZK: 'false' }, + expectedFlags: '' + }; - it('should parse --dir flag with additional flags', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - ]); + compiler = CompactCompiler.fromArgs([], testData.env); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should parse version flag', () => { - compiler = CompactCompiler.fromArgs(['+0.24.0']); + it('parses complex arguments with all options', () => { + const testData = { + args: ['--dir', 'security', '--skip-zk', '--verbose', '+0.24.0'], + env: {}, + expectedTargetDir: 'security', + expectedFlags: '--skip-zk --verbose', + expectedVersion: '0.24.0' + }; - expect(compiler.testVersion).toBe('0.24.0'); - expect(compiler.testFlags).toBe(''); - }); + compiler = CompactCompiler.fromArgs(testData.args, testData.env); - it('should parse complex arguments', () => { - compiler = CompactCompiler.fromArgs([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]); - - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); - expect(compiler.testVersion).toBe('0.24.0'); + expect(compiler.testTargetDir).toBe(testData.expectedTargetDir); + expect(compiler.testFlags).toBe(testData.expectedFlags); + expect(compiler.testVersion).toBe(testData.expectedVersion); }); - it('should combine environment variables with CLI flags', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access', '--verbose'], { - SKIP_ZK: 'true', - }); + it('combines environment variables with CLI flags', () => { + const testData = { + args: ['--dir', 'access', '--verbose'], + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk --verbose' + }; + + compiler = CompactCompiler.fromArgs(testData.args, testData.env); - expect(compiler.testTargetDir).toBe('access'); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should deduplicate flags when both env var and CLI flag are present', () => { - compiler = CompactCompiler.fromArgs(['--skip-zk', '--verbose'], { - SKIP_ZK: 'true', - }); + it('deduplicates flags from environment and CLI', () => { + const testData = { + args: ['--skip-zk', '--verbose'], + env: { SKIP_ZK: 'true' }, + expectedFlags: '--skip-zk --verbose' + }; + + compiler = CompactCompiler.fromArgs(testData.args, testData.env); - expect(compiler.testFlags).toBe('--skip-zk --verbose'); + expect(compiler.testFlags).toBe(testData.expectedFlags); }); - it('should throw error for --dir without argument', () => { + it('throws error for --dir without argument', () => { expect(() => CompactCompiler.fromArgs(['--dir'])).toThrow( - '--dir flag requires a directory name', + '--dir flag requires a directory name' ); }); - it('should throw error for --dir followed by another flag', () => { + it('throws error for --dir followed by another flag', () => { expect(() => CompactCompiler.fromArgs(['--dir', '--skip-zk'])).toThrow( - '--dir flag requires a directory name', + '--dir flag requires a directory name' ); }); }); describe('validateEnvironment', () => { - it('should validate successfully and display environment info', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); // getToolchainVersion - - compiler = new CompactCompiler( - '--skip-zk', - 'security', - '0.24.0', - mockExec, - ); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await expect(compiler.validateEnvironment()).resolves.not.toThrow(); - - // 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.24.0 --version', - ); // getToolchainVersion() - - // Verify passed args - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.24.0', - 'security', - '0.24.0', - ); - - displaySpy.mockRestore(); - }); - - it('should handle CompactCliNotFoundError with installation instructions', async () => { - mockExec.mockRejectedValue(new Error('Command not found')); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); + it('calls validator and displays environment info', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + toolchainVersion: 'Compactc version: 0.24.0', + targetDir: 'security', + version: '0.24.0' + }; - it('should handle version retrieval failures after successful CLI check', async () => { mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // validate() succeeds - .mockRejectedValueOnce(new Error('Version command failed')); // getDevToolsVersion() fails + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); - compiler = new CompactCompiler('', undefined, undefined, mockExec); + const displaySpy = vi.spyOn(CompilerUIService, 'displayEnvInfo').mockImplementation(() => {}); - await expect(compiler.validateEnvironment()).rejects.toThrow( - 'Version command failed', - ); - }); - - it('should handle PromisifiedChildProcessError specifically', async () => { - const childProcessError = new Error('Command execution failed') as any; - childProcessError.stdout = 'some output'; - childProcessError.stderr = 'some error'; - - mockExec.mockRejectedValue(childProcessError); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - "'compact' CLI not found in PATH. Please install the Compact developer tools.", - ); - }); - - it('should handle non-Error exceptions gracefully', async () => { - mockExec.mockRejectedValue('String error message'); - compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.validateEnvironment()).rejects.toThrow( - CompactCliNotFoundError, - ); - }); - - it('should validate with specific version flag', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.25.0', - stderr: '', - }); - - compiler = new CompactCompiler('', undefined, '0.25.0', mockExec); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); + compiler = new CompactCompiler('--skip-zk', testData.targetDir, testData.version, mockExec); await compiler.validateEnvironment(); - // Verify version-specific toolchain call - expect(mockExec).toHaveBeenNthCalledWith( - 3, - 'compact compile +0.25.0 --version', - ); expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.25.0', - undefined, // no targetDir - '0.25.0', - ); - - displaySpy.mockRestore(); - }); - - it('should validate without target directory or version', async () => { - mockExec - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) - .mockResolvedValueOnce({ - stdout: 'Compactc version: 0.24.0', - stderr: '', - }); - - compiler = new CompactCompiler('', undefined, undefined, mockExec); - const displaySpy = vi - .spyOn(UIService, 'displayEnvInfo') - .mockImplementation(() => {}); - - await compiler.validateEnvironment(); - - // Verify default toolchain call (no version flag) - expect(mockExec).toHaveBeenNthCalledWith(3, 'compact compile --version'); - expect(displaySpy).toHaveBeenCalledWith( - 'compact 0.1.0', - 'Compactc version: 0.24.0', - undefined, - undefined, + testData.devToolsVersion, + testData.toolchainVersion, + testData.targetDir, + testData.version ); displaySpy.mockRestore(); }); }); - describe('compile', () => { - it('should handle empty source directory', async () => { - mockReaddir.mockResolvedValue([]); + describe('legacy compatibility', () => { + it('compile() method calls execute()', async () => { compiler = new CompactCompiler('', undefined, undefined, mockExec); - - await expect(compiler.compile()).resolves.not.toThrow(); - }); - - it('should throw error if target directory does not exist', async () => { - mockExistsSync.mockReturnValue(false); - compiler = new CompactCompiler('', 'nonexistent', undefined, mockExec); - - await expect(compiler.compile()).rejects.toThrow(DirectoryNotFoundError); - }); - - it('should compile files successfully', async () => { - const mockDirents = [ - { - name: 'MyToken.compact', - isFile: () => true, - isDirectory: () => false, - }, - { - name: 'Ownable.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); - compiler = new CompactCompiler( - '--skip-zk', - undefined, - undefined, - mockExec, - ); + const executeSpy = vi.spyOn(compiler, 'execute').mockResolvedValue(); await compiler.compile(); - expect(mockExec).toHaveBeenCalledWith( - expect.stringContaining('compact compile --skip-zk'), - ); - }); - - it('should handle compilation errors gracefully', async () => { - const brokenDirent = { - name: 'Broken.compact', - isFile: () => true, - isDirectory: () => false, - }; - - const mockDirents = [brokenDirent]; - mockReaddir.mockResolvedValue(mockDirents as any); - mockExistsSync.mockReturnValue(true); - - const testMockExec = vi - .fn() - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: 'compact 0.1.0', stderr: '' }) // getDevToolsVersion - .mockResolvedValueOnce({ stdout: 'Compactc 0.24.0', stderr: '' }) // getToolchainVersion - .mockRejectedValueOnce(new Error('Compilation failed')); // compileFile execution - - compiler = new CompactCompiler('', undefined, undefined, testMockExec); - - // Test that compilation errors are properly propagated - let thrownError: unknown; - try { - await compiler.compile(); - expect.fail('Expected compilation to throw an error'); - } catch (error) { - thrownError = error; - } - - expect(thrownError).toBeInstanceOf(Error); - expect((thrownError as Error).message).toBe( - `Failed to compile ${brokenDirent.name}: Compilation failed`, - ); - expect(testMockExec).toHaveBeenCalledTimes(4); - }); - }); - - describe('Real-world scenarios', () => { - beforeEach(() => { - const mockDirents = [ - { - name: 'AccessControl.compact', - isFile: () => true, - isDirectory: () => false, - }, - ]; - mockReaddir.mockResolvedValue(mockDirents as any); - }); - - it('should handle turbo compact command', () => { - compiler = CompactCompiler.fromArgs([]); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBeUndefined(); - }); - - it('should handle SKIP_ZK=true turbo compact command', () => { - compiler = CompactCompiler.fromArgs([], { SKIP_ZK: 'true' }); - - expect(compiler.testFlags).toBe('--skip-zk'); - }); - - it('should handle turbo compact:access command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'access']); - - expect(compiler.testFlags).toBe(''); - expect(compiler.testTargetDir).toBe('access'); - }); - - it('should handle turbo compact:security -- --skip-zk command', () => { - compiler = CompactCompiler.fromArgs(['--dir', 'security', '--skip-zk']); - - expect(compiler.testFlags).toBe('--skip-zk'); - expect(compiler.testTargetDir).toBe('security'); - }); - - it('should handle version specification', () => { - compiler = CompactCompiler.fromArgs(['+0.24.0']); - - expect(compiler.testVersion).toBe('0.24.0'); - }); - - it.each([ - { - name: 'with skip zk env var only', - args: [ - '--dir', - 'security', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'true' }, - }, - { - name: 'with skip-zk flag only', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'false' }, - }, - { - name: 'with both skip-zk flag and env var', - args: [ - '--dir', - 'security', - '--skip-zk', - '--no-communications-commitment', - '+0.24.0', - ], - env: { SKIP_ZK: 'true' }, - }, - ])('should handle complex command $name', ({ args, env }) => { - compiler = CompactCompiler.fromArgs(args, env); - - expect(compiler.testFlags).toBe( - '--skip-zk --no-communications-commitment', - ); - expect(compiler.testTargetDir).toBe('security'); - expect(compiler.testVersion).toBe('0.24.0'); + expect(executeSpy).toHaveBeenCalled(); + executeSpy.mockRestore(); }); }); }); From 590f8b5c0328ee910d44248b689ec4c6b589afec Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 22:12:09 -0300 Subject: [PATCH 14/44] fix fmt --- compact/src/runCompiler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index 1b07b158..b2551f43 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -91,7 +91,7 @@ function showUsageHelp(): void { console.log(chalk.yellow('\nExamples:')); console.log( chalk.yellow( - ' compact-compiler # Compile all files', + ' compact-compiler # Compile all files', ), ); console.log( From 8ab1d189a99eeb03bdb105e859a0999b3cff625f Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 22:12:27 -0300 Subject: [PATCH 15/44] refactor runCompiler tests --- compact/test/runCompiler.test.ts | 455 +++++++++++++------------------ 1 file changed, 194 insertions(+), 261 deletions(-) diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index 11244f2d..4f625c3a 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { CompactCompiler } from '../src/Compiler.js'; +import { BaseErrorHandler } from '../src/BaseServices.js'; import { CompactCliNotFoundError, CompilationError, @@ -7,14 +8,20 @@ import { isPromisifiedChildProcessError, } from '../src/types/errors.js'; -// Mock CompactCompiler +// Mock dependencies vi.mock('../src/Compiler.js', () => ({ CompactCompiler: { fromArgs: vi.fn(), }, })); -// Mock error utilities +vi.mock('../src/BaseServices.js', () => ({ + BaseErrorHandler: { + handleCommonErrors: vi.fn(), + handleUnexpectedError: vi.fn(), + }, +})); + vi.mock('../src/types/errors.js', async () => { const actual = await vi.importActual('../src/types/errors.js'); return { @@ -54,6 +61,8 @@ const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); describe('runCompiler CLI', () => { let mockCompile: ReturnType; let mockFromArgs: ReturnType; + let mockHandleCommonErrors: ReturnType; + let mockHandleUnexpectedError: ReturnType; let originalArgv: string[]; beforeEach(() => { @@ -65,6 +74,8 @@ describe('runCompiler CLI', () => { mockCompile = vi.fn(); mockFromArgs = vi.mocked(CompactCompiler.fromArgs); + mockHandleCommonErrors = vi.mocked(BaseErrorHandler.handleCommonErrors); + mockHandleUnexpectedError = vi.mocked(BaseErrorHandler.handleUnexpectedError); // Mock CompactCompiler instance mockFromArgs.mockReturnValue({ @@ -85,287 +96,200 @@ describe('runCompiler CLI', () => { }); describe('successful compilation', () => { - it('should compile successfully with no arguments', async () => { + it('compiles successfully with no arguments', async () => { + const testData = { + expectedArgs: [] + }; + mockCompile.mockResolvedValue(undefined); // Import and run the CLI await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); expect(mockCompile).toHaveBeenCalled(); expect(mockExit).not.toHaveBeenCalled(); }); - it('should compile successfully with arguments', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - ]; + it('compiles successfully with arguments', async () => { + const testData = { + args: ['--dir', 'security', '--skip-zk'], + processArgv: ['node', 'runCompiler.js', '--dir', 'security', '--skip-zk'] + }; + + process.argv = testData.processArgv; mockCompile.mockResolvedValue(undefined); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - ]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); expect(mockCompile).toHaveBeenCalled(); expect(mockExit).not.toHaveBeenCalled(); }); }); - describe('error handling', () => { - it('should handle CompactCliNotFoundError with installation instructions', async () => { - const error = new CompactCliNotFoundError('CLI not found'); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: CLI not found', - ); - expect(mockSpinner.info).toHaveBeenCalledWith( - "[COMPILE] Install with: curl --proto '=https' --tlsv1.2 -LsSf https://github.com/midnightntwrk/compact/releases/latest/download/compact-installer.sh | sh", - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); + describe('error handling delegation', () => { + it('delegates to BaseErrorHandler.handleCommonErrors first', async () => { + const testData = { + error: new CompactCliNotFoundError('CLI not found'), + operation: 'COMPILE' + }; - it('should handle DirectoryNotFoundError with helpful message', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/nonexistent', - ); - mockCompile.mockRejectedValue(error); + mockHandleCommonErrors.mockReturnValue(true); // Indicates error was handled + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: Directory not found', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation ); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle CompilationError with file context', async () => { - const error = new CompilationError( - 'Compilation failed', - 'MyToken.compact', - ); - mockCompile.mockRejectedValue(error); - - await import('../src/runCompiler.js'); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: MyToken.compact', - ); - expect(mockExit).toHaveBeenCalledWith(1); - }); + it('handles compiler-specific errors when BaseErrorHandler returns false', async () => { + const testData = { + error: new CompilationError('Compilation failed', 'MyToken.compact'), + expectedMessage: '[COMPILE] Compilation failed for file: MyToken.compact' + }; - it('should handle CompilationError with unknown file', async () => { - const error = new CompilationError('Compilation failed'); - mockCompile.mockRejectedValue(error); + mockHandleCommonErrors.mockReturnValue(false); // Not handled by base + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Compilation failed for file: unknown', - ); + expect(mockHandleCommonErrors).toHaveBeenCalled(); + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); + it('handles CompilationError with unknown file', async () => { + const testData = { + error: new CompilationError('Compilation failed', ''), + expectedMessage: '[COMPILE] Compilation failed for file: unknown' + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Error: --dir flag requires a directory name', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nUsage: compact-compiler [options]', - ); - expect(mockExit).toHaveBeenCalledWith(1); + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); }); - it('should handle unexpected errors', async () => { - const msg = 'Something unexpected happened'; - const error = new Error(msg); - mockCompile.mockRejectedValue(error); + it('shows usage help for argument parsing errors', async () => { + const testData = { + error: new Error('--dir flag requires a directory name') + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nIf this error persists, please check:', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Compact CLI is installed and in PATH', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Source files exist and are readable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Specified Compact version exists', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • File system permissions are correct', - ); + expect(mockConsoleLog).toHaveBeenCalledWith('\nUsage: compact-compiler [options]'); + expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); expect(mockExit).toHaveBeenCalledWith(1); }); - it('should handle non-Error exceptions', async () => { - const msg = 'String error'; - mockCompile.mockRejectedValue(msg); + it('delegates unexpected errors to BaseErrorHandler', async () => { + const testData = { + error: new Error('Unexpected error'), + operation: 'COMPILE' + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockSpinner.fail).toHaveBeenCalledWith( - `[COMPILE] Unexpected error: ${msg}`, + expect(mockHandleUnexpectedError).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation ); - expect(mockExit).toHaveBeenCalledWith(1); }); }); - describe('environment validation errors', () => { - it('should handle promisified child process errors', async () => { - const mockIsPromisifiedChildProcessError = vi.mocked( - isPromisifiedChildProcessError, - ); - - const error = { - message: 'Command failed', - stdout: 'some output', - stderr: 'error details', + describe('CompilationError handling', () => { + it('displays stderr output when available', async () => { + const testData = { + execError: { + stderr: 'Detailed error output', + stdout: 'some output' + } }; - // Return true for this specific error - mockIsPromisifiedChildProcessError.mockImplementation( - (err) => err === error, + const compilationError = new CompilationError( + 'Compilation failed', + 'MyToken.compact', + testData.execError ); - mockCompile.mockRejectedValue(error); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockCompile.mockRejectedValue(compilationError); await import('../src/runCompiler.js'); - expect(mockIsPromisifiedChildProcessError).toHaveBeenCalledWith(error); - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[COMPILE] Environment validation failed: Command failed', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTroubleshooting:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Check that Compact CLI is installed and in PATH', - ); expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Verify the specified Compact version exists', + expect.stringContaining('Additional error details: Detailed error output') ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' • Ensure you have proper permissions', - ); - expect(mockExit).toHaveBeenCalledWith(1); }); - }); - describe('usage help', () => { - it('should show complete usage help for argument parsing errors', async () => { - const error = new Error('--dir flag requires a directory name'); - mockFromArgs.mockImplementation(() => { - throw error; - }); + it('skips stderr output when it contains stdout/stderr keywords', async () => { + const testData = { + execError: { + stderr: 'Error: stdout and stderr already displayed', + stdout: 'some output' + } + }; + + const compilationError = new CompilationError( + 'Compilation failed', + 'MyToken.compact', + testData.execError + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockCompile.mockRejectedValue(compilationError); await import('../src/runCompiler.js'); - // Verify all sections of help are shown - expect(mockConsoleLog).toHaveBeenCalledWith( - '\nUsage: compact-compiler [options]', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir Compile specific directory (access, archive, security, token, utils)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --skip-zk Skip zero-knowledge proof generation', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' + Use specific toolchain version (e.g., +0.24.0)', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nExamples:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler # Compile all files', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir security # Compile security directory', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --dir access --skip-zk # Compile access with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' compact-compiler --skip-zk +0.24.0 # Use specific version', - ); - expect(mockConsoleLog).toHaveBeenCalledWith('\nTurbo integration:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact # Full build', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' turbo compact:security -- --skip-zk # Directory with flags', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' SKIP_ZK=true turbo compact # Environment variables', + expect(mockConsoleLog).not.toHaveBeenCalledWith( + expect.stringContaining('Additional error details') ); }); }); - describe('directory error help', () => { - it('should show all available directories', async () => { - const error = new DirectoryNotFoundError( - 'Directory not found', - 'src/invalid', - ); - mockCompile.mockRejectedValue(error); + describe('argument parsing error handling', () => { + it('shows complete usage help', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + expectedSections: [ + '\nUsage: compact-compiler [options]', + '\nOptions:', + ' --dir Compile specific directory (access, archive, security, token, utils)', + ' --skip-zk Skip zero-knowledge proof generation', + ' + Use specific toolchain version (e.g., +0.24.0)', + '\nExamples:', + ' compact-compiler # Compile all files', + ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', + '\nTurbo integration:', + ' turbo compact # Full build' + ] + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockCompile.mockRejectedValue(testData.error); await import('../src/runCompiler.js'); - expect(mockConsoleLog).toHaveBeenCalledWith('\nAvailable directories:'); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir access # Compile access control contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir archive # Compile archive contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir security # Compile security contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir token # Compile token contracts', - ); - expect(mockConsoleLog).toHaveBeenCalledWith( - ' --dir utils # Compile utility contracts', - ); + testData.expectedSections.forEach(section => { + expect(mockConsoleLog).toHaveBeenCalledWith(section); + }); }); }); @@ -374,85 +298,94 @@ describe('runCompiler CLI', () => { mockCompile.mockResolvedValue(undefined); }); - it('should handle turbo compact', async () => { - process.argv = ['node', 'runCompiler.js']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([]); - }); + it('handles turbo compact', async () => { + const testData = { + processArgv: ['node', 'runCompiler.js'], + expectedArgs: [] + }; - it('should handle turbo compact:security', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'security']; + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(['--dir', 'security']); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); - it('should handle turbo compact:access -- --skip-zk', async () => { - process.argv = ['node', 'runCompiler.js', '--dir', 'access', '--skip-zk']; - - await import('../src/runCompiler.js'); - - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'access', - '--skip-zk', - ]); - }); + it('handles turbo compact:security', async () => { + const testData = { + processArgv: ['node', 'runCompiler.js', '--dir', 'security'], + expectedArgs: ['--dir', 'security'] + }; - it('should handle version specification', async () => { - process.argv = ['node', 'runCompiler.js', '+0.24.0', '--skip-zk']; + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(['+0.24.0', '--skip-zk']); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); - it('should handle complex command', async () => { - process.argv = [ - 'node', - 'runCompiler.js', - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]; + it('handles complex command with multiple flags', async () => { + const testData = { + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'security', + '--skip-zk', + '--verbose', + '+0.24.0', + ], + expectedArgs: [ + '--dir', + 'security', + '--skip-zk', + '--verbose', + '+0.24.0', + ] + }; + + process.argv = testData.processArgv; await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([ - '--dir', - 'security', - '--skip-zk', - '--verbose', - '+0.24.0', - ]); + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); }); }); describe('integration with CompactCompiler', () => { - it('should pass arguments correctly to CompactCompiler.fromArgs', async () => { - const args = ['--dir', 'token', '--skip-zk', '+0.24.0']; - process.argv = ['node', 'runCompiler.js', ...args]; + it('passes arguments correctly to CompactCompiler.fromArgs', async () => { + const testData = { + args: ['--dir', 'token', '--skip-zk', '+0.24.0'], + processArgv: ['node', 'runCompiler.js', '--dir', 'token', '--skip-zk', '+0.24.0'] + }; + + process.argv = testData.processArgv; mockCompile.mockResolvedValue(undefined); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith(args); + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); expect(mockFromArgs).toHaveBeenCalledTimes(1); expect(mockCompile).toHaveBeenCalledTimes(1); }); - it('should handle empty arguments', async () => { - process.argv = ['node', 'runCompiler.js']; - mockCompile.mockResolvedValue(undefined); + it('handles fromArgs throwing errors', async () => { + const testData = { + error: new Error('Invalid arguments') + }; + + mockFromArgs.mockImplementation(() => { + throw testData.error; + }); await import('../src/runCompiler.js'); - expect(mockFromArgs).toHaveBeenCalledWith([]); + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), + 'COMPILE' + ); + expect(mockExit).toHaveBeenCalledWith(1); }); }); }); From 2be014f97caff09f019f432981edc3d87bd2f1f2 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 22:12:48 -0300 Subject: [PATCH 16/44] fix import --- compact/test/runCompiler.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index 4f625c3a..e155a29a 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -4,7 +4,6 @@ import { BaseErrorHandler } from '../src/BaseServices.js'; import { CompactCliNotFoundError, CompilationError, - DirectoryNotFoundError, isPromisifiedChildProcessError, } from '../src/types/errors.js'; From cb1051e591a772423219f06d3cac8bad3066a967 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 22:44:45 -0300 Subject: [PATCH 17/44] turn off lit key error --- biome.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/biome.json b/biome.json index 6e55f10c..e312d13d 100644 --- a/biome.json +++ b/biome.json @@ -52,6 +52,9 @@ "useExhaustiveDependencies": "off", "useJsxKeyInIterable": "off" }, + "complexity": { + "useLiteralKeys": "off" + }, "performance": { "noBarrelFile": "error", "noReExportAll": "error", From 543eef020bd7d685ad5d305ab9adbb76aa9d73e2 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 22:44:54 -0300 Subject: [PATCH 18/44] fix fmt --- compact/src/BaseServices.ts | 90 ++++++++------ compact/src/Compiler.ts | 19 +-- compact/src/Formatter.ts | 60 +++++++--- compact/src/runCompiler.ts | 2 +- compact/src/runFormatter.ts | 18 +-- compact/test/BaseServices.test.ts | 191 +++++++++++++++++++++--------- compact/test/Compiler.test.ts | 141 ++++++++++++++-------- compact/test/runCompiler.test.ts | 78 +++++++----- 8 files changed, 393 insertions(+), 206 deletions(-) diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts index 27b875fc..e0ad89e4 100644 --- a/compact/src/BaseServices.ts +++ b/compact/src/BaseServices.ts @@ -65,7 +65,7 @@ export abstract class BaseEnvironmentValidator { const isAvailable = await this.checkCompactAvailable(); if (!isAvailable) { throw new CompactCliNotFoundError( - "'compact' CLI not found in PATH. Please install the Compact developer tools." + "'compact' CLI not found in PATH. Please install the Compact developer tools.", ); } @@ -104,6 +104,7 @@ export class FileDiscovery { } return []; } catch (err) { + // biome-ignore lint/suspicious/noConsole: Displaying path console.warn(`Error accessing ${fullPath}:`, err); return []; } @@ -112,6 +113,7 @@ export class FileDiscovery { const results = await Promise.all(filePromises); return results.flat(); } catch (err) { + // biome-ignore lint/suspicious/noConsole: Displaying dir console.error(`Failed to read dir: ${dir}`, err); return []; } @@ -161,22 +163,22 @@ export abstract class BaseCompactService { * Shared UI service for consistent styling across compiler and formatter. * Provides common output formatting and user feedback patterns. */ -export class SharedUIService { +export const SharedUIService = { /** * Prints formatted output with consistent indentation and coloring. */ - static printOutput(output: string, colorFn: (text: string) => string): void { + 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 base environment information. */ - static displayBaseEnvInfo( + displayBaseEnvInfo( operation: string, devToolsVersion: string, targetDir?: string, @@ -188,14 +190,14 @@ export class SharedUIService { } spinner.info( - chalk.blue(`[${operation}] Compact developer tools: ${devToolsVersion}`) + chalk.blue(`[${operation}] Compact developer tools: ${devToolsVersion}`), ); - } + }, /** * Displays operation start message with file count. */ - static showOperationStart( + showOperationStart( operation: string, action: string, fileCount: number, @@ -205,36 +207,46 @@ export class SharedUIService { const spinner = ora(); spinner.info( chalk.blue( - `[${operation}] Found ${fileCount} .compact file(s) to ${action}${searchLocation}` - ) + `[${operation}] Found ${fileCount} .compact file(s) to ${action}${searchLocation}`, + ), ); - } + }, /** * Displays a warning when no .compact files are found. */ - static showNoFiles(operation: string, targetDir?: string): void { + showNoFiles(operation: string, targetDir?: string): void { const searchLocation = targetDir ? `${targetDir}/` : 'src/'; const spinner = ora(); spinner.warn( - chalk.yellow(`[${operation}] No .compact files found in ${searchLocation}.`) + chalk.yellow( + `[${operation}] No .compact files found in ${searchLocation}.`, + ), ); - } + }, /** * Shows available directories when DirectoryNotFoundError occurs. */ - static showAvailableDirectories(operation: string): void { + showAvailableDirectories(operation: string): void { console.log(chalk.yellow('\nAvailable directories:')); console.log( chalk.yellow(` --dir access # ${operation} access control contracts`), ); - console.log(chalk.yellow(` --dir archive # ${operation} archive contracts`)); - console.log(chalk.yellow(` --dir security # ${operation} security contracts`)); - console.log(chalk.yellow(` --dir token # ${operation} token contracts`)); - console.log(chalk.yellow(` --dir utils # ${operation} utility contracts`)); - } -} + console.log( + chalk.yellow(` --dir archive # ${operation} archive contracts`), + ); + console.log( + chalk.yellow(` --dir security # ${operation} security contracts`), + ); + console.log( + chalk.yellow(` --dir token # ${operation} token contracts`), + ); + console.log( + chalk.yellow(` --dir utils # ${operation} utility contracts`), + ); + }, +}; /** * Base class for Compact operations (compilation, formatting). @@ -271,7 +283,10 @@ export abstract class BaseCompactOperation { /** * Discovers files and handles empty results. */ - protected async discoverFiles(): Promise<{ files: string[]; searchDir: string }> { + protected async discoverFiles(): Promise<{ + files: string[]; + searchDir: string; + }> { const searchDir = this.getSearchDirectory(); this.validateTargetDirectory(searchDir); @@ -304,7 +319,8 @@ export abstract class BaseCompactOperation { for (let i = 0; i < args.length; i++) { if (args[i] === '--dir') { - const dirNameExists = i + 1 < args.length && !args[i + 1].startsWith('--'); + const dirNameExists = + i + 1 < args.length && !args[i + 1].startsWith('--'); if (dirNameExists) { targetDir = args[i + 1]; i++; @@ -324,12 +340,8 @@ export abstract class BaseCompactOperation { * Base error handler for both compiler and formatter CLIs. * Handles common error types with operation-specific context. */ -export class BaseErrorHandler { - static handleCommonErrors( - error: unknown, - spinner: Ora, - operation: string, - ): boolean { +export const BaseErrorHandler = { + handleCommonErrors(error: unknown, spinner: Ora, operation: string): boolean { // CompactCliNotFoundError if (error instanceof Error && error.name === 'CompactCliNotFoundError') { spinner.fail(chalk.red(`[${operation}] Error: ${error.message}`)); @@ -351,13 +363,17 @@ export class BaseErrorHandler { // Environment validation errors if (isPromisifiedChildProcessError(error)) { spinner.fail( - chalk.red(`[${operation}] Environment validation failed: ${error.message}`), + chalk.red( + `[${operation}] Environment validation failed: ${error.message}`, + ), ); console.log(chalk.gray('\nTroubleshooting:')); console.log( chalk.gray(' • Check that Compact CLI is installed and in PATH'), ); - console.log(chalk.gray(' • Verify the specified Compact version exists')); + console.log( + chalk.gray(' • Verify the specified Compact version exists'), + ); console.log(chalk.gray(' • Ensure you have proper permissions')); return true; } @@ -372,13 +388,9 @@ export class BaseErrorHandler { } return false; // Not handled, let specific handler deal with it - } + }, - static handleUnexpectedError( - error: unknown, - spinner: Ora, - operation: string, - ): void { + handleUnexpectedError(error: unknown, spinner: Ora, operation: string): void { const errorMessage = error instanceof Error ? error.message : String(error); spinner.fail(chalk.red(`[${operation}] Unexpected error: ${errorMessage}`)); @@ -386,5 +398,5 @@ export class BaseErrorHandler { console.log(chalk.gray(' • Compact CLI is installed and in PATH')); console.log(chalk.gray(' • Source files exist and are readable')); console.log(chalk.gray(' • File system permissions are correct')); - } -} + }, +}; diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index 3b7c111d..a0541409 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -4,13 +4,13 @@ import { basename, join } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import { - BaseEnvironmentValidator, - BaseCompactService, + ARTIFACTS_DIR, BaseCompactOperation, + BaseCompactService, + BaseEnvironmentValidator, + type ExecFunction, SharedUIService, SRC_DIR, - ARTIFACTS_DIR, - type ExecFunction, } from './BaseServices.js'; import { CompilationError, @@ -98,7 +98,7 @@ export const CompilerUIService = { const spinner = ora(); spinner.info( - chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`) + chalk.blue(`[COMPILE] Compact toolchain: ${toolchainVersion}`), ); if (version) { @@ -110,7 +110,12 @@ export const CompilerUIService = { * Displays compilation start message. */ showCompilationStart(fileCount: number, targetDir?: string): void { - SharedUIService.showOperationStart('COMPILE', 'compile', fileCount, targetDir); + SharedUIService.showOperationStart( + 'COMPILE', + 'compile', + fileCount, + targetDir, + ); }, /** @@ -154,7 +159,7 @@ export class CompactCompiler extends BaseCompactOperation { args: string[], env: NodeJS.ProcessEnv = process.env, ): CompactCompiler { - const { targetDir, remainingArgs } = this.parseBaseArgs(args); + const { targetDir, remainingArgs } = CompactCompiler.parseBaseArgs(args); const flags: string[] = []; let version: string | undefined; diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 891ed326..06fbdc72 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -4,12 +4,12 @@ import { join } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import { - BaseEnvironmentValidator, - BaseCompactService, BaseCompactOperation, + BaseCompactService, + BaseEnvironmentValidator, + type ExecFunction, SharedUIService, SRC_DIR, - type ExecFunction, } from './BaseServices.js'; import { FormatterError, @@ -29,10 +29,12 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { try { await this.execFn('compact help format'); } catch (error) { - if (isPromisifiedChildProcessError(error) && - error.stderr?.includes('formatter not available')) { + if ( + isPromisifiedChildProcessError(error) && + error.stderr?.includes('formatter not available') + ) { throw new FormatterNotAvailableError( - 'Formatter not available. Please update your Compact compiler with: compact update' + 'Formatter not available. Please update your Compact compiler with: compact update', ); } throw error; @@ -57,7 +59,9 @@ export class FormatterService extends BaseCompactService { /** * Formats files in-place in the specified directory or current directory. */ - async formatInPlace(targetPath?: string): Promise<{ stdout: string; stderr: string }> { + async formatInPlace( + targetPath?: string, + ): Promise<{ stdout: string; stderr: string }> { const pathArg = targetPath ? ` "${targetPath}"` : ''; const command = `compact format${pathArg}`; return this.executeCompactCommand(command, 'Failed to format'); @@ -75,7 +79,10 @@ export class FormatterService extends BaseCompactService { const command = `compact format --check${pathArg}`; try { - const result = await this.executeCompactCommand(command, 'Failed to check formatting'); + const result = await this.executeCompactCommand( + command, + 'Failed to check formatting', + ); return { ...result, isFormatted: true }; } catch (error: unknown) { if (isPromisifiedChildProcessError(error)) { @@ -84,7 +91,7 @@ export class FormatterService extends BaseCompactService { return { stdout: error.stdout, stderr: error.stderr || '', - isFormatted: false + isFormatted: false, }; } } @@ -95,14 +102,19 @@ export class FormatterService extends BaseCompactService { /** * Formats a list of specific files. */ - async formatFiles(files: string[]): Promise<{ stdout: string; stderr: string }> { + async formatFiles( + files: string[], + ): Promise<{ stdout: string; stderr: string }> { if (files.length === 0) { return { stdout: '', stderr: '' }; } - const fileArgs = files.map(file => `"${join(SRC_DIR, file)}"`).join(' '); + const fileArgs = files.map((file) => `"${join(SRC_DIR, file)}"`).join(' '); const command = `compact format ${fileArgs}`; - return this.executeCompactCommand(command, `Failed to format files: ${files.join(', ')}`); + return this.executeCompactCommand( + command, + `Failed to format files: ${files.join(', ')}`, + ); } protected createError(message: string, cause?: unknown): Error { @@ -130,7 +142,11 @@ export const FormatterUIService = { /** * Displays formatting start message. */ - showFormattingStart(fileCount: number, mode: 'format' | 'check', targetDir?: string): void { + showFormattingStart( + fileCount: number, + mode: 'format' | 'check', + targetDir?: string, + ): void { const action = mode === 'check' ? 'check formatting for' : 'format'; SharedUIService.showOperationStart('FORMAT', action, fileCount, targetDir); }, @@ -179,9 +195,10 @@ export class CompactFormatter extends BaseCompactOperation { execFn?: ExecFunction, ) { // For single directory target, use it as targetDir - const targetDir = targets.length === 1 && !targets[0].endsWith('.compact') - ? targets[0] - : undefined; + const targetDir = + targets.length === 1 && !targets[0].endsWith('.compact') + ? targets[0] + : undefined; super(targetDir); this.checkMode = checkMode; @@ -194,7 +211,7 @@ export class CompactFormatter extends BaseCompactOperation { * Factory method to create a CompactFormatter from command-line arguments. */ static fromArgs(args: string[]): CompactFormatter { - const { targetDir, remainingArgs } = this.parseBaseArgs(args); + const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); let checkMode = false; const targets: string[] = []; @@ -237,7 +254,10 @@ export class CompactFormatter extends BaseCompactOperation { await this.validateEnvironment(); // Handle specific file targets - if (this.targets.length > 0 && this.targets.every(target => target.endsWith('.compact'))) { + if ( + this.targets.length > 0 && + this.targets.every((target) => target.endsWith('.compact')) + ) { return this.formatSpecificFiles(); } @@ -292,7 +312,9 @@ export class CompactFormatter extends BaseCompactOperation { } const spinner = ora(); - spinner.succeed(chalk.green(`[FORMAT] Successfully formatted ${files.length} file(s)`)); + spinner.succeed( + chalk.green(`[FORMAT] Successfully formatted ${files.length} file(s)`), + ); } } diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index b2551f43..a6ed221d 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -2,8 +2,8 @@ import chalk from 'chalk'; import ora, { type Ora } from 'ora'; -import { CompactCompiler } from './Compiler.js'; import { BaseErrorHandler } from './BaseServices.js'; +import { CompactCompiler } from './Compiler.js'; import { type CompilationError, isPromisifiedChildProcessError, diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts index 1302a320..fb1aa6e7 100644 --- a/compact/src/runFormatter.ts +++ b/compact/src/runFormatter.ts @@ -2,8 +2,8 @@ import chalk from 'chalk'; import ora, { type Ora } from 'ora'; -import { CompactFormatter } from './Formatter.js'; import { BaseErrorHandler } from './BaseServices.js'; +import { CompactFormatter } from './Formatter.js'; import { type FormatterError, isPromisifiedChildProcessError, @@ -37,9 +37,7 @@ function handleError(error: unknown, spinner: Ora): void { // FormatterNotAvailableError - specific to formatting if (error instanceof Error && error.name === 'FormatterNotAvailableError') { spinner.fail(chalk.red(`[FORMAT] Error: ${error.message}`)); - spinner.info( - chalk.blue('[FORMAT] Update compiler with: compact update'), - ); + spinner.info(chalk.blue('[FORMAT] Update compiler with: compact update')); spinner.info( chalk.blue('[FORMAT] Update dev tools with: compact self update'), ); @@ -130,13 +128,19 @@ function showUsageHelp(): void { ); console.log(chalk.yellow('\nIntegration examples:')); console.log( - chalk.yellow(' turbo format # Full formatting'), + chalk.yellow( + ' turbo format # Full formatting', + ), ); console.log( - chalk.yellow(' turbo format:security # Directory formatting'), + chalk.yellow( + ' turbo format:security # Directory formatting', + ), ); console.log( - chalk.yellow(' turbo format:check # Check formatting'), + chalk.yellow( + ' turbo format:check # Check formatting', + ), ); } diff --git a/compact/test/BaseServices.test.ts b/compact/test/BaseServices.test.ts index 414052a4..ffaa2f17 100644 --- a/compact/test/BaseServices.test.ts +++ b/compact/test/BaseServices.test.ts @@ -1,16 +1,14 @@ -import { describe, test, it, expect, vi, beforeEach, Mock } from 'vitest'; import { join } from 'node:path'; -import chalk from 'chalk'; import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; import { + BaseCompactOperation, + BaseCompactService, BaseEnvironmentValidator, + BaseErrorHandler, FileDiscovery, - BaseCompactService, SharedUIService, - BaseCompactOperation, - BaseErrorHandler, SRC_DIR, - type ExecFunction, } from '../src/BaseServices.js'; import { CompactCliNotFoundError, @@ -60,11 +58,13 @@ class TestEnvironmentValidator extends BaseEnvironmentValidator { } class TestCompactService extends BaseCompactService { - async testCommand(command: string): Promise<{ stdout: string; stderr: string }> { + async testCommand( + command: string, + ): Promise<{ stdout: string; stderr: string }> { return this.executeCompactCommand(command, 'Test operation failed'); } - protected createError(message: string, cause?: unknown): Error { + protected createError(message: string, _cause?: unknown): Error { return new Error(message); } } @@ -75,7 +75,7 @@ class TestCompactOperation extends BaseCompactOperation { } async execute(): Promise { - const { files } = await this.discoverFiles(); + //const { files } = await this.discoverFiles(); return Promise.resolve(); } @@ -134,7 +134,9 @@ describe('BaseEnvironmentValidator', () => { it('throws CompactCliNotFoundError when CLI is not available', async () => { mockExec.mockRejectedValue(new Error('Command not found')); - await expect(validator.validateBase()).rejects.toThrow(CompactCliNotFoundError); + await expect(validator.validateBase()).rejects.toThrow( + CompactCliNotFoundError, + ); }); }); }); @@ -143,7 +145,7 @@ describe('FileDiscovery', () => { let fileDiscovery: FileDiscovery; let mockReaddir: Mock; - beforeEach( async() => { + beforeEach(async () => { fileDiscovery = new FileDiscovery(); mockReaddir = vi.mocked(await import('node:fs/promises')).readdir; }); @@ -151,11 +153,19 @@ describe('FileDiscovery', () => { it('discovers .compact files recursively', async () => { mockReaddir .mockResolvedValueOnce([ - { name: 'MyToken.compact', isFile: () => true, isDirectory: () => false }, + { + name: 'MyToken.compact', + isFile: () => true, + isDirectory: () => false, + }, { name: 'access', isFile: () => false, isDirectory: () => true }, ] as any) .mockResolvedValueOnce([ - { name: 'AccessControl.compact', isFile: () => true, isDirectory: () => false }, + { + name: 'AccessControl.compact', + isFile: () => true, + isDirectory: () => false, + }, ] as any); const files = await fileDiscovery.getCompactFiles('src'); @@ -212,20 +222,20 @@ describe('BaseCompactService', () => { }); it('handles command execution errors', async () => { - const errMsg = 'Command failed' + const errMsg = 'Command failed'; mockExec.mockRejectedValue(new Error(errMsg)); await expect(service.testCommand('compact test')).rejects.toThrow( - `Test operation failed: ${errMsg}` + `Test operation failed: ${errMsg}`, ); }); it('handles non-Error rejections', async () => { - const otherMsg = 'String error' + const otherMsg = 'String error'; mockExec.mockRejectedValue(otherMsg); await expect(service.testCommand('compact test')).rejects.toThrow( - `Test operation failed: ${otherMsg}` + `Test operation failed: ${otherMsg}`, ); }); }); @@ -243,7 +253,9 @@ describe('SharedUIService', () => { // split, filter, map SharedUIService.printOutput('line1\nline2\n\nline3', colorFn); - expect(consoleSpy).toHaveBeenCalledWith('colored: line1\n line2\n line3'); + expect(consoleSpy).toHaveBeenCalledWith( + 'colored: line1\n line2\n line3', + ); consoleSpy.mockRestore(); }); @@ -262,7 +274,7 @@ describe('SharedUIService', () => { const testData = { operation: 'TEST', version: 'compact 0.2.0', - targetDir: 'security' + targetDir: 'security', }; const { operation, version, targetDir } = testData; @@ -273,8 +285,14 @@ describe('SharedUIService', () => { expect(ora).toHaveBeenCalled(); expect(mockSpinner.info).toHaveBeenCalledTimes(2); - expect(mockSpinner.info).toHaveBeenNthCalledWith(1, `[${operation}] TARGET_DIR: ${targetDir}`); - expect(mockSpinner.info).toHaveBeenNthCalledWith(2, `[${operation}] Compact developer tools: ${version}`); + expect(mockSpinner.info).toHaveBeenNthCalledWith( + 1, + `[${operation}] TARGET_DIR: ${targetDir}`, + ); + expect(mockSpinner.info).toHaveBeenNthCalledWith( + 2, + `[${operation}] Compact developer tools: ${version}`, + ); }); it('displays environment info without target directory', () => { @@ -283,7 +301,9 @@ describe('SharedUIService', () => { SharedUIService.displayBaseEnvInfo(operation, version); expect(ora).toHaveBeenCalled(); - expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Compact developer tools: ${version}`); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Compact developer tools: ${version}`, + ); }); }); @@ -292,16 +312,23 @@ describe('SharedUIService', () => { operation: 'TEST', action: 'compact 0.2.0', fileCount: 5, - targetDir: 'security' + targetDir: 'security', }; const { operation, action, fileCount, targetDir } = testData; it('displays operation start message with targetDir', () => { vi.mocked(ora).mockReturnValue(mockSpinner as any); - SharedUIService.showOperationStart(operation, action, fileCount, targetDir); + SharedUIService.showOperationStart( + operation, + action, + fileCount, + targetDir, + ); expect(ora).toHaveBeenCalled(); - expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Found ${fileCount} .compact file(s) to ${action} in ${targetDir}/`); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Found ${fileCount} .compact file(s) to ${action} in ${targetDir}/`, + ); }); it('displays operation start message without targetDir', () => { @@ -311,14 +338,16 @@ describe('SharedUIService', () => { SharedUIService.showOperationStart(operation, action, fileCount); expect(ora).toHaveBeenCalled(); - expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith(`[${operation}] Found ${fileCount} .compact file(s) to ${action}`); + expect(mockSpinner.info).toHaveBeenCalledExactlyOnceWith( + `[${operation}] Found ${fileCount} .compact file(s) to ${action}`, + ); }); }); describe('showNoFiles', () => { const testData = { operation: 'TEST', - targetDir: 'security' + targetDir: 'security', }; const { operation, targetDir } = testData; @@ -328,7 +357,9 @@ describe('SharedUIService', () => { SharedUIService.showNoFiles(operation, targetDir); expect(ora).toHaveBeenCalled(); - expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith(`[${operation}] No .compact files found in ${targetDir}/.`); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith( + `[${operation}] No .compact files found in ${targetDir}/.`, + ); }); it('shows no files warning without targetDir', () => { @@ -337,7 +368,9 @@ describe('SharedUIService', () => { SharedUIService.showNoFiles(operation); expect(ora).toHaveBeenCalled(); - expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith(`[${operation}] No .compact files found in src/.`); + expect(mockSpinner.warn).toHaveBeenCalledExactlyOnceWith( + `[${operation}] No .compact files found in src/.`, + ); }); }); @@ -348,12 +381,27 @@ describe('SharedUIService', () => { SharedUIService.showAvailableDirectories(operation); - expect(consoleSpy).toHaveBeenNthCalledWith(1, '\nAvailable directories:') - expect(consoleSpy).toHaveBeenNthCalledWith(2, ` --dir access # ${operation} access control contracts`) - expect(consoleSpy).toHaveBeenNthCalledWith(3, ` --dir archive # ${operation} archive contracts`) - expect(consoleSpy).toHaveBeenNthCalledWith(4, ` --dir security # ${operation} security contracts`) - expect(consoleSpy).toHaveBeenNthCalledWith(5, ` --dir token # ${operation} token contracts`) - expect(consoleSpy).toHaveBeenNthCalledWith(6, ` --dir utils # ${operation} utility contracts`) + expect(consoleSpy).toHaveBeenNthCalledWith(1, '\nAvailable directories:'); + expect(consoleSpy).toHaveBeenNthCalledWith( + 2, + ` --dir access # ${operation} access control contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 3, + ` --dir archive # ${operation} archive contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 4, + ` --dir security # ${operation} security contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 5, + ` --dir token # ${operation} token contracts`, + ); + expect(consoleSpy).toHaveBeenNthCalledWith( + 6, + ` --dir utils # ${operation} utility contracts`, + ); consoleSpy.mockRestore(); }); }); @@ -372,22 +420,31 @@ describe('BaseCompactOperation', () => { it('passes when target directory exists', () => { mockExistsSync.mockReturnValue(true); - expect(() => operation['validateTargetDirectory']('src/security')).not.toThrow(); + expect(() => + operation['validateTargetDirectory']('src/security'), + ).not.toThrow(); }); it('throws when target directory does not exist', () => { mockExistsSync.mockReturnValue(false); const missingDir = 'src/missingDir'; - const expErr = new DirectoryNotFoundError(`Target directory ${missingDir} does not exist`, missingDir); + const expErr = new DirectoryNotFoundError( + `Target directory ${missingDir} does not exist`, + missingDir, + ); - expect(() => operation['validateTargetDirectory'](missingDir)).toThrow(expErr); + expect(() => operation['validateTargetDirectory'](missingDir)).toThrow( + expErr, + ); }); it('does not validate when no target directory is set', () => { const noTargetOperation = new TestCompactOperation(); mockExistsSync.mockReturnValue(false); - expect(() => noTargetOperation['validateTargetDirectory']('src')).not.toThrow(); + expect(() => + noTargetOperation['validateTargetDirectory']('src'), + ).not.toThrow(); }); }); @@ -420,7 +477,7 @@ describe('BaseCompactOperation', () => { const args = ['--dir']; expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( - '--dir flag requires a directory name' + '--dir flag requires a directory name', ); }); @@ -428,7 +485,7 @@ describe('BaseCompactOperation', () => { const args = ['--dir', '--other-flag']; expect(() => BaseCompactOperation['parseBaseArgs'](args)).toThrow( - '--dir flag requires a directory name' + '--dir flag requires a directory name', ); }); @@ -460,25 +517,33 @@ describe('BaseErrorHandler', () => { it('handles CompactCliNotFoundError', () => { const error = new CompactCliNotFoundError('CLI not found'); - const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); expect(result).toBe(true); expect(mockSpinner.fail).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Error: CLI not found`) + expect.stringContaining(`[${operation}] Error: CLI not found`), ); expect(mockSpinner.info).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Install with:`) + expect.stringContaining(`[${operation}] Install with:`), ); }); it('handles DirectoryNotFoundError', () => { const error = new DirectoryNotFoundError('Directory not found', '/path'); - const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); expect(result).toBe(true); expect(mockSpinner.fail).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Error: Directory not found`) + expect.stringContaining(`[${operation}] Error: Directory not found`), ); }); @@ -489,29 +554,43 @@ describe('BaseErrorHandler', () => { code: 1, }); - const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); expect(result).toBe(true); expect(mockSpinner.fail).toHaveBeenCalledExactlyOnceWith( - expect.stringContaining(`[${operation}] Environment validation failed`) + expect.stringContaining(`[${operation}] Environment validation failed`), ); }); it('handles --dir argument parsing errors', () => { const error = new Error('--dir flag requires a directory name'); - const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); expect(result).toBe(false); // Should let specific handler show usage expect(mockSpinner.fail).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Error: --dir flag requires a directory name`) + expect.stringContaining( + `[${operation}] Error: --dir flag requires a directory name`, + ), ); }); it('returns false for unhandled errors', () => { const error = new Error('Some other error'); - const result = BaseErrorHandler.handleCommonErrors(error, mockSpinner, operation); + const result = BaseErrorHandler.handleCommonErrors( + error, + mockSpinner, + operation, + ); expect(result).toBe(false); }); @@ -526,7 +605,9 @@ describe('BaseErrorHandler', () => { BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); expect(mockSpinner.fail).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Unexpected error: Unexpected error`) + expect.stringContaining( + `[${operation}] Unexpected error: Unexpected error`, + ), ); }); @@ -536,7 +617,9 @@ describe('BaseErrorHandler', () => { BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); expect(mockSpinner.fail).toHaveBeenCalledWith( - expect.stringContaining(`[${operation}] Unexpected error: String error`) + expect.stringContaining( + `[${operation}] Unexpected error: String error`, + ), ); }); @@ -547,7 +630,7 @@ describe('BaseErrorHandler', () => { BaseErrorHandler.handleUnexpectedError(error, mockSpinner, operation); expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('If this error persists') + expect.stringContaining('If this error persists'), ); consoleSpy.mockRestore(); }); diff --git a/compact/test/Compiler.test.ts b/compact/test/Compiler.test.ts index 7d89343d..d8cca6f9 100644 --- a/compact/test/Compiler.test.ts +++ b/compact/test/Compiler.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { join } from 'node:path'; import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { ARTIFACTS_DIR, SRC_DIR } from '../src/BaseServices.js'; import { + CompactCompiler, CompilerEnvironmentValidator, CompilerService, CompilerUIService, - CompactCompiler, } from '../src/Compiler.js'; -import { SRC_DIR, ARTIFACTS_DIR } from '../src/BaseServices.js'; import { CompilationError } from '../src/types/errors.js'; // Mock dependencies @@ -43,12 +43,12 @@ describe('CompilerEnvironmentValidator', () => { it('returns default toolchain version', async () => { const testData = { expectedOutput: 'Compactc version: 0.24.0', - expectedCommand: 'compact compile --version' + expectedCommand: 'compact compile --version', }; mockExec.mockResolvedValue({ stdout: ` ${testData.expectedOutput} \n`, - stderr: '' + stderr: '', }); const version = await validator.getToolchainVersion(); @@ -61,12 +61,12 @@ describe('CompilerEnvironmentValidator', () => { const testData = { version: '0.25.0', expectedOutput: 'Compactc version: 0.25.0', - expectedCommand: 'compact compile +0.25.0 --version' + expectedCommand: 'compact compile +0.25.0 --version', }; mockExec.mockResolvedValue({ stdout: testData.expectedOutput, - stderr: '' + stderr: '', }); const version = await validator.getToolchainVersion(testData.version); @@ -80,19 +80,22 @@ describe('CompilerEnvironmentValidator', () => { it('returns both dev tools and toolchain versions', async () => { const testData = { devToolsVersion: 'compact 0.2.0', - toolchainVersion: 'Compactc version: 0.24.0' + toolchainVersion: 'Compactc version: 0.24.0', }; mockExec .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) - .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); + .mockResolvedValueOnce({ + stdout: testData.toolchainVersion, + stderr: '', + }); const result = await validator.validate(); expect(result).toEqual({ devToolsVersion: testData.devToolsVersion, - toolchainVersion: testData.toolchainVersion + toolchainVersion: testData.toolchainVersion, }); }); @@ -100,17 +103,22 @@ describe('CompilerEnvironmentValidator', () => { const testData = { version: '0.25.0', devToolsVersion: 'compact 0.2.0', - toolchainVersion: 'Compactc version: 0.25.0' + toolchainVersion: 'Compactc version: 0.25.0', }; mockExec .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) - .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); + .mockResolvedValueOnce({ + stdout: testData.toolchainVersion, + stderr: '', + }); await validator.validate(testData.version); - expect(mockExec).toHaveBeenCalledWith('compact compile +0.25.0 --version'); + expect(mockExec).toHaveBeenCalledWith( + 'compact compile +0.25.0 --version', + ); }); }); }); @@ -132,12 +140,17 @@ describe('CompilerService', () => { version: '0.24.0', expectedInputPath: join(SRC_DIR, 'contracts/MyToken.compact'), expectedOutputDir: join(ARTIFACTS_DIR, 'MyToken'), - expectedCommand: 'compact compile +0.24.0 --skip-zk --verbose "src/contracts/MyToken.compact" "artifacts/MyToken"' + expectedCommand: + 'compact compile +0.24.0 --skip-zk --verbose "src/contracts/MyToken.compact" "artifacts/MyToken"', }; mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); - await service.compileFile(testData.file, testData.flags, testData.version); + await service.compileFile( + testData.file, + testData.flags, + testData.version, + ); expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); }); @@ -146,7 +159,8 @@ describe('CompilerService', () => { const testData = { file: 'MyToken.compact', flags: '--skip-zk', - expectedCommand: 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"' + expectedCommand: + 'compact compile --skip-zk "src/MyToken.compact" "artifacts/MyToken"', }; mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); @@ -160,7 +174,8 @@ describe('CompilerService', () => { const testData = { file: 'MyToken.compact', flags: '', - expectedCommand: 'compact compile "src/MyToken.compact" "artifacts/MyToken"' + expectedCommand: + 'compact compile "src/MyToken.compact" "artifacts/MyToken"', }; mockExec.mockResolvedValue({ stdout: 'Success', stderr: '' }); @@ -174,20 +189,20 @@ describe('CompilerService', () => { const testData = { file: 'MyToken.compact', flags: '--skip-zk', - errorMessage: 'Syntax error on line 10' + errorMessage: 'Syntax error on line 10', }; mockExec.mockRejectedValue(new Error(testData.errorMessage)); await expect( - service.compileFile(testData.file, testData.flags) + service.compileFile(testData.file, testData.flags), ).rejects.toThrow(CompilationError); }); it('CompilationError includes file name', async () => { const testData = { file: 'contracts/MyToken.compact', - flags: '--skip-zk' + flags: '--skip-zk', }; mockExec.mockRejectedValue(new Error('Compilation failed')); @@ -205,7 +220,7 @@ describe('CompilerService', () => { it('extracts file name from error message', () => { const testData = { message: 'Failed to compile contracts/MyToken.compact: Syntax error', - expectedFile: 'contracts/MyToken.compact' + expectedFile: 'contracts/MyToken.compact', }; const error = service['createError'](testData.message); @@ -217,7 +232,7 @@ describe('CompilerService', () => { it('uses "unknown" when file name cannot be extracted', () => { const testData = { message: 'Some generic error message', - expectedFile: 'unknown' + expectedFile: 'unknown', }; const error = service['createError'](testData.message); @@ -233,7 +248,7 @@ describe('CompilerUIService', () => { beforeEach(() => { mockSpinner = { - info: vi.fn() + info: vi.fn(), }; vi.mocked(ora).mockReturnValue(mockSpinner); }); @@ -244,37 +259,53 @@ describe('CompilerUIService', () => { devToolsVersion: 'compact 0.2.0', toolchainVersion: 'Compactc version: 0.24.0', targetDir: 'security', - version: '0.24.0' + version: '0.24.0', }; CompilerUIService.displayEnvInfo( testData.devToolsVersion, testData.toolchainVersion, testData.targetDir, - testData.version + testData.version, ); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] TARGET_DIR: security'); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact developer tools: compact 0.2.0'); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact toolchain: Compactc version: 0.24.0'); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Using toolchain version: 0.24.0'); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] TARGET_DIR: security', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] Compact developer tools: compact 0.2.0', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] Compact toolchain: Compactc version: 0.24.0', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] Using toolchain version: 0.24.0', + ); }); it('displays minimal environment information', () => { const testData = { devToolsVersion: 'compact 0.2.0', - toolchainVersion: 'Compactc version: 0.24.0' + toolchainVersion: 'Compactc version: 0.24.0', }; CompilerUIService.displayEnvInfo( testData.devToolsVersion, - testData.toolchainVersion + testData.toolchainVersion, ); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact developer tools: compact 0.2.0'); - expect(mockSpinner.info).toHaveBeenCalledWith('[COMPILE] Compact toolchain: Compactc version: 0.24.0'); - expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('TARGET_DIR')); - expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('Using toolchain version')); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] Compact developer tools: compact 0.2.0', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[COMPILE] Compact toolchain: Compactc version: 0.24.0', + ); + expect(mockSpinner.info).not.toHaveBeenCalledWith( + expect.stringContaining('TARGET_DIR'), + ); + expect(mockSpinner.info).not.toHaveBeenCalledWith( + expect.stringContaining('Using toolchain version'), + ); }); }); }); @@ -301,14 +332,14 @@ describe('CompactCompiler', () => { const testData = { flags: '--skip-zk --verbose', targetDir: 'security', - version: '0.24.0' + version: '0.24.0', }; compiler = new CompactCompiler( testData.flags, testData.targetDir, testData.version, - mockExec + mockExec, ); expect(compiler.testFlags).toBe(testData.flags); @@ -319,7 +350,7 @@ describe('CompactCompiler', () => { it('trims flags parameter', () => { const testData = { inputFlags: ' --skip-zk --verbose ', - expectedFlags: '--skip-zk --verbose' + expectedFlags: '--skip-zk --verbose', }; compiler = new CompactCompiler(testData.inputFlags); @@ -340,7 +371,7 @@ describe('CompactCompiler', () => { it('parses SKIP_ZK environment variable', () => { const testData = { env: { SKIP_ZK: 'true' }, - expectedFlags: '--skip-zk' + expectedFlags: '--skip-zk', }; compiler = CompactCompiler.fromArgs([], testData.env); @@ -351,7 +382,7 @@ describe('CompactCompiler', () => { it('ignores SKIP_ZK when not "true"', () => { const testData = { env: { SKIP_ZK: 'false' }, - expectedFlags: '' + expectedFlags: '', }; compiler = CompactCompiler.fromArgs([], testData.env); @@ -365,7 +396,7 @@ describe('CompactCompiler', () => { env: {}, expectedTargetDir: 'security', expectedFlags: '--skip-zk --verbose', - expectedVersion: '0.24.0' + expectedVersion: '0.24.0', }; compiler = CompactCompiler.fromArgs(testData.args, testData.env); @@ -379,7 +410,7 @@ describe('CompactCompiler', () => { const testData = { args: ['--dir', 'access', '--verbose'], env: { SKIP_ZK: 'true' }, - expectedFlags: '--skip-zk --verbose' + expectedFlags: '--skip-zk --verbose', }; compiler = CompactCompiler.fromArgs(testData.args, testData.env); @@ -391,7 +422,7 @@ describe('CompactCompiler', () => { const testData = { args: ['--skip-zk', '--verbose'], env: { SKIP_ZK: 'true' }, - expectedFlags: '--skip-zk --verbose' + expectedFlags: '--skip-zk --verbose', }; compiler = CompactCompiler.fromArgs(testData.args, testData.env); @@ -401,13 +432,13 @@ describe('CompactCompiler', () => { it('throws error for --dir without argument', () => { expect(() => CompactCompiler.fromArgs(['--dir'])).toThrow( - '--dir flag requires a directory name' + '--dir flag requires a directory name', ); }); it('throws error for --dir followed by another flag', () => { expect(() => CompactCompiler.fromArgs(['--dir', '--skip-zk'])).toThrow( - '--dir flag requires a directory name' + '--dir flag requires a directory name', ); }); }); @@ -418,17 +449,27 @@ describe('CompactCompiler', () => { devToolsVersion: 'compact 0.2.0', toolchainVersion: 'Compactc version: 0.24.0', targetDir: 'security', - version: '0.24.0' + version: '0.24.0', }; mockExec .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) - .mockResolvedValueOnce({ stdout: testData.toolchainVersion, stderr: '' }); + .mockResolvedValueOnce({ + stdout: testData.toolchainVersion, + stderr: '', + }); - const displaySpy = vi.spyOn(CompilerUIService, 'displayEnvInfo').mockImplementation(() => {}); + const displaySpy = vi + .spyOn(CompilerUIService, 'displayEnvInfo') + .mockImplementation(() => {}); - compiler = new CompactCompiler('--skip-zk', testData.targetDir, testData.version, mockExec); + compiler = new CompactCompiler( + '--skip-zk', + testData.targetDir, + testData.version, + mockExec, + ); await compiler.validateEnvironment(); @@ -436,7 +477,7 @@ describe('CompactCompiler', () => { testData.devToolsVersion, testData.toolchainVersion, testData.targetDir, - testData.version + testData.version, ); displaySpy.mockRestore(); diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index e155a29a..4457c3fc 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -1,6 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CompactCompiler } from '../src/Compiler.js'; import { BaseErrorHandler } from '../src/BaseServices.js'; +import { CompactCompiler } from '../src/Compiler.js'; import { CompactCliNotFoundError, CompilationError, @@ -74,7 +74,9 @@ describe('runCompiler CLI', () => { mockCompile = vi.fn(); mockFromArgs = vi.mocked(CompactCompiler.fromArgs); mockHandleCommonErrors = vi.mocked(BaseErrorHandler.handleCommonErrors); - mockHandleUnexpectedError = vi.mocked(BaseErrorHandler.handleUnexpectedError); + mockHandleUnexpectedError = vi.mocked( + BaseErrorHandler.handleUnexpectedError, + ); // Mock CompactCompiler instance mockFromArgs.mockReturnValue({ @@ -97,7 +99,7 @@ describe('runCompiler CLI', () => { describe('successful compilation', () => { it('compiles successfully with no arguments', async () => { const testData = { - expectedArgs: [] + expectedArgs: [], }; mockCompile.mockResolvedValue(undefined); @@ -113,7 +115,13 @@ describe('runCompiler CLI', () => { it('compiles successfully with arguments', async () => { const testData = { args: ['--dir', 'security', '--skip-zk'], - processArgv: ['node', 'runCompiler.js', '--dir', 'security', '--skip-zk'] + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'security', + '--skip-zk', + ], }; process.argv = testData.processArgv; @@ -131,7 +139,7 @@ describe('runCompiler CLI', () => { it('delegates to BaseErrorHandler.handleCommonErrors first', async () => { const testData = { error: new CompactCliNotFoundError('CLI not found'), - operation: 'COMPILE' + operation: 'COMPILE', }; mockHandleCommonErrors.mockReturnValue(true); // Indicates error was handled @@ -142,7 +150,7 @@ describe('runCompiler CLI', () => { expect(mockHandleCommonErrors).toHaveBeenCalledWith( testData.error, expect.any(Object), // spinner - testData.operation + testData.operation, ); expect(mockExit).toHaveBeenCalledWith(1); }); @@ -150,7 +158,8 @@ describe('runCompiler CLI', () => { it('handles compiler-specific errors when BaseErrorHandler returns false', async () => { const testData = { error: new CompilationError('Compilation failed', 'MyToken.compact'), - expectedMessage: '[COMPILE] Compilation failed for file: MyToken.compact' + expectedMessage: + '[COMPILE] Compilation failed for file: MyToken.compact', }; mockHandleCommonErrors.mockReturnValue(false); // Not handled by base @@ -166,7 +175,7 @@ describe('runCompiler CLI', () => { it('handles CompilationError with unknown file', async () => { const testData = { error: new CompilationError('Compilation failed', ''), - expectedMessage: '[COMPILE] Compilation failed for file: unknown' + expectedMessage: '[COMPILE] Compilation failed for file: unknown', }; mockHandleCommonErrors.mockReturnValue(false); @@ -179,7 +188,7 @@ describe('runCompiler CLI', () => { it('shows usage help for argument parsing errors', async () => { const testData = { - error: new Error('--dir flag requires a directory name') + error: new Error('--dir flag requires a directory name'), }; mockHandleCommonErrors.mockReturnValue(false); @@ -187,7 +196,9 @@ describe('runCompiler CLI', () => { await import('../src/runCompiler.js'); - expect(mockConsoleLog).toHaveBeenCalledWith('\nUsage: compact-compiler [options]'); + expect(mockConsoleLog).toHaveBeenCalledWith( + '\nUsage: compact-compiler [options]', + ); expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); expect(mockExit).toHaveBeenCalledWith(1); }); @@ -195,7 +206,7 @@ describe('runCompiler CLI', () => { it('delegates unexpected errors to BaseErrorHandler', async () => { const testData = { error: new Error('Unexpected error'), - operation: 'COMPILE' + operation: 'COMPILE', }; mockHandleCommonErrors.mockReturnValue(false); @@ -206,7 +217,7 @@ describe('runCompiler CLI', () => { expect(mockHandleUnexpectedError).toHaveBeenCalledWith( testData.error, expect.any(Object), // spinner - testData.operation + testData.operation, ); }); }); @@ -216,14 +227,14 @@ describe('runCompiler CLI', () => { const testData = { execError: { stderr: 'Detailed error output', - stdout: 'some output' - } + stdout: 'some output', + }, }; const compilationError = new CompilationError( 'Compilation failed', 'MyToken.compact', - testData.execError + testData.execError, ); mockHandleCommonErrors.mockReturnValue(false); @@ -233,7 +244,9 @@ describe('runCompiler CLI', () => { await import('../src/runCompiler.js'); expect(mockConsoleLog).toHaveBeenCalledWith( - expect.stringContaining('Additional error details: Detailed error output') + expect.stringContaining( + 'Additional error details: Detailed error output', + ), ); }); @@ -241,14 +254,14 @@ describe('runCompiler CLI', () => { const testData = { execError: { stderr: 'Error: stdout and stderr already displayed', - stdout: 'some output' - } + stdout: 'some output', + }, }; const compilationError = new CompilationError( 'Compilation failed', 'MyToken.compact', - testData.execError + testData.execError, ); mockHandleCommonErrors.mockReturnValue(false); @@ -258,7 +271,7 @@ describe('runCompiler CLI', () => { await import('../src/runCompiler.js'); expect(mockConsoleLog).not.toHaveBeenCalledWith( - expect.stringContaining('Additional error details') + expect.stringContaining('Additional error details'), ); }); }); @@ -277,8 +290,8 @@ describe('runCompiler CLI', () => { ' compact-compiler # Compile all files', ' SKIP_ZK=true compact-compiler --dir token # Use environment variable', '\nTurbo integration:', - ' turbo compact # Full build' - ] + ' turbo compact # Full build', + ], }; mockHandleCommonErrors.mockReturnValue(false); @@ -286,7 +299,7 @@ describe('runCompiler CLI', () => { await import('../src/runCompiler.js'); - testData.expectedSections.forEach(section => { + testData.expectedSections.forEach((section) => { expect(mockConsoleLog).toHaveBeenCalledWith(section); }); }); @@ -300,7 +313,7 @@ describe('runCompiler CLI', () => { it('handles turbo compact', async () => { const testData = { processArgv: ['node', 'runCompiler.js'], - expectedArgs: [] + expectedArgs: [], }; process.argv = testData.processArgv; @@ -313,7 +326,7 @@ describe('runCompiler CLI', () => { it('handles turbo compact:security', async () => { const testData = { processArgv: ['node', 'runCompiler.js', '--dir', 'security'], - expectedArgs: ['--dir', 'security'] + expectedArgs: ['--dir', 'security'], }; process.argv = testData.processArgv; @@ -340,7 +353,7 @@ describe('runCompiler CLI', () => { '--skip-zk', '--verbose', '+0.24.0', - ] + ], }; process.argv = testData.processArgv; @@ -355,7 +368,14 @@ describe('runCompiler CLI', () => { it('passes arguments correctly to CompactCompiler.fromArgs', async () => { const testData = { args: ['--dir', 'token', '--skip-zk', '+0.24.0'], - processArgv: ['node', 'runCompiler.js', '--dir', 'token', '--skip-zk', '+0.24.0'] + processArgv: [ + 'node', + 'runCompiler.js', + '--dir', + 'token', + '--skip-zk', + '+0.24.0', + ], }; process.argv = testData.processArgv; @@ -370,7 +390,7 @@ describe('runCompiler CLI', () => { it('handles fromArgs throwing errors', async () => { const testData = { - error: new Error('Invalid arguments') + error: new Error('Invalid arguments'), }; mockFromArgs.mockImplementation(() => { @@ -382,7 +402,7 @@ describe('runCompiler CLI', () => { expect(mockHandleCommonErrors).toHaveBeenCalledWith( testData.error, expect.any(Object), - 'COMPILE' + 'COMPILE', ); expect(mockExit).toHaveBeenCalledWith(1); }); From 0f68ac6e73f7c777c14f35d26e6cd4cb9b513dc5 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 23:09:41 -0300 Subject: [PATCH 19/44] rename fmt-and-lint -> check --- package.json | 6 +++--- turbo.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 338b47c7..6343eae6 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,9 @@ "compact": "turbo run compact --filter=@openzeppelin-compact/contracts", "build": "turbo run build --filter=!'docs'", "test": "turbo run test --filter=@openzeppelin-compact/contracts", - "fmt-and-lint": "biome check . --changed", - "fmt-and-lint:fix": "biome check . --changed --write", - "fmt-and-lint:ci": "biome ci . --changed --no-errors-on-unmatched", + "check": "biome check . --changed", + "check:fix": "biome check . --changed --write", + "check:ci": "biome ci . --changed --no-errors-on-unmatched", "types": "turbo run types --filter=!'docs'", "clean": "turbo run clean --filter=!'docs'" }, diff --git a/turbo.json b/turbo.json index b1fea330..b355e2ba 100644 --- a/turbo.json +++ b/turbo.json @@ -88,9 +88,9 @@ "outputs": [], "cache": false }, - "//#fmt-and-lint": {}, - "//#fmt-and-lint:ci": {}, - "//#fmt-and-lint:fix": { + "//#check": {}, + "//#check:ci": {}, + "//#check:fix": { "cache": false }, "clean": { From 8b0cbc3a54669f9fb6c0ccbd19133c2081831039 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 23:10:03 -0300 Subject: [PATCH 20/44] update CI with rename and compact fmt --- .github/workflows/checks.yml | 2 +- .github/workflows/test.yml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 944d14a1..808b3ee9 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -24,4 +24,4 @@ jobs: uses: ./.github/actions/setup - name: Format & Lint - run: turbo fmt-and-lint:ci + run: turbo check:ci diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7a2d626..bccd2fd0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,6 +79,9 @@ jobs: } compile + - name: Run Compact formatter + run: turbo format:check + - name: Run type checks run: turbo types From 4877d0405fbb225300af0635d40bc29aa5635330 Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 23:10:18 -0300 Subject: [PATCH 21/44] rename fmt-and-lint -> check --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f411b50c..e4a8554b 100644 --- a/README.md +++ b/README.md @@ -107,12 +107,12 @@ turbo test ### Format and lint files ```bash -turbo fmt-and-lint:fix +turbo check:fix ``` ### All together now! ```bash -turbo compact test fmt-and-lint:fix +turbo compact test check:fix ``` ## Security From d802c54b7c151906cc9cd4ad613cdc4d86ae3bfc Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 23:55:12 -0300 Subject: [PATCH 22/44] update readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4a8554b..ba1f02c5 100644 --- a/README.md +++ b/README.md @@ -104,12 +104,18 @@ SKIP_ZK=true turbo compact turbo test ``` -### Format and lint files +### Format and lint ts files ```bash turbo check:fix ``` +### Format Compact files + +```bash +turbo format:fix +``` + ### All together now! ```bash turbo compact test check:fix From 119feacefcecd69733b6ae3ed8abb20c2874ff4c Mon Sep 17 00:00:00 2001 From: andrew Date: Tue, 9 Sep 2025 23:56:01 -0300 Subject: [PATCH 23/44] make default format readonly --- compact/src/Formatter.ts | 33 +++++++++++++++------------------ contracts/package.json | 2 +- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 06fbdc72..63378e2b 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -56,10 +56,7 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { * Extends base service with format-specific command construction. */ export class FormatterService extends BaseCompactService { - /** - * Formats files in-place in the specified directory or current directory. - */ - async formatInPlace( + async formatAndWrite( targetPath?: string, ): Promise<{ stdout: string; stderr: string }> { const pathArg = targetPath ? ` "${targetPath}"` : ''; @@ -144,7 +141,7 @@ export const FormatterUIService = { */ showFormattingStart( fileCount: number, - mode: 'format' | 'check', + mode: 'check' | 'write', targetDir?: string, ): void { const action = mode === 'check' ? 'check formatting for' : 'format'; @@ -183,14 +180,14 @@ export const FormatterUIService = { export class CompactFormatter extends BaseCompactOperation { private readonly environmentValidator: FormatterEnvironmentValidator; private readonly formatterService: FormatterService; - private readonly checkMode: boolean; + private readonly writeMode: boolean; private readonly targets: string[]; /** * Creates a new CompactFormatter instance. */ constructor( - checkMode = false, + writeMode = false, targets: string[] = [], execFn?: ExecFunction, ) { @@ -201,7 +198,7 @@ export class CompactFormatter extends BaseCompactOperation { : undefined; super(targetDir); - this.checkMode = checkMode; + this.writeMode = writeMode; this.targets = targets; this.environmentValidator = new FormatterEnvironmentValidator(execFn); this.formatterService = new FormatterService(execFn); @@ -213,7 +210,7 @@ export class CompactFormatter extends BaseCompactOperation { static fromArgs(args: string[]): CompactFormatter { const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); - let checkMode = false; + let writeMode = false; const targets: string[] = []; // Add targetDir to targets if specified @@ -222,14 +219,14 @@ export class CompactFormatter extends BaseCompactOperation { } for (const arg of remainingArgs) { - if (arg === '--check') { - checkMode = true; + if (arg === '--write') { + writeMode = true; } else if (!arg.startsWith('--')) { targets.push(arg); } } - return new CompactFormatter(checkMode, targets); + return new CompactFormatter(writeMode, targets); } /** @@ -276,7 +273,7 @@ export class CompactFormatter extends BaseCompactOperation { * Formats specific files provided as arguments. */ private async formatSpecificFiles(): Promise { - if (this.checkMode) { + if (!this.writeMode) { for (const file of this.targets) { await this.checkFile(file); } @@ -294,14 +291,14 @@ export class CompactFormatter extends BaseCompactOperation { const { files, searchDir } = await this.discoverFiles(); if (files.length === 0) return; - const mode = this.checkMode ? 'check' : 'format'; + const mode = this.writeMode ? 'write' : 'check'; FormatterUIService.showFormattingStart(files.length, mode, this.targetDir); - if (this.checkMode) { + if (!this.writeMode) { const result = await this.formatterService.checkFormatting(searchDir); FormatterUIService.showCheckResults(result.isFormatted, result.stdout); } else { - const result = await this.formatterService.formatInPlace(searchDir); + const result = await this.formatterService.formatAndWrite(searchDir); // Successful formatting typically produces no output if (result.stdout.trim()) { @@ -339,8 +336,8 @@ export class CompactFormatter extends BaseCompactOperation { /** * For testing - expose internal state */ - get testCheckMode(): boolean { - return this.checkMode; + get testWriteMode(): boolean { + return this.writeMode; } get testTargets(): string[] { diff --git a/contracts/package.json b/contracts/package.json index 8cddb107..7fe1bda3 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -23,7 +23,7 @@ "build": "compact-builder && tsc", "test": "compact-compiler --skip-zk && vitest run", "format": "compact-formatter", - "format:check": "compact-formatter --check", + "format:fix": "compact-formatter --write", "types": "tsc -p tsconfig.json --noEmit", "clean": "git clean -fXd" }, From d4431e8d5fb511163162e23c569b69be176ee42e Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:15:02 -0300 Subject: [PATCH 24/44] improve error handling, fix success msg --- compact/src/Formatter.ts | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 63378e2b..884f863b 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -76,18 +76,15 @@ export class FormatterService extends BaseCompactService { const command = `compact format --check${pathArg}`; try { - const result = await this.executeCompactCommand( - command, - 'Failed to check formatting', - ); + const result = await this.executeCompactCommand(command, 'Failed to check formatting'); return { ...result, isFormatted: true }; } catch (error: unknown) { - if (isPromisifiedChildProcessError(error)) { - // Exit code 1 with formatting differences is expected behavior - if (error.code === 1 && error.stdout) { + if (error instanceof FormatterError && isPromisifiedChildProcessError(error.cause)) { + const childProcessError = error.cause; + if (childProcessError.code === 1 && childProcessError.stdout) { return { - stdout: error.stdout, - stderr: error.stderr || '', + stdout: childProcessError.stdout, + stderr: childProcessError.stderr || '', isFormatted: false, }; } @@ -247,7 +244,7 @@ export class CompactFormatter extends BaseCompactOperation { /** * Main formatting execution method. */ - async execute(): Promise { + async format(): Promise { await this.validateEnvironment(); // Handle specific file targets @@ -262,13 +259,6 @@ export class CompactFormatter extends BaseCompactOperation { return this.formatDirectory(); } - /** - * Legacy method name for backwards compatibility. - */ - async format(): Promise { - return this.execute(); - } - /** * Formats specific files provided as arguments. */ @@ -310,7 +300,7 @@ export class CompactFormatter extends BaseCompactOperation { const spinner = ora(); spinner.succeed( - chalk.green(`[FORMAT] Successfully formatted ${files.length} file(s)`), + chalk.green(`[FORMAT] Processed ${files.length} file(s)`), ); } } From 8f1c67ae43df84c7f07abb96ea897f32e674255f Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:16:02 -0300 Subject: [PATCH 25/44] remove execute method --- compact/src/BaseServices.ts | 1 - compact/src/Compiler.ts | 9 +-------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts index e0ad89e4..9e32852b 100644 --- a/compact/src/BaseServices.ts +++ b/compact/src/BaseServices.ts @@ -304,7 +304,6 @@ export abstract class BaseCompactOperation { * Abstract methods that must be implemented by subclasses. */ abstract validateEnvironment(): Promise; - abstract execute(): Promise; abstract showNoFiles(): void; /** diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index a0541409..5829803a 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -207,7 +207,7 @@ export class CompactCompiler extends BaseCompactOperation { /** * Main compilation execution method. */ - async execute(): Promise { + async compile(): Promise { await this.validateEnvironment(); const { files } = await this.discoverFiles(); @@ -220,13 +220,6 @@ export class CompactCompiler extends BaseCompactOperation { } } - /** - * Legacy method name for backwards compatibility. - */ - async compile(): Promise { - return this.execute(); - } - /** * Compiles a single file with progress reporting. */ From 5a015b3378c555addd2c83717b7a843cc1ee796e Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:16:22 -0300 Subject: [PATCH 26/44] fix format task --- turbo.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/turbo.json b/turbo.json index b355e2ba..261221fc 100644 --- a/turbo.json +++ b/turbo.json @@ -74,12 +74,12 @@ "outputs": ["dist/**"] }, "format": { - "dependsOn": ["^build"] - }, - "format:check": { "dependsOn": ["^build"], "cache": false }, + "format:fix": { + "dependsOn": ["^build"] + }, "types": { "dependsOn": [ "@openzeppelin-compact/compact#build", From 05f3bf34e058d6648e0ddd63dd2b36a3a260df5c Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:16:42 -0300 Subject: [PATCH 27/44] add formatter tests --- compact/test/Formatter.test.ts | 452 +++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 compact/test/Formatter.test.ts diff --git a/compact/test/Formatter.test.ts b/compact/test/Formatter.test.ts new file mode 100644 index 00000000..4ed111d9 --- /dev/null +++ b/compact/test/Formatter.test.ts @@ -0,0 +1,452 @@ +import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; +import { join } from 'node:path'; +import ora from 'ora'; +import { + FormatterEnvironmentValidator, + FormatterService, + FormatterUIService, + CompactFormatter, +} from '../src/Formatter.js'; +import { SRC_DIR } from '../src/BaseServices.js'; +import { + FormatterError, + FormatterNotAvailableError, +} from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('chalk', () => ({ + default: { + blue: vi.fn((text) => text), + green: vi.fn((text) => text), + red: vi.fn((text) => text), + yellow: vi.fn((text) => text), + cyan: vi.fn((text) => text), + white: vi.fn((text) => text), + }, +})); + +vi.mock('ora', () => ({ + default: vi.fn(() => ({ + start: vi.fn().mockReturnThis(), + succeed: vi.fn(), + fail: vi.fn(), + info: vi.fn(), + })), +})); + +describe('FormatterEnvironmentValidator', () => { + let validator: FormatterEnvironmentValidator; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + validator = new FormatterEnvironmentValidator(mockExec); + }); + + describe('checkFormatterAvailable', () => { + it('succeeds when formatter is available', async () => { + const testData = { + expectedCommand: 'compact help format', + response: { stdout: 'Format help text', stderr: '' } + }; + + mockExec.mockResolvedValue(testData.response); + + await expect(validator.checkFormatterAvailable()).resolves.not.toThrow(); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + }); + + it('throws FormatterNotAvailableError when formatter not available', async () => { + const testData = { + error: Object.assign(new Error('Command failed'), { + stderr: 'formatter not available', + stdout: '', + }) + }; + + mockExec.mockRejectedValue(testData.error); + await expect(validator.checkFormatterAvailable()).rejects.toThrow(FormatterNotAvailableError); + }); + + it('re-throws other errors', async () => { + const testData = { + error: new Error('Different error'), + }; + + mockExec.mockRejectedValue(testData.error); + + await expect(validator.checkFormatterAvailable()).rejects.toThrow(testData.error); + }); + }); + + describe('validate', () => { + it('returns dev tools version when validation succeeds', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + expectedResult: { devToolsVersion: 'compact 0.2.0' } + }; + + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) // checkCompactAvailable + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) // getDevToolsVersion + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); // checkFormatterAvailable + + const result = await validator.validate(); + + expect(result).toEqual(testData.expectedResult); + }); + }); +}); + +describe('FormatterService', () => { + let service: FormatterService; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + service = new FormatterService(mockExec); + }); + + describe('formatAndWrite', () => { + it('constructs correct command with target path', async () => { + const testData = { + targetPath: 'security', + expectedCommand: 'compact format "security"', + response: { stdout: 'Formatted successfully', stderr: '' } + }; + + mockExec.mockResolvedValue(testData.response); + + const result = await service.formatAndWrite(testData.targetPath); + + expect(result).toEqual(testData.response); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + }); + + it('constructs command without target path', async () => { + const testData = { + expectedCommand: 'compact format', + response: { stdout: 'Formatted successfully', stderr: '' } + }; + + mockExec.mockResolvedValue(testData.response); + + await service.formatAndWrite(); + + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + }); + + it('throws FormatterError on failure', async () => { + const testData = { + targetPath: 'security', + error: new Error('Format failed') + }; + + mockExec.mockRejectedValue(testData.error); + + await expect(service.formatAndWrite(testData.targetPath)).rejects.toThrow(FormatterError); + }); + }); + + describe('checkFormatting', () => { + it('returns true when no formatting needed', async () => { + const testData = { + targetPath: 'security', + expectedCommand: 'compact format --check "security"', + response: { stdout: 'All files formatted', stderr: '' }, + expectedResult: { + stdout: 'All files formatted', + stderr: '', + isFormatted: true + } + }; + + mockExec.mockResolvedValue(testData.response); + + const result = await service.checkFormatting(testData.targetPath); + + expect(result).toEqual(testData.expectedResult); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + }); + + it('returns false when formatting differences exist', async () => { + const testData = { + error: Object.assign(new Error('Formatting differences'), { + code: 1, + stdout: 'Differences found', + stderr: 'Formatting failed', + }), + expectedResult: { + stdout: 'Differences found', + stderr: 'Formatting failed', + isFormatted: false + } + }; + + mockExec.mockRejectedValue(testData.error); + const result = await service.checkFormatting(); + + expect(result).toEqual(testData.expectedResult); + }); + + it('throws FormatterError for unexpected failures', async () => { + const testData = { + error: new Error('Unexpected error') + }; + + mockExec.mockRejectedValue(testData.error); + + await expect(service.checkFormatting()).rejects.toThrow(FormatterError); + }); + + it('handles FormatterError with PromisifiedChildProcessError cause', async () => { + const childProcessError = Object.assign(new Error('Format check failed'), { + code: 1, + stdout: 'Differences found', + stderr: 'Formatting failed', + }); + + const formatterError = new FormatterError('Failed to check formatting', undefined, childProcessError); + + // Mock executeCompactCommand to throw the FormatterError + const executeSpy = vi.spyOn(service as any, 'executeCompactCommand'); + executeSpy.mockRejectedValue(formatterError); + + const result = await service.checkFormatting(); + + expect(result).toEqual({ + stdout: 'Differences found', + stderr: 'Formatting failed', + isFormatted: false + }); + + executeSpy.mockRestore(); + }); + }); + + describe('formatFiles', () => { + it('formats multiple files correctly', async () => { + const testData = { + files: ['MyToken.compact', 'Security.compact'], + expectedCommand: `compact format "${join(SRC_DIR, 'MyToken.compact')}" "${join(SRC_DIR, 'Security.compact')}"`, + response: { stdout: 'Files formatted', stderr: '' } + }; + + mockExec.mockResolvedValue(testData.response); + + const result = await service.formatFiles(testData.files); + + expect(result).toEqual(testData.response); + expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + }); + + it('returns empty result for empty file list', async () => { + const testData = { + files: [], + expectedResult: { stdout: '', stderr: '' } + }; + + const result = await service.formatFiles(testData.files); + + expect(result).toEqual(testData.expectedResult); + expect(mockExec).not.toHaveBeenCalled(); + }); + }); + + describe('createError', () => { + it('extracts target from error message', () => { + const testData = { + message: 'Failed to format security', + expectedTarget: 'security' + }; + + // Access protected method using bracket notation + const error = service['createError'](testData.message); + + expect(error).toBeInstanceOf(FormatterError); + expect((error as FormatterError).target).toBe(testData.expectedTarget); + }); + + it('extracts target from file error message', () => { + const testData = { + message: 'Failed to format files: MyToken.compact, Security.compact', + expectedTarget: 'MyToken.compact, Security.compact' + }; + + const error = service['createError'](testData.message); + + expect(error).toBeInstanceOf(FormatterError); + expect((error as FormatterError).target).toBe(testData.expectedTarget); + }); + + it('handles message without target', () => { + const testData = { + message: 'Some generic error', + expectedTarget: undefined + }; + + const error = service['createError'](testData.message); + + expect(error).toBeInstanceOf(FormatterError); + expect((error as FormatterError).target).toBe(testData.expectedTarget); + }); + }); +}); + +describe('FormatterUIService', () => { + let mockSpinner: any; + + beforeEach(() => { + vi.clearAllMocks(); + mockSpinner = { + info: vi.fn(), + succeed: vi.fn(), + fail: vi.fn(), + }; + vi.mocked(ora).mockReturnValue(mockSpinner); + }); + + describe('displayEnvInfo', () => { + it('displays environment information with target directory', () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + targetDir: 'security' + }; + + FormatterUIService.displayEnvInfo(testData.devToolsVersion, testData.targetDir); + + expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] TARGET_DIR: security'); + expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] Compact developer tools: compact 0.2.0'); + }); + + it('displays environment information without target directory', () => { + const testData = { + devToolsVersion: 'compact 0.2.0' + }; + + FormatterUIService.displayEnvInfo(testData.devToolsVersion); + + expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] Compact developer tools: compact 0.2.0'); + expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('TARGET_DIR')); + }); + }); + + describe('showCheckResults', () => { + it('shows success when files are formatted', () => { + const testData = { + isFormatted: true + }; + + FormatterUIService.showCheckResults(testData.isFormatted); + + expect(mockSpinner.succeed).toHaveBeenCalledWith('[FORMAT] All files are properly formatted'); + }); + + it('shows failure with differences when files need formatting', () => { + const testData = { + isFormatted: false, + differences: 'Some formatting differences' + }; + + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + + FormatterUIService.showCheckResults(testData.isFormatted, testData.differences); + + expect(mockSpinner.fail).toHaveBeenCalledWith('[FORMAT] Some files are not properly formatted'); + expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Formatting differences')); + + consoleSpy.mockRestore(); + }); + }); +}); + +describe('CompactFormatter', () => { + let formatter: CompactFormatter; + let mockExec: Mock; + + beforeEach(() => { + vi.clearAllMocks(); + mockExec = vi.fn(); + }); + + describe('constructor', () => { + it('creates instance with default parameters', () => { + formatter = new CompactFormatter(); + + expect(formatter).toBeInstanceOf(CompactFormatter); + expect(formatter.testWriteMode).toBe(false); + expect(formatter.testTargets).toEqual([]); + }); + + it('creates instance with all parameters', () => { + const testData = { + writeMode: true, + targets: ['security', 'MyToken.compact'] + }; + + formatter = new CompactFormatter(testData.writeMode, testData.targets, mockExec); + + expect(formatter.testWriteMode).toBe(testData.writeMode); + expect(formatter.testTargets).toEqual(testData.targets); + }); + }); + + describe('fromArgs', () => { + it('parses empty arguments', () => { + formatter = CompactFormatter.fromArgs([]); + + expect(formatter.testWriteMode).toBe(false); + expect(formatter.testTargets).toEqual([]); + }); + + it('parses --write flag', () => { + const testData = { + args: ['--write'], + expectedWriteMode: true + }; + + formatter = CompactFormatter.fromArgs(testData.args); + + expect(formatter.testWriteMode).toBe(testData.expectedWriteMode); + }); + + it('parses complex arguments', () => { + const testData = { + args: ['--dir', 'security', '--write', 'MyToken.compact'], + expectedTargets: ['security', 'MyToken.compact'], + expectedWriteMode: true + }; + + formatter = CompactFormatter.fromArgs(testData.args); + + expect(formatter.testWriteMode).toBe(testData.expectedWriteMode); + expect(formatter.testTargets).toEqual(testData.expectedTargets); + }); + }); + + describe('validateEnvironment', () => { + it('calls validator and displays environment info', async () => { + const testData = { + devToolsVersion: 'compact 0.2.0', + targetDir: 'security' + }; + + mockExec + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); + + const displaySpy = vi.spyOn(FormatterUIService, 'displayEnvInfo').mockImplementation(() => {}); + + formatter = new CompactFormatter(false, [testData.targetDir], mockExec); + + await formatter.validateEnvironment(); + + expect(displaySpy).toHaveBeenCalledWith(testData.devToolsVersion, testData.targetDir); + + displaySpy.mockRestore(); + }); + }); +}); From 279cd4bea54e665a42b0caf3b1232dae4ebb9a22 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:17:03 -0300 Subject: [PATCH 28/44] tidy up tests --- compact/test/BaseServices.test.ts | 13 +++---------- compact/test/Compiler.test.ts | 12 ------------ 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/compact/test/BaseServices.test.ts b/compact/test/BaseServices.test.ts index ffaa2f17..07900cc2 100644 --- a/compact/test/BaseServices.test.ts +++ b/compact/test/BaseServices.test.ts @@ -50,7 +50,7 @@ const mockSpinner = { succeed: vi.fn(), }; -// Concrete implementations for testing abstract classes +// Impls for abstract classes class TestEnvironmentValidator extends BaseEnvironmentValidator { async validate(): Promise<{ devToolsVersion: string }> { return this.validateBase(); @@ -70,14 +70,7 @@ class TestCompactService extends BaseCompactService { } class TestCompactOperation extends BaseCompactOperation { - async validateEnvironment(): Promise { - // Test implementation - } - - async execute(): Promise { - //const { files } = await this.discoverFiles(); - return Promise.resolve(); - } + async validateEnvironment(): Promise {} showNoFiles(): void { SharedUIService.showNoFiles('TEST', this.targetDir); @@ -250,7 +243,7 @@ describe('SharedUIService', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); const colorFn = (text: string) => `colored: ${text}`; - // split, filter, map + // Split, filter, map SharedUIService.printOutput('line1\nline2\n\nline3', colorFn); expect(consoleSpy).toHaveBeenCalledWith( diff --git a/compact/test/Compiler.test.ts b/compact/test/Compiler.test.ts index d8cca6f9..54014531 100644 --- a/compact/test/Compiler.test.ts +++ b/compact/test/Compiler.test.ts @@ -483,16 +483,4 @@ describe('CompactCompiler', () => { displaySpy.mockRestore(); }); }); - - describe('legacy compatibility', () => { - it('compile() method calls execute()', async () => { - compiler = new CompactCompiler('', undefined, undefined, mockExec); - const executeSpy = vi.spyOn(compiler, 'execute').mockResolvedValue(); - - await compiler.compile(); - - expect(executeSpy).toHaveBeenCalled(); - executeSpy.mockRestore(); - }); - }); }); From 2b066dfb1a5c3a32773ea953e8575d1a42c84ece Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:17:33 -0300 Subject: [PATCH 29/44] fix fmt --- compact/src/Formatter.ts | 10 ++- compact/test/Formatter.test.ts | 142 +++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 52 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 884f863b..b5ccc0e9 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -76,10 +76,16 @@ export class FormatterService extends BaseCompactService { const command = `compact format --check${pathArg}`; try { - const result = await this.executeCompactCommand(command, 'Failed to check formatting'); + const result = await this.executeCompactCommand( + command, + 'Failed to check formatting', + ); return { ...result, isFormatted: true }; } catch (error: unknown) { - if (error instanceof FormatterError && isPromisifiedChildProcessError(error.cause)) { + if ( + error instanceof FormatterError && + isPromisifiedChildProcessError(error.cause) + ) { const childProcessError = error.cause; if (childProcessError.code === 1 && childProcessError.stdout) { return { diff --git a/compact/test/Formatter.test.ts b/compact/test/Formatter.test.ts index 4ed111d9..cae9e840 100644 --- a/compact/test/Formatter.test.ts +++ b/compact/test/Formatter.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, vi, beforeEach, Mock } from 'vitest'; import { join } from 'node:path'; import ora from 'ora'; +import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'; +import { SRC_DIR } from '../src/BaseServices.js'; import { + CompactFormatter, FormatterEnvironmentValidator, FormatterService, FormatterUIService, - CompactFormatter, } from '../src/Formatter.js'; -import { SRC_DIR } from '../src/BaseServices.js'; import { FormatterError, FormatterNotAvailableError, @@ -48,7 +48,7 @@ describe('FormatterEnvironmentValidator', () => { it('succeeds when formatter is available', async () => { const testData = { expectedCommand: 'compact help format', - response: { stdout: 'Format help text', stderr: '' } + response: { stdout: 'Format help text', stderr: '' }, }; mockExec.mockResolvedValue(testData.response); @@ -62,11 +62,13 @@ describe('FormatterEnvironmentValidator', () => { error: Object.assign(new Error('Command failed'), { stderr: 'formatter not available', stdout: '', - }) + }), }; mockExec.mockRejectedValue(testData.error); - await expect(validator.checkFormatterAvailable()).rejects.toThrow(FormatterNotAvailableError); + await expect(validator.checkFormatterAvailable()).rejects.toThrow( + FormatterNotAvailableError, + ); }); it('re-throws other errors', async () => { @@ -76,7 +78,9 @@ describe('FormatterEnvironmentValidator', () => { mockExec.mockRejectedValue(testData.error); - await expect(validator.checkFormatterAvailable()).rejects.toThrow(testData.error); + await expect(validator.checkFormatterAvailable()).rejects.toThrow( + testData.error, + ); }); }); @@ -84,7 +88,7 @@ describe('FormatterEnvironmentValidator', () => { it('returns dev tools version when validation succeeds', async () => { const testData = { devToolsVersion: 'compact 0.2.0', - expectedResult: { devToolsVersion: 'compact 0.2.0' } + expectedResult: { devToolsVersion: 'compact 0.2.0' }, }; mockExec @@ -114,7 +118,7 @@ describe('FormatterService', () => { const testData = { targetPath: 'security', expectedCommand: 'compact format "security"', - response: { stdout: 'Formatted successfully', stderr: '' } + response: { stdout: 'Formatted successfully', stderr: '' }, }; mockExec.mockResolvedValue(testData.response); @@ -128,7 +132,7 @@ describe('FormatterService', () => { it('constructs command without target path', async () => { const testData = { expectedCommand: 'compact format', - response: { stdout: 'Formatted successfully', stderr: '' } + response: { stdout: 'Formatted successfully', stderr: '' }, }; mockExec.mockResolvedValue(testData.response); @@ -141,12 +145,14 @@ describe('FormatterService', () => { it('throws FormatterError on failure', async () => { const testData = { targetPath: 'security', - error: new Error('Format failed') + error: new Error('Format failed'), }; mockExec.mockRejectedValue(testData.error); - await expect(service.formatAndWrite(testData.targetPath)).rejects.toThrow(FormatterError); + await expect(service.formatAndWrite(testData.targetPath)).rejects.toThrow( + FormatterError, + ); }); }); @@ -159,8 +165,8 @@ describe('FormatterService', () => { expectedResult: { stdout: 'All files formatted', stderr: '', - isFormatted: true - } + isFormatted: true, + }, }; mockExec.mockResolvedValue(testData.response); @@ -181,8 +187,8 @@ describe('FormatterService', () => { expectedResult: { stdout: 'Differences found', stderr: 'Formatting failed', - isFormatted: false - } + isFormatted: false, + }, }; mockExec.mockRejectedValue(testData.error); @@ -193,7 +199,7 @@ describe('FormatterService', () => { it('throws FormatterError for unexpected failures', async () => { const testData = { - error: new Error('Unexpected error') + error: new Error('Unexpected error'), }; mockExec.mockRejectedValue(testData.error); @@ -202,13 +208,20 @@ describe('FormatterService', () => { }); it('handles FormatterError with PromisifiedChildProcessError cause', async () => { - const childProcessError = Object.assign(new Error('Format check failed'), { - code: 1, - stdout: 'Differences found', - stderr: 'Formatting failed', - }); + const childProcessError = Object.assign( + new Error('Format check failed'), + { + code: 1, + stdout: 'Differences found', + stderr: 'Formatting failed', + }, + ); - const formatterError = new FormatterError('Failed to check formatting', undefined, childProcessError); + const formatterError = new FormatterError( + 'Failed to check formatting', + undefined, + childProcessError, + ); // Mock executeCompactCommand to throw the FormatterError const executeSpy = vi.spyOn(service as any, 'executeCompactCommand'); @@ -219,7 +232,7 @@ describe('FormatterService', () => { expect(result).toEqual({ stdout: 'Differences found', stderr: 'Formatting failed', - isFormatted: false + isFormatted: false, }); executeSpy.mockRestore(); @@ -231,7 +244,7 @@ describe('FormatterService', () => { const testData = { files: ['MyToken.compact', 'Security.compact'], expectedCommand: `compact format "${join(SRC_DIR, 'MyToken.compact')}" "${join(SRC_DIR, 'Security.compact')}"`, - response: { stdout: 'Files formatted', stderr: '' } + response: { stdout: 'Files formatted', stderr: '' }, }; mockExec.mockResolvedValue(testData.response); @@ -245,7 +258,7 @@ describe('FormatterService', () => { it('returns empty result for empty file list', async () => { const testData = { files: [], - expectedResult: { stdout: '', stderr: '' } + expectedResult: { stdout: '', stderr: '' }, }; const result = await service.formatFiles(testData.files); @@ -259,7 +272,7 @@ describe('FormatterService', () => { it('extracts target from error message', () => { const testData = { message: 'Failed to format security', - expectedTarget: 'security' + expectedTarget: 'security', }; // Access protected method using bracket notation @@ -272,7 +285,7 @@ describe('FormatterService', () => { it('extracts target from file error message', () => { const testData = { message: 'Failed to format files: MyToken.compact, Security.compact', - expectedTarget: 'MyToken.compact, Security.compact' + expectedTarget: 'MyToken.compact, Security.compact', }; const error = service['createError'](testData.message); @@ -284,7 +297,7 @@ describe('FormatterService', () => { it('handles message without target', () => { const testData = { message: 'Some generic error', - expectedTarget: undefined + expectedTarget: undefined, }; const error = service['createError'](testData.message); @@ -312,50 +325,70 @@ describe('FormatterUIService', () => { it('displays environment information with target directory', () => { const testData = { devToolsVersion: 'compact 0.2.0', - targetDir: 'security' + targetDir: 'security', }; - FormatterUIService.displayEnvInfo(testData.devToolsVersion, testData.targetDir); - - expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] TARGET_DIR: security'); - expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] Compact developer tools: compact 0.2.0'); + FormatterUIService.displayEnvInfo( + testData.devToolsVersion, + testData.targetDir, + ); + + expect(mockSpinner.info).toHaveBeenCalledWith( + '[FORMAT] TARGET_DIR: security', + ); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[FORMAT] Compact developer tools: compact 0.2.0', + ); }); it('displays environment information without target directory', () => { const testData = { - devToolsVersion: 'compact 0.2.0' + devToolsVersion: 'compact 0.2.0', }; FormatterUIService.displayEnvInfo(testData.devToolsVersion); - expect(mockSpinner.info).toHaveBeenCalledWith('[FORMAT] Compact developer tools: compact 0.2.0'); - expect(mockSpinner.info).not.toHaveBeenCalledWith(expect.stringContaining('TARGET_DIR')); + expect(mockSpinner.info).toHaveBeenCalledWith( + '[FORMAT] Compact developer tools: compact 0.2.0', + ); + expect(mockSpinner.info).not.toHaveBeenCalledWith( + expect.stringContaining('TARGET_DIR'), + ); }); }); describe('showCheckResults', () => { it('shows success when files are formatted', () => { const testData = { - isFormatted: true + isFormatted: true, }; FormatterUIService.showCheckResults(testData.isFormatted); - expect(mockSpinner.succeed).toHaveBeenCalledWith('[FORMAT] All files are properly formatted'); + expect(mockSpinner.succeed).toHaveBeenCalledWith( + '[FORMAT] All files are properly formatted', + ); }); it('shows failure with differences when files need formatting', () => { const testData = { isFormatted: false, - differences: 'Some formatting differences' + differences: 'Some formatting differences', }; const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - FormatterUIService.showCheckResults(testData.isFormatted, testData.differences); + FormatterUIService.showCheckResults( + testData.isFormatted, + testData.differences, + ); - expect(mockSpinner.fail).toHaveBeenCalledWith('[FORMAT] Some files are not properly formatted'); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Formatting differences')); + expect(mockSpinner.fail).toHaveBeenCalledWith( + '[FORMAT] Some files are not properly formatted', + ); + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Formatting differences'), + ); consoleSpy.mockRestore(); }); @@ -383,10 +416,14 @@ describe('CompactFormatter', () => { it('creates instance with all parameters', () => { const testData = { writeMode: true, - targets: ['security', 'MyToken.compact'] + targets: ['security', 'MyToken.compact'], }; - formatter = new CompactFormatter(testData.writeMode, testData.targets, mockExec); + formatter = new CompactFormatter( + testData.writeMode, + testData.targets, + mockExec, + ); expect(formatter.testWriteMode).toBe(testData.writeMode); expect(formatter.testTargets).toEqual(testData.targets); @@ -404,7 +441,7 @@ describe('CompactFormatter', () => { it('parses --write flag', () => { const testData = { args: ['--write'], - expectedWriteMode: true + expectedWriteMode: true, }; formatter = CompactFormatter.fromArgs(testData.args); @@ -416,7 +453,7 @@ describe('CompactFormatter', () => { const testData = { args: ['--dir', 'security', '--write', 'MyToken.compact'], expectedTargets: ['security', 'MyToken.compact'], - expectedWriteMode: true + expectedWriteMode: true, }; formatter = CompactFormatter.fromArgs(testData.args); @@ -430,7 +467,7 @@ describe('CompactFormatter', () => { it('calls validator and displays environment info', async () => { const testData = { devToolsVersion: 'compact 0.2.0', - targetDir: 'security' + targetDir: 'security', }; mockExec @@ -438,13 +475,18 @@ describe('CompactFormatter', () => { .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); - const displaySpy = vi.spyOn(FormatterUIService, 'displayEnvInfo').mockImplementation(() => {}); + const displaySpy = vi + .spyOn(FormatterUIService, 'displayEnvInfo') + .mockImplementation(() => {}); formatter = new CompactFormatter(false, [testData.targetDir], mockExec); await formatter.validateEnvironment(); - expect(displaySpy).toHaveBeenCalledWith(testData.devToolsVersion, testData.targetDir); + expect(displaySpy).toHaveBeenCalledWith( + testData.devToolsVersion, + testData.targetDir, + ); displaySpy.mockRestore(); }); From 2f1082e94b4465b17478b8e5faab3d9f81b67bae Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 15:54:57 -0300 Subject: [PATCH 30/44] improve base services docs --- compact/src/BaseServices.ts | 346 ++++++++++++++++++++++++++++++++---- 1 file changed, 309 insertions(+), 37 deletions(-) diff --git a/compact/src/BaseServices.ts b/compact/src/BaseServices.ts index 9e32852b..e25ae28a 100644 --- a/compact/src/BaseServices.ts +++ b/compact/src/BaseServices.ts @@ -13,32 +13,63 @@ import { isPromisifiedChildProcessError, } from './types/errors.ts'; -/** Source directory containing .compact files */ +/** + * Default source directory containing .compact files. + * All Compact operations expect source files to be in this directory. + */ export const SRC_DIR: string = 'src'; -/** Output directory for compiled artifacts */ + +/** + * Default output directory for compiled artifacts. + * Compilation results are written to subdirectories within this path. + */ export const ARTIFACTS_DIR: string = 'artifacts'; /** - * Function type for executing shell commands. - * Allows dependency injection for testing and customization. + * Function signature for executing shell commands. + * + * Enables dependency injection for testing and allows customization + * of command execution behavior across different environments. + * + * @param command - The shell command to execute + * @returns Promise resolving to command output with stdout and stderr */ export type ExecFunction = ( command: string, ) => Promise<{ stdout: string; stderr: string }>; /** - * Base environment validator that handles common CLI validation. - * Extended by specific validators for compilation and formatting. + * Abstract base class for validating Compact CLI environment. + * + * Provides common validation logic shared across different Compact operations + * (compilation, formatting, etc.). Subclasses extend this with operation-specific + * validation requirements. + * + * @example + * ```typescript + * class CompilerValidator extends BaseEnvironmentValidator { + * async validate(version?: string) { + * const { devToolsVersion } = await this.validateBase(); + * const toolchainVersion = await this.getToolchainVersion(version); + * return { devToolsVersion, toolchainVersion }; + * } + * } + * ``` */ export abstract class BaseEnvironmentValidator { protected execFn: ExecFunction; + /** + * @param execFn - Command execution function (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. + * Tests whether the Compact CLI is available in the system PATH. + * + * @returns Promise resolving to true if CLI is accessible, false otherwise */ async checkCompactAvailable(): Promise { try { @@ -50,7 +81,10 @@ export abstract class BaseEnvironmentValidator { } /** - * Retrieves the version of the Compact developer tools. + * Retrieves the version string of the installed Compact developer tools. + * + * @returns Promise resolving to the trimmed version output + * @throws Error if the version command fails */ async getDevToolsVersion(): Promise { const { stdout } = await this.execFn('compact --version'); @@ -58,8 +92,13 @@ export abstract class BaseEnvironmentValidator { } /** - * Base validation that checks CLI availability. - * Override in subclasses for specific validation requirements. + * Performs base environment validation that all operations require. + * + * Verifies CLI availability and retrieves version information. + * Subclasses should call this before performing operation-specific validation. + * + * @returns Promise resolving to base validation results + * @throws CompactCliNotFoundError if CLI is not available in PATH */ async validateBase(): Promise<{ devToolsVersion: string }> { const isAvailable = await this.checkCompactAvailable(); @@ -74,20 +113,51 @@ export abstract class BaseEnvironmentValidator { } /** - * Abstract method for specific validation logic. - * Must be implemented by subclasses. + * Operation-specific validation logic. + * + * Subclasses must implement this to perform validation requirements + * specific to their operation (e.g., checking formatter availability, + * validating compiler versions). + * + * @param args - Variable arguments for operation-specific validation + * @returns Promise resolving to operation-specific validation results */ abstract validate(...args: any[]): Promise; } /** - * Shared file discovery service for both compilation and formatting. - * Recursively scans directories and filters for .compact file extensions. + * Service for discovering .compact files within a directory tree. + * + * Recursively scans directories and returns relative paths to all .compact files + * found. Used by both compilation and formatting operations to identify + * target files for processing. + * + * @example + * ```typescript + * const discovery = new FileDiscovery(); + * const files = await discovery.getCompactFiles('src/contracts'); + * // Returns: ['Token.compact', 'security/AccessControl.compact'] + * ``` */ export class FileDiscovery { /** - * Recursively discovers all .compact files in a directory. - * Returns relative paths from the SRC_DIR for consistent processing. + * Recursively discovers all .compact files within a directory. + * + * Returns paths relative to SRC_DIR for consistent processing across + * different operations. Gracefully handles access errors by logging + * warnings and continuing with remaining files. + * + * @param dir - Directory path to search (can be relative or absolute) + * @returns Promise resolving to array of relative file paths from SRC_DIR + * + * @example + * ```typescript + * // Search in specific subdirectory + * const files = await discovery.getCompactFiles('src/contracts'); + * + * // Search entire source tree + * const allFiles = await discovery.getCompactFiles('src'); + * ``` */ async getCompactFiles(dir: string): Promise { try { @@ -121,18 +191,56 @@ export class FileDiscovery { } /** - * Base service for executing Compact CLI commands. - * Provides common command execution patterns with error handling. + * Abstract base class for services that execute Compact CLI commands. + * + * Provides common patterns for command execution and error handling. + * Subclasses implement operation-specific command construction while + * inheriting consistent error handling and logging behavior. + * + * @example + * ```typescript + * class FormatterService extends BaseCompactService { + * async formatFiles(files: string[]) { + * const command = `compact format ${files.join(' ')}`; + * return this.executeCompactCommand(command, 'Failed to format files'); + * } + * + * protected createError(message: string, cause?: unknown): Error { + * return new FormatterError(message, cause); + * } + * } + * ``` */ export abstract class BaseCompactService { protected execFn: ExecFunction; + /** + * @param execFn - Command execution function (defaults to promisified child_process.exec) + */ constructor(execFn: ExecFunction = promisify(execCallback)) { this.execFn = execFn; } /** - * Executes a compact command and handles common error patterns. + * Executes a Compact CLI command with consistent error handling. + * + * Catches execution errors and wraps them in operation-specific error types + * using the createError method. Provides consistent error context across + * different operations. + * + * @param command - The complete command string to execute + * @param errorContext - Human-readable context for error messages + * @returns Promise resolving to command output + * @throws Operation-specific error (created by subclass createError method) + * + * @example + * ```typescript + * // In a subclass: + * const result = await this.executeCompactCommand( + * 'compact format --check src/', + * 'Failed to check formatting' + * ); + * ``` */ protected async executeCompactCommand( command: string, @@ -153,19 +261,44 @@ export abstract class BaseCompactService { } /** - * Abstract method for creating operation-specific errors. - * Must be implemented by subclasses. + * Creates operation-specific error instances. + * + * Subclasses must implement this to return appropriate error types + * (e.g., FormatterError, CompilationError) that provide operation-specific + * context and error handling behavior. + * + * @dev Mostly for edge cases that aren't picked up by the dev tool error handling. + * + * @param message - Error message describing what failed + * @param cause - Original error that triggered this failure (optional) + * @returns Error instance appropriate for the operation */ protected abstract createError(message: string, cause?: unknown): Error; } /** - * Shared UI service for consistent styling across compiler and formatter. - * Provides common output formatting and user feedback patterns. + * Shared UI utilities for consistent styling across Compact operations. + * + * Provides common output formatting, progress indicators, and user feedback + * patterns. Ensures all Compact tools have consistent visual appearance + * and behavior. */ export const SharedUIService = { /** - * Prints formatted output with consistent indentation and coloring. + * Formats command output with consistent indentation and coloring. + * + * Filters empty lines and adds 4-space indentation to create visually + * distinct output sections. Used for displaying stdout/stderr from + * Compact CLI commands. + * + * @param output - Raw output text to format + * @param colorFn - Chalk color function for styling the output + * + * @example + * ```typescript + * SharedUIService.printOutput(result.stdout, chalk.cyan); + * SharedUIService.printOutput(result.stderr, chalk.red); + * ``` */ printOutput(output: string, colorFn: (text: string) => string): void { const lines = output @@ -176,7 +309,15 @@ export const SharedUIService = { }, /** - * Displays base environment information. + * Displays base environment information common to all operations. + * + * Shows developer tools version and optional target directory. + * Called by operation-specific UI services to provide consistent + * environment context. + * + * @param operation - Operation name for message prefixes (e.g., 'COMPILE', 'FORMAT') + * @param devToolsVersion - Version string of installed Compact tools + * @param targetDir - Optional target directory being processed */ displayBaseEnvInfo( operation: string, @@ -195,7 +336,15 @@ export const SharedUIService = { }, /** - * Displays operation start message with file count. + * Displays operation start message with file count and location. + * + * Provides user feedback when beginning to process multiple files. + * Shows count of files found and optional location context. + * + * @param operation - Operation name for message prefixes + * @param action - Action being performed (e.g., 'compile', 'format', 'check formatting for') + * @param fileCount - Number of files being processed + * @param targetDir - Optional directory being processed */ showOperationStart( operation: string, @@ -213,7 +362,13 @@ export const SharedUIService = { }, /** - * Displays a warning when no .compact files are found. + * Displays warning when no .compact files are found in target location. + * + * Provides clear feedback about search location and reminds users + * where files are expected to be located. + * + * @param operation - Operation name for message prefixes + * @param targetDir - Optional directory that was searched */ showNoFiles(operation: string, targetDir?: string): void { const searchLocation = targetDir ? `${targetDir}/` : 'src/'; @@ -226,7 +381,12 @@ export const SharedUIService = { }, /** - * Shows available directories when DirectoryNotFoundError occurs. + * Shows available directory options when DirectoryNotFoundError occurs. + * + * Provides helpful context about valid directory names that can be + * used with the --dir flag. Displayed after directory not found errors. + * + * @param operation - Operation name for contextualized help text */ showAvailableDirectories(operation: string): void { console.log(chalk.yellow('\nAvailable directories:')); @@ -249,20 +409,48 @@ export const SharedUIService = { }; /** - * Base class for Compact operations (compilation, formatting). - * Provides common patterns for argument parsing, validation, and execution. + * Abstract base class for Compact operations (compilation, formatting, etc.). + * + * Provides common infrastructure for file discovery, directory validation, + * and argument parsing. Subclasses implement operation-specific logic while + * inheriting shared patterns for working with .compact files. + * + * @example + * ```typescript + * class CompactFormatter extends BaseCompactOperation { + * constructor(writeMode = false, targets: string[] = [], execFn?: ExecFunction) { + * super(targets[0]); // Extract targetDir from targets + * // ... operation-specific setup + * } + * + * async format() { + * await this.validateEnvironment(); + * const { files } = await this.discoverFiles(); + * // ... process files + * } + * } + * ``` */ export abstract class BaseCompactOperation { protected readonly fileDiscovery: FileDiscovery; protected readonly targetDir?: string; + /** + * @param targetDir - Optional subdirectory within src/ to limit operation scope + */ constructor(targetDir?: string) { this.targetDir = targetDir; this.fileDiscovery = new FileDiscovery(); } /** - * Validates the target directory exists if specified. + * Validates that the target directory exists (if specified). + * + * Only performs validation when targetDir is set. Throws DirectoryNotFoundError + * if the specified directory doesn't exist, providing clear user feedback. + * + * @param searchDir - Full path to the directory that should exist + * @throws DirectoryNotFoundError if targetDir is set but directory doesn't exist */ protected validateTargetDirectory(searchDir: string): void { if (this.targetDir && !existsSync(searchDir)) { @@ -274,14 +462,29 @@ export abstract class BaseCompactOperation { } /** - * Gets the search directory based on target directory. + * Determines the directory to search based on target configuration. + * + * @returns Full path to search directory (either SRC_DIR or SRC_DIR/targetDir) */ protected getSearchDirectory(): string { return this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; } /** - * Discovers files and handles empty results. + * Discovers .compact files and handles common validation/feedback. + * + * Performs the complete file discovery workflow: validates directories, + * discovers files, and handles empty results with appropriate user feedback. + * + * @returns Promise resolving to discovered files and search directory + * + * @example + * ```typescript + * const { files, searchDir } = await this.discoverFiles(); + * if (files.length === 0) return; // Already handled by showNoFiles() + * + * // Process discovered files... + * ``` */ protected async discoverFiles(): Promise<{ files: string[]; @@ -301,13 +504,45 @@ export abstract class BaseCompactOperation { } /** - * Abstract methods that must be implemented by subclasses. + * Validates the environment for this operation. + * + * Subclasses implement operation-specific validation (CLI availability, + * tool versions, feature availability, etc.). */ abstract validateEnvironment(): Promise; + + /** + * Displays operation-specific "no files found" message. + * + * Subclasses implement this to provide operation-appropriate messaging + * when no .compact files are discovered. + */ abstract showNoFiles(): void; /** - * Common argument parsing patterns. + * Parses common command-line arguments shared across operations. + * + * Extracts --dir flag and returns remaining arguments for operation-specific + * parsing. Provides consistent argument handling patterns across all tools. + * + * @param args - Raw command-line arguments array + * @returns Parsed base arguments and remaining args for further processing + * @throws Error if --dir flag is malformed + * + * @example + * ```typescript + * static fromArgs(args: string[]) { + * const { targetDir, remainingArgs } = this.parseBaseArgs(args); + * + * // Process operation-specific flags from remainingArgs + * let writeMode = false; + * for (const arg of remainingArgs) { + * if (arg === '--write') writeMode = true; + * } + * + * return new MyOperation(targetDir, writeMode); + * } + * ``` */ protected static parseBaseArgs(args: string[]): { targetDir?: string; @@ -336,10 +571,36 @@ export abstract class BaseCompactOperation { } /** - * Base error handler for both compiler and formatter CLIs. - * Handles common error types with operation-specific context. + * Centralized error handling for CLI applications. + * + * Provides consistent error presentation and user guidance across all + * Compact tools. Handles common error types with appropriate messaging + * and recovery suggestions. */ export const BaseErrorHandler = { + /** + * Handles common error types that can occur across all operations. + * + * Processes errors that are shared between compilation, formatting, and + * other operations. Returns true if the error was handled, false if + * operation-specific handling is needed. + * + * @param error - Error that occurred during operation + * @param spinner - Ora spinner instance for consistent UI messaging + * @param operation - Operation name for contextualized error messages + * @returns true if error was handled, false if caller should handle it + * + * @example + * ```typescript + * function handleError(error: unknown, spinner: Ora) { + * if (BaseErrorHandler.handleCommonErrors(error, spinner, 'COMPILE')) { + * return; // Error was handled + * } + * + * // Handle operation-specific errors... + * } + * ``` + */ handleCommonErrors(error: unknown, spinner: Ora, operation: string): boolean { // CompactCliNotFoundError if (error instanceof Error && error.name === 'CompactCliNotFoundError') { @@ -389,6 +650,17 @@ export const BaseErrorHandler = { return false; // Not handled, let specific handler deal with it }, + /** + * Handles unexpected errors with generic troubleshooting guidance. + * + * Provides fallback error handling for errors not covered by common + * error types. Shows general troubleshooting steps that apply to + * most Compact operations. + * + * @param error - Unexpected error that occurred + * @param spinner - Ora spinner instance for consistent UI messaging + * @param operation - Operation name for contextualized error messages + */ handleUnexpectedError(error: unknown, spinner: Ora, operation: string): void { const errorMessage = error instanceof Error ? error.message : String(error); spinner.fail(chalk.red(`[${operation}] Unexpected error: ${errorMessage}`)); From a1497450189210354be3338181ab0f823084b949 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 16:10:45 -0300 Subject: [PATCH 31/44] update and improve docs --- compact/src/Compiler.ts | 318 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 300 insertions(+), 18 deletions(-) diff --git a/compact/src/Compiler.ts b/compact/src/Compiler.ts index 5829803a..c1d57290 100755 --- a/compact/src/Compiler.ts +++ b/compact/src/Compiler.ts @@ -18,12 +18,40 @@ import { } from './types/errors.ts'; /** - * Environment validator specific to compilation operations. - * Extends base validator with compilation-specific version checking. + * Environment validator specialized for Compact compilation operations. + * + * Extends the base validator with compilation-specific requirements including + * toolchain version validation and compatibility checking. Ensures the Compact + * compiler toolchain is available and properly configured before attempting + * compilation operations. + * + * @example + * ```typescript + * const validator = new CompilerEnvironmentValidator(); + * const { devToolsVersion, toolchainVersion } = await validator.validate('1.2.0'); + * console.log(`Using toolchain ${toolchainVersion}`); + * ``` */ export class CompilerEnvironmentValidator extends BaseEnvironmentValidator { /** - * Retrieves the version of the Compact toolchain/compiler. + * Retrieves the version string of the Compact compiler toolchain. + * + * Queries the Compact CLI for toolchain version information, optionally + * targeting a specific version. This is separate from the dev tools version + * and represents the actual compiler backend being used. + * + * @param version - Optional specific toolchain version to query (e.g., '1.2.0') + * @returns Promise resolving to the trimmed toolchain version string + * @throws Error if the toolchain version command fails or version doesn't exist + * + * @example + * ```typescript + * // Get default toolchain version + * const defaultVersion = await validator.getToolchainVersion(); + * + * // Get specific toolchain version + * const specificVersion = await validator.getToolchainVersion('1.2.0'); + * ``` */ async getToolchainVersion(version?: string): Promise { const versionFlag = version ? `+${version}` : ''; @@ -34,7 +62,26 @@ export class CompilerEnvironmentValidator extends BaseEnvironmentValidator { } /** - * Validates environment for compilation with optional version. + * Performs comprehensive environment validation for compilation operations. + * + * Validates both the base Compact CLI environment and compilation-specific + * requirements. Ensures that the specified toolchain version (if any) is + * available and properly installed. + * + * @param version - Optional specific toolchain version to validate + * @returns Promise resolving to validation results including both dev tools and toolchain versions + * @throws CompactCliNotFoundError if CLI is not available + * @throws Error if specified toolchain version is not available + * + * @example + * ```typescript + * // Validate with default toolchain + * const result = await validator.validate(); + * + * // Validate with specific toolchain version + * const result = await validator.validate('1.2.0'); + * console.log(`Dev tools: ${result.devToolsVersion}, Toolchain: ${result.toolchainVersion}`); + * ``` */ async validate(version?: string): Promise<{ devToolsVersion: string; @@ -48,12 +95,50 @@ export class CompilerEnvironmentValidator extends BaseEnvironmentValidator { } /** - * Service for executing compilation commands. - * Extends base service with compilation-specific command construction. + * Service for executing Compact compilation commands. + * + * Handles the construction and execution of compilation commands for individual + * .compact files. Manages input/output path resolution, command flag application, + * and version targeting. Provides consistent error handling for compilation failures. + * + * @example + * ```typescript + * const compiler = new CompilerService(); + * const result = await compiler.compileFile( + * 'Token.compact', + * '--skip-zk', + * '1.2.0' + * ); + * console.log('Compilation output:', result.stdout); + * ``` */ export class CompilerService extends BaseCompactService { /** * Compiles a single .compact file using the Compact CLI. + * + * Constructs the appropriate compilation command with input/output paths, + * applies the specified flags and version, then executes the compilation. + * Input files are resolved relative to SRC_DIR, and output is written to + * a subdirectory in ARTIFACTS_DIR named after the input file. + * + * @param file - Relative path to the .compact file from SRC_DIR (e.g., 'Token.compact') + * @param flags - Compilation flags to apply (e.g., '--skip-zk') + * @param version - Optional specific toolchain version to use (e.g., '1.2.0') + * @returns Promise resolving to command execution results with stdout and stderr + * @throws CompilationError if the compilation fails + * + * @example + * ```typescript + * // Basic compilation + * await compiler.compileFile('Token.compact', '', undefined); + * + * // Compilation with flags and version + * await compiler.compileFile( + * 'contracts/security/AccessControl.compact', + * '--skip-zk', + * '1.2.0' + * ); + * ``` */ async compileFile( file: string, @@ -70,6 +155,24 @@ export class CompilerService extends BaseCompactService { return this.executeCompactCommand(command, `Failed to compile ${file}`); } + /** + * Creates compilation-specific error instances. + * + * Wraps compilation failures in CompilationError instances that provide + * additional context including the file that failed to compile. Extracts + * the filename from error messages when possible for better error reporting. + * + * @param message - Error message describing the compilation failure + * @param cause - Original error that caused the compilation failure (optional) + * @returns CompilationError instance with file context and cause information + * + * @example + * ```typescript + * // This method is called automatically by executeCompactCommand + * // when compilation fails, creating errors like: + * // CompilationError: Failed to compile Token.compact: syntax error + * ``` + */ protected createError(message: string, cause?: unknown): Error { // Extract file name from error message for CompilationError const match = message.match(/Failed to compile (.+?):/); @@ -79,14 +182,42 @@ export class CompilerService extends BaseCompactService { } /** - * UI service specific to compilation operations. - * Extends shared UI with compilation-specific formatting. + * UI service specialized for compilation operations. + * + * Provides compilation-specific user interface elements and messaging. + * Extends the shared UI service with compilation-focused information display, + * progress reporting, and status messaging. Ensures consistent visual presentation + * across compilation operations. */ export const CompilerUIService = { ...SharedUIService, /** - * Displays compilation environment information. + * Displays comprehensive compilation environment information. + * + * Shows both developer tools and toolchain versions, along with optional + * target directory and version override information. Provides users with + * clear visibility into the compilation environment configuration. + * + * @param devToolsVersion - Version of the installed Compact developer tools + * @param toolchainVersion - Version of the Compact compiler toolchain being used + * @param targetDir - Optional target directory being compiled (relative to src/) + * @param version - Optional specific toolchain version being used + * + * @example + * ```typescript + * CompilerUIService.displayEnvInfo( + * 'compact-dev-tools 2.1.0', + * 'compact-toolchain 1.8.0', + * 'contracts', + * '1.8.0' + * ); + * // Output: + * // ℹ [COMPILE] TARGET_DIR: contracts + * // ℹ [COMPILE] Compact developer tools: compact-dev-tools 2.1.0 + * // ℹ [COMPILE] Compact toolchain: compact-toolchain 1.8.0 + * // ℹ [COMPILE] Using toolchain version: 1.8.0 + * ``` */ displayEnvInfo( devToolsVersion: string, @@ -107,7 +238,23 @@ export const CompilerUIService = { }, /** - * Displays compilation start message. + * Displays compilation start message with file count and location context. + * + * Informs users about the scope of the compilation operation, including + * the number of files found and the directory being processed. Provides + * clear expectations about the work to be performed. + * + * @param fileCount - Number of .compact files discovered for compilation + * @param targetDir - Optional target directory being compiled + * + * @example + * ```typescript + * CompilerUIService.showCompilationStart(3, 'contracts'); + * // Output: ℹ [COMPILE] Found 3 .compact file(s) to compile in contracts/ + * + * CompilerUIService.showCompilationStart(1); + * // Output: ℹ [COMPILE] Found 1 .compact file(s) to compile + * ``` */ showCompilationStart(fileCount: number, targetDir?: string): void { SharedUIService.showOperationStart( @@ -119,7 +266,22 @@ export const CompilerUIService = { }, /** - * Displays no files warning for compilation. + * Displays warning when no .compact files are found for compilation. + * + * Provides clear feedback when the compilation operation cannot proceed + * because no source files were discovered in the target location. + * Helps users understand where files are expected to be located. + * + * @param targetDir - Optional target directory that was searched + * + * @example + * ```typescript + * CompilerUIService.showNoFiles('contracts'); + * // Output: ⚠ [COMPILE] No .compact files found in contracts/. + * + * CompilerUIService.showNoFiles(); + * // Output: ⚠ [COMPILE] No .compact files found in src/. + * ``` */ showNoFiles(targetDir?: string): void { SharedUIService.showNoFiles('COMPILE', targetDir); @@ -127,8 +289,27 @@ export const CompilerUIService = { }; /** - * Main compiler class that orchestrates the compilation process. - * Extends base operation with compilation-specific logic. + * Main compiler orchestrator for Compact compilation operations. + * + * Coordinates the complete compilation workflow from environment validation + * through file processing. Manages compilation configuration including flags, + * toolchain versions, and target directories. Provides progress reporting + * and error handling for batch compilation operations. + * + * @example + * ```typescript + * // Basic compilation of all files in src/ + * const compiler = new CompactCompiler(); + * await compiler.compile(); + * + * // Compilation with optimization flags + * const compiler = new CompactCompiler('--skip-zk'); + * await compiler.compile(); + * + * // Compilation of specific directory with version override + * const compiler = new CompactCompiler('', 'contracts', '1.2.0'); + * await compiler.compile(); + * ``` */ export class CompactCompiler extends BaseCompactOperation { private readonly environmentValidator: CompilerEnvironmentValidator; @@ -137,7 +318,22 @@ export class CompactCompiler extends BaseCompactOperation { private readonly version?: string; /** - * Creates a new CompactCompiler instance. + * Creates a new CompactCompiler instance with specified configuration. + * + * Initializes the compiler with compilation flags, target directory scope, + * and optional toolchain version override. Sets up the necessary services + * for environment validation and command execution. + * + * @param flags - Compilation flags to apply to all files (e.g., '--skip-zk') + * @param targetDir - Optional subdirectory within src/ to limit compilation scope + * @param version - Optional specific toolchain version to use (e.g., '1.2.0') + * @param execFn - Optional command execution function for testing/customization + * + * @example + * ```typescript + * // Compile all files with default settings + * const compiler = new CompactCompiler(); + * ``` */ constructor( flags = '', @@ -154,6 +350,32 @@ export class CompactCompiler extends BaseCompactOperation { /** * Factory method to create a CompactCompiler from command-line arguments. + * + * Parses command-line arguments and environment variables to construct + * a properly configured CompactCompiler instance. Handles flag processing, + * directory targeting, version specification, and environment-based configuration. + * + * @param args - Raw command-line arguments array + * @param env - Process environment variables (defaults to process.env) + * @returns Configured CompactCompiler instance ready for execution + * @throws Error if arguments are malformed (e.g., --dir without directory name) + * + * @example + * ```typescript + * // Parse from command line: ['--dir', 'contracts', '+1.2.0'] + * const compiler = CompactCompiler.fromArgs([ + * '--dir', 'contracts', + * '+1.2.0' + * ]); + * + * // With environment variable for skipping ZK proofs + * const compiler = CompactCompiler.fromArgs( + * { SKIP_ZK: 'true' } + * ); + * + * // Parse from actual process arguments + * const compiler = CompactCompiler.fromArgs(process.argv.slice(2)); + * ``` */ static fromArgs( args: string[], @@ -183,7 +405,26 @@ export class CompactCompiler extends BaseCompactOperation { } /** - * Validates the compilation environment. + * Validates the compilation environment and displays configuration information. + * + * Performs comprehensive environment validation including CLI availability, + * toolchain version verification, and configuration display. Must be called + * before attempting compilation operations. + * + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws Error if specified toolchain version is not available + * + * @example + * ```typescript + * try { + * await compiler.validateEnvironment(); + * // Environment is valid, proceed with compilation + * } catch (error) { + * if (error instanceof CompactCliNotFoundError) { + * console.error('Please install Compact CLI first'); + * } + * } + * ``` */ async validateEnvironment(): Promise { const { devToolsVersion, toolchainVersion } = @@ -198,14 +439,39 @@ export class CompactCompiler extends BaseCompactOperation { } /** - * Shows no files warning for compilation. + * Displays warning message when no .compact files are found. + * + * Shows operation-specific messaging when file discovery returns no results. + * Provides clear feedback about the search location and expected file locations. */ showNoFiles(): void { CompilerUIService.showNoFiles(this.targetDir); } /** - * Main compilation execution method. + * Executes the complete compilation workflow. + * + * Orchestrates the full compilation process: validates environment, discovers + * source files, and compiles each file with progress reporting. Handles batch + * compilation of multiple files with individual error isolation. + * + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws DirectoryNotFoundError if target directory doesn't exist + * @throws CompilationError if any file fails to compile + * + * @example + * ```typescript + * const compiler = new CompactCompiler('--skip-zk'); + * + * try { + * await compiler.compile(); + * console.log('Compilation completed successfully'); + * } catch (error) { + * if (error instanceof CompilationError) { + * console.error(`Failed to compile ${error.file}: ${error.message}`); + * } + * } + * ``` */ async compile(): Promise { await this.validateEnvironment(); @@ -221,7 +487,23 @@ export class CompactCompiler extends BaseCompactOperation { } /** - * Compiles a single file with progress reporting. + * Compiles a single file with progress reporting and error handling. + * + * Handles the compilation of an individual .compact file with visual progress + * indicators, output formatting, and comprehensive error reporting. Provides + * detailed feedback about compilation status and results. + * + * @param file - Relative path to the .compact file from SRC_DIR + * @param index - Current file index in the batch (0-based) + * @param total - Total number of files being compiled + * @throws CompilationError if the file fails to compile + * + * @example + * ```typescript + * // This method is typically called internally by compile() + * // but can be used for individual file compilation: + * await compiler.compileFile('Token.compact', 0, 1); + * ``` */ private async compileFile( file: string, From bef4b34a10ccecadc2cfc0c56522aa6cdb31ecee Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 16:58:08 -0300 Subject: [PATCH 32/44] improve runCompiler and runFormatter docs --- compact/src/runCompiler.ts | 65 +++++++++++++++++++++++++++++++++-- compact/src/runFormatter.ts | 67 +++++++++++++++++++++++++++++++++++-- 2 files changed, 126 insertions(+), 6 deletions(-) diff --git a/compact/src/runCompiler.ts b/compact/src/runCompiler.ts index a6ed221d..e3e4f79e 100644 --- a/compact/src/runCompiler.ts +++ b/compact/src/runCompiler.ts @@ -10,7 +10,24 @@ import { } from './types/errors.js'; /** - * Executes the Compact compiler CLI with improved error handling and user feedback. + * Main entry point for the Compact compiler CLI application. + * + * Orchestrates the complete compilation workflow from command-line argument + * parsing through execution and error handling. Provides user-friendly feedback + * and comprehensive error reporting for compilation operations. + * + * The function handles the full lifecycle: + * + * 1. Parses command-line arguments into compiler configuration. + * 2. Executes the compilation process with progress indicators. + * 3. Handles errors with detailed, actionable feedback. + * 4. Exits with appropriate status codes for CI/CD integration. + * + * @example + * ```bash + * # Called from command line as: + * compact-compiler --dir ./contracts/src/security --skip-zk +0.24.0 + * ``` */ async function runCompiler(): Promise { const spinner = ora(chalk.blue('[COMPILE] Compact compiler started')).info(); @@ -26,7 +43,30 @@ async function runCompiler(): Promise { } /** - * Centralized error handling with compiler-specific error types. + * Comprehensive error handler for compilation-specific failures. + * + * Provides layered error handling that first attempts common error resolution + * before falling back to compilation-specific error types. Ensures users receive + * actionable feedback for all failure scenarios with appropriate visual styling + * and contextual information. + * + * Error handling priority: + * + * 1. Common errors (CLI not found, directory issues, environment problems). + * 2. Compilation-specific errors (file compilation failures). + * 3. Argument parsing errors (malformed command-line usage). + * 4. Unexpected errors (with troubleshooting guidance). + * + * @param error - The error that occurred during compilation + * @param spinner - Ora spinner instance for consistent UI feedback + * + * @example + * ```typescript + * // This function handles errors like: + * // - CompilationError: Failed to compile Token.compact + * // - CompactCliNotFoundError: 'compact' CLI not found in PATH + * // - DirectoryNotFoundError: Target directory contracts/ does not exist + * ``` */ function handleError(error: unknown, spinner: Ora): void { // Try common error handling first @@ -70,7 +110,26 @@ function handleError(error: unknown, spinner: Ora): void { } /** - * Shows usage help with examples for compilation scenarios. + * Displays comprehensive usage help for the Compact compiler CLI. + * + * Provides detailed documentation of all available command-line options, + * practical usage examples, and integration patterns. Helps users understand + * both basic and advanced compilation scenarios, including environment variable + * usage and toolchain version management. + * + * The help includes: + * + * - Complete option descriptions with parameter details. + * - Practical examples for common compilation tasks. + * - Integration patterns with build tools like Turbo. + * - Environment variable configuration options. + * + * @example + * ```typescript + * // Called automatically when argument parsing fails: + * // compact-compiler --dir # Missing directory name + * // Shows full usage help to guide correct usage + * ``` */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-compiler [options]')); diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts index fb1aa6e7..1346a785 100644 --- a/compact/src/runFormatter.ts +++ b/compact/src/runFormatter.ts @@ -10,7 +10,26 @@ import { } from './types/errors.js'; /** - * Executes the Compact formatter CLI with improved error handling and user feedback. + * Main entry point for the Compact formatter CLI application. + * + * Coordinates the complete formatting workflow from command-line argument + * parsing through execution and error handling. Provides comprehensive user + * feedback and detailed error reporting for both check and write formatting + * operations. + * + * The function manages the full application lifecycle: + * + * 1. Parses command-line arguments into formatter configuration. + * 2. Executes formatting operations with visual progress indicators. + * 3. Handles all error scenarios with actionable user guidance. + * 4. Exits with appropriate status codes for automated workflows. + * + * @example + * ```bash + * # Called from command line as: + * compact-formatter --check --dir ./contracts/src/security + * compact-formatter --write ./contracts/src/access/AccessControl.compact + * ``` */ async function runFormatter(): Promise { const spinner = ora(chalk.blue('[FORMAT] Compact formatter started')).info(); @@ -26,7 +45,30 @@ async function runFormatter(): Promise { } /** - * Centralized error handling with formatter-specific error types. + * Specialized error handler for formatting operation failures. + * + * Implements multi-layered error handling that addresses both common infrastructure + * issues and formatting-specific problems. Provides detailed diagnostic information + * and recovery suggestions tailored to formatting workflows. + * + * Error handling hierarchy: + * + * 1. Common errors (CLI availability, directory validation). + * 2. Formatter availability errors (toolchain compatibility issues). + * 3. Formatting operation errors (file processing failures). + * 4. Argument parsing errors (command-line usage problems). + * 5. Unexpected errors (with comprehensive troubleshooting). + * + * @param error - The error that occurred during formatting operations + * @param spinner - Ora spinner instance for consistent visual feedback + * + * @example + * ```typescript + * // This function handles errors such as: + * // - FormatterNotAvailableError: Formatter not available in current toolchain + * // - FormatterError: Failed to format Token.compact + * // - DirectoryNotFoundError: Target directory contracts/ does not exist + * ``` */ function handleError(error: unknown, spinner: Ora): void { // Try common error handling first @@ -80,7 +122,26 @@ function handleError(error: unknown, spinner: Ora): void { } /** - * Shows usage help with examples for formatting scenarios. + * Displays comprehensive usage documentation for the Compact formatter CLI. + * + * Provides complete reference documentation including all command-line options, + * practical usage patterns, and integration examples. Covers both basic formatting + * operations and advanced workflows including check mode, directory targeting, + * and specific file processing. + * + * The help documentation includes: + * + * - Detailed option descriptions with behavior explanations. + * - Comprehensive examples for common formatting scenarios. + * - Integration patterns with build systems and CI/CD workflows. + * - Best practices for different development workflows. + * + * @example + * ```typescript + * // Automatically displayed when argument parsing fails: + * // compact-formatter --dir # Missing directory name + * // Shows complete usage guide to assist proper command construction + * ``` */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); From 9801f3a1015e2a69725e8e14eb2ab4aa6d2e7db9 Mon Sep 17 00:00:00 2001 From: andrew Date: Wed, 10 Sep 2025 22:47:11 -0300 Subject: [PATCH 33/44] add formatter docs --- compact/src/Formatter.ts | 409 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 389 insertions(+), 20 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index b5ccc0e9..cbc52cdd 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -18,12 +18,44 @@ import { } from './types/errors.ts'; /** - * Environment validator specific to formatting operations. - * Extends base validator with formatter availability checking. + * Environment validator specialized for Compact formatting operations. + * + * Extends the base validator with formatting-specific requirements including + * formatter availability checking and version compatibility validation. Ensures + * the Compact formatter is available and properly configured before attempting + * formatting operations. + * + * The formatter requires Compact compiler version 0.25.0 or later to be available. + * + * @example + * ```typescript + * const validator = new FormatterEnvironmentValidator(); + * const { devToolsVersion } = await validator.validate(); + * console.log(`Formatter available with dev tools ${devToolsVersion}`); + * ``` */ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { /** - * Checks if the formatter is available (requires compiler 0.25.0+). + * Verifies that the Compact formatter is available and accessible. + * + * Tests formatter availability by attempting to access the format help command. + * The formatter requires Compact compiler version 0.25.0 or later, and this + * method provides clear error messaging when the formatter is not available. + * + * @throws FormatterNotAvailableError if formatter is not available in current toolchain + * @throws Error if help command fails for other reasons + * + * @example + * ```typescript + * try { + * await validator.checkFormatterAvailable(); + * console.log('Formatter is ready for use'); + * } catch (error) { + * if (error instanceof FormatterNotAvailableError) { + * console.error('Please update Compact compiler to use formatter'); + * } + * } + * ``` */ async checkFormatterAvailable(): Promise { try { @@ -42,7 +74,21 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { } /** - * Validates environment for formatting operations. + * Performs comprehensive environment validation for formatting operations. + * + * Validates both the base Compact CLI environment and formatting-specific + * requirements. Ensures the formatter is available and accessible before + * proceeding with formatting operations. + * + * @returns Promise resolving to validation results with dev tools version + * @throws CompactCliNotFoundError if CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + * + * @example + * ```typescript + * const { devToolsVersion } = await validator.validate(); + * console.log(`Environment validated with ${devToolsVersion}`); + * ``` */ async validate(): Promise<{ devToolsVersion: string }> { const { devToolsVersion } = await this.validateBase(); @@ -52,10 +98,49 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { } /** - * Service for executing formatting commands. - * Extends base service with format-specific command construction. + * Service for executing Compact formatting commands. + * + * Handles the construction and execution of formatting commands for both check + * and write operations. Manages path resolution, command flag application, and + * provides specialized error handling for formatting failures. Supports both + * directory-wide and individual file formatting operations. + * + * @example + * ```typescript + * const formatter = new FormatterService(); + * + * // Check formatting without modifications + * const checkResult = await formatter.checkFormatting('src/contracts'); + * console.log('Is formatted:', checkResult.isFormatted); + * + * // Format and write changes + * await formatter.formatAndWrite('src/contracts'); + * ``` */ export class FormatterService extends BaseCompactService { + /** + * Formats files and writes the changes to disk. + * + * Executes the format command in write mode, applying formatting changes + * directly to the source files. Can target a specific directory path or + * operate on the entire source tree when no path is specified. + * + * @param targetPath - Optional path to target for formatting (directory or file) + * @returns Promise resolving to command execution results with stdout and stderr + * @throws FormatterError if formatting operation fails + * + * @example + * ```typescript + * // Format all files in the project + * await formatter.formatAndWrite(); + * + * // Format specific directory + * await formatter.formatAndWrite('src/contracts/security'); + * + * // Format specific file + * await formatter.formatAndWrite('src/Token.compact'); + * ``` + */ async formatAndWrite( targetPath?: string, ): Promise<{ stdout: string; stderr: string }> { @@ -66,6 +151,28 @@ export class FormatterService extends BaseCompactService { /** * Checks if files are properly formatted without modifying them. + * + * Executes the format command in check mode to validate formatting without + * making changes. Returns both the execution results and a boolean indicating + * whether the files are properly formatted. Exit code 1 with output indicates + * formatting differences, while other errors represent actual failures. + * + * @param targetPath - Optional path to check for formatting (directory or file) + * @returns Promise resolving to check results including formatting status + * @throws FormatterError if check operation fails (excluding formatting differences) + * + * @example + * ```typescript + * // Check all files + * const result = await formatter.checkFormatting(); + * if (!result.isFormatted) { + * console.log('Formatting differences:', result.stdout); + * } + * + * // Check specific directory + * const result = await formatter.checkFormatting('src/contracts'); + * console.log('Directory is formatted:', result.isFormatted); + * ``` */ async checkFormatting(targetPath?: string): Promise<{ stdout: string; @@ -100,7 +207,28 @@ export class FormatterService extends BaseCompactService { } /** - * Formats a list of specific files. + * Formats a specific list of .compact files. + * + * Applies formatting to the provided list of files, resolving their paths + * relative to the SRC_DIR. Useful for formatting only specific files rather + * than entire directories, such as when processing files from a git diff + * or user selection. + * + * @param files - Array of relative file paths from SRC_DIR to format + * @returns Promise resolving to command execution results + * @throws FormatterError if any file fails to format + * + * @example + * ```typescript + * // Format specific files + * await formatter.formatFiles([ + * 'Token.compact', + * 'contracts/security/AccessControl.compact' + * ]); + * + * // Handle empty file list gracefully + * await formatter.formatFiles([]); // Returns empty results + * ``` */ async formatFiles( files: string[], @@ -117,6 +245,25 @@ export class FormatterService extends BaseCompactService { ); } + /** + * Creates formatting-specific error instances. + * + * Wraps formatting failures in FormatterError instances that provide + * additional context including the target that failed to format. Extracts + * the target (file or directory) from error messages when possible for + * better error reporting and debugging. + * + * @param message - Error message describing the formatting failure + * @param cause - Original error that caused the formatting failure (optional) + * @returns FormatterError instance with target context and cause information + * + * @example + * ```typescript + * // This method is called automatically by executeCompactCommand + * // when formatting fails, creating errors like: + * // FormatterError: Failed to format contracts/Token.compact + * ``` + */ protected createError(message: string, cause?: unknown): Error { // Extract target from error message for FormatterError const match = message.match(/Failed to format(?: files:)? (.+)/); @@ -126,21 +273,61 @@ export class FormatterService extends BaseCompactService { } /** - * UI service specific to formatting operations. - * Extends shared UI with format-specific messaging. + * UI service specialized for formatting operations. + * + * Provides formatting-specific user interface elements and messaging. + * Extends the shared UI service with formatting-focused information display, + * check result reporting, and operation status messaging. Ensures consistent + * visual presentation across formatting operations. */ export const FormatterUIService = { ...SharedUIService, /** * Displays formatting environment information. + * + * Shows developer tools version and optional target directory information + * for formatting operations. Provides users with clear visibility into + * the formatting environment configuration. + * + * @param devToolsVersion - Version of the installed Compact developer tools + * @param targetDir - Optional target directory being formatted (relative to src/) + * + * @example + * ```typescript + * FormatterUIService.displayEnvInfo( + * 'compact-dev-tools 2.1.0', + * 'contracts' + * ); + * // Output: + * // ℹ [FORMAT] TARGET_DIR: contracts + * // ℹ [FORMAT] Compact developer tools: compact-dev-tools 2.1.0 + * ``` */ displayEnvInfo(devToolsVersion: string, targetDir?: string): void { SharedUIService.displayBaseEnvInfo('FORMAT', devToolsVersion, targetDir); }, /** - * Displays formatting start message. + * Displays formatting start message with operation context. + * + * Informs users about the scope of the formatting operation, including + * the number of files found, the mode of operation (check vs write), + * and the directory being processed. Provides clear expectations about + * the work to be performed. + * + * @param fileCount - Number of .compact files discovered for formatting + * @param mode - Operation mode: 'check' for validation, 'write' for formatting + * @param targetDir - Optional target directory being processed + * + * @example + * ```typescript + * FormatterUIService.showFormattingStart(3, 'check', 'contracts'); + * // Output: ℹ [FORMAT] Found 3 .compact file(s) to check formatting for in contracts/ + * + * FormatterUIService.showFormattingStart(5, 'write'); + * // Output: ℹ [FORMAT] Found 5 .compact file(s) to format + * ``` */ showFormattingStart( fileCount: number, @@ -152,14 +339,49 @@ export const FormatterUIService = { }, /** - * Displays no files warning for formatting. + * Displays warning when no .compact files are found for formatting. + * + * Provides clear feedback when the formatting operation cannot proceed + * because no source files were discovered in the target location. + * Helps users understand where files are expected to be located. + * + * @param targetDir - Optional target directory that was searched + * + * @example + * ```typescript + * FormatterUIService.showNoFiles('contracts'); + * // Output: ⚠ [FORMAT] No .compact files found in contracts/. + * + * FormatterUIService.showNoFiles(); + * // Output: ⚠ [FORMAT] No .compact files found in src/. + * ``` */ showNoFiles(targetDir?: string): void { SharedUIService.showNoFiles('FORMAT', targetDir); }, /** - * Displays formatting check results. + * Displays formatting check results with appropriate visual feedback. + * + * Shows the outcome of formatting checks with success/failure indicators + * and optional formatting differences. Provides clear visual distinction + * between properly formatted code and code that needs formatting changes. + * + * @param isFormatted - Whether the checked files are properly formatted + * @param differences - Optional formatting differences to display + * + * @example + * ```typescript + * // Show success for properly formatted files + * FormatterUIService.showCheckResults(true); + * // Output: ✓ [FORMAT] All files are properly formatted + * + * // Show failure with differences + * FormatterUIService.showCheckResults(false, 'Token.compact needs formatting'); + * // Output: ✗ [FORMAT] Some files are not properly formatted + * // Formatting differences: + * // Token.compact needs formatting + * ``` */ showCheckResults(isFormatted: boolean, differences?: string): void { const spinner = ora(); @@ -177,8 +399,28 @@ export const FormatterUIService = { }; /** - * Main formatter class that orchestrates the formatting process. - * Extends base operation with formatting-specific logic. + * Main formatter orchestrator for Compact formatting operations. + * + * Coordinates the complete formatting workflow from environment validation + * through file processing. Manages formatting configuration including check/write + * modes, target specifications (directories or individual files), and provides + * progress reporting and error handling for both batch and individual file + * formatting operations. + * + * @example + * ```typescript + * // Check formatting of all files + * const formatter = new CompactFormatter(false); + * await formatter.format(); + * + * // Format specific files + * const formatter = new CompactFormatter(true, ['Token.compact', 'AccessControl.compact']); + * await formatter.format(); + * + * // Format specific directory + * const formatter = new CompactFormatter(true, ['contracts']); + * await formatter.format(); + * ``` */ export class CompactFormatter extends BaseCompactOperation { private readonly environmentValidator: FormatterEnvironmentValidator; @@ -187,7 +429,34 @@ export class CompactFormatter extends BaseCompactOperation { private readonly targets: string[]; /** - * Creates a new CompactFormatter instance. + * Creates a new CompactFormatter instance with specified configuration. + * + * Initializes the formatter with operation mode (check vs write), target + * specifications (directories or files), and sets up the necessary services + * for environment validation and command execution. Handles both directory + * and individual file targeting scenarios. + * + * @param writeMode - Whether to write formatting changes (true) or just check (false) + * @param targets - Array of target directories or files to format + * @param execFn - Optional command execution function for testing/customization + * + * @example + * ```typescript + * // Check formatting of all files (default) + * const formatter = new CompactFormatter(); + * + * // Format all files with changes written + * const formatter = new CompactFormatter(true); + * + * // Check specific files without writing + * const formatter = new CompactFormatter(false, ['Token.compact', 'AccessControl.compact']); + * + * // Format specific directory + * const formatter = new CompactFormatter(true, ['contracts']); + * + * // For testing with custom execution function + * const formatter = new CompactFormatter(false, [], mockExecFn); + * ``` */ constructor( writeMode = false, @@ -209,6 +478,33 @@ export class CompactFormatter extends BaseCompactOperation { /** * Factory method to create a CompactFormatter from command-line arguments. + * + * Parses command-line arguments to construct a properly configured + * CompactFormatter instance. Handles flag processing, target specification, + * and mode determination from command-line inputs. Provides the primary + * interface between CLI arguments and formatter configuration. + * + * @param args - Raw command-line arguments array + * @returns Configured CompactFormatter instance ready for execution + * @throws Error if arguments are malformed (e.g., --dir without directory name) + * + * @example + * ```typescript + * // Parse from command line: ['--check', '--dir', 'contracts'] + * const formatter = CompactFormatter.fromArgs([ + * '--check', + * '--dir', 'contracts' + * ]); + * + * // Parse write mode with specific files: ['--write', 'Token.compact'] + * const formatter = CompactFormatter.fromArgs([ + * '--write', + * 'Token.compact' + * ]); + * + * // Parse from actual process arguments + * const formatter = CompactFormatter.fromArgs(process.argv.slice(2)); + * ``` */ static fromArgs(args: string[]): CompactFormatter { const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); @@ -233,7 +529,26 @@ export class CompactFormatter extends BaseCompactOperation { } /** - * Validates the formatting environment. + * Validates the formatting environment and displays configuration information. + * + * Performs comprehensive environment validation including CLI availability, + * formatter availability verification, and configuration display. Must be + * called before attempting formatting operations. + * + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + * + * @example + * ```typescript + * try { + * await formatter.validateEnvironment(); + * // Environment is valid, proceed with formatting + * } catch (error) { + * if (error instanceof FormatterNotAvailableError) { + * console.error('Please update Compact compiler to use formatter'); + * } + * } + * ``` */ async validateEnvironment(): Promise { const { devToolsVersion } = await this.environmentValidator.validate(); @@ -241,14 +556,41 @@ export class CompactFormatter extends BaseCompactOperation { } /** - * Shows no files warning for formatting. + * Displays warning message when no .compact files are found. + * + * Shows operation-specific messaging when file discovery returns no results. + * Provides clear feedback about the search location and expected file locations. */ showNoFiles(): void { FormatterUIService.showNoFiles(this.targetDir); } /** - * Main formatting execution method. + * Executes the complete formatting workflow. + * + * Orchestrates the full formatting process: validates environment, determines + * operation mode (specific files vs directory), and executes the appropriate + * formatting strategy. Handles both check and write operations with progress + * reporting and error handling. + * + * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws FormatterNotAvailableError if formatter is not available + * @throws DirectoryNotFoundError if target directory doesn't exist + * @throws FormatterError if any file fails to format + * + * @example + * ```typescript + * const formatter = new CompactFormatter(false, ['contracts']); + * + * try { + * await formatter.format(); + * console.log('Formatting check completed successfully'); + * } catch (error) { + * if (error instanceof FormatterError) { + * console.error(`Formatting failed: ${error.message}`); + * } + * } + * ``` */ async format(): Promise { await this.validateEnvironment(); @@ -266,7 +608,20 @@ export class CompactFormatter extends BaseCompactOperation { } /** - * Formats specific files provided as arguments. + * Formats or checks specific files provided as command-line arguments. + * + * Handles formatting operations when specific .compact files are provided + * as targets. In check mode, validates each file individually with separate + * status reporting. In write mode, formats all specified files in a single + * operation for efficiency. + * + * @throws FormatterError if any file fails to format or check + * + * @example + * ```typescript + * // This method is called internally when targets are specific files: + * // compact-formatter Token.compact AccessControl.compact + * ``` */ private async formatSpecificFiles(): Promise { if (!this.writeMode) { @@ -281,7 +636,21 @@ export class CompactFormatter extends BaseCompactOperation { } /** - * Formats all files in a directory or current directory. + * Formats or checks all files in a directory or the entire source tree. + * + * Handles batch formatting operations for directory targets or the entire + * project when no specific targets are provided. Discovers files, reports + * progress, and executes the appropriate formatting strategy based on the + * operation mode. + * + * @throws FormatterError if directory formatting fails + * + * @example + * ```typescript + * // This method is called internally for directory operations: + * // compact-formatter --dir contracts + * // compact-formatter # formats entire src/ directory + * ``` */ private async formatDirectory(): Promise { const { files, searchDir } = await this.discoverFiles(); From 041e2f0f942f5cd5e6afc8fffa6babd54e39f2ce Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 00:39:28 -0300 Subject: [PATCH 34/44] simplify formatter --- compact/src/Formatter.ts | 595 +++++++++------------------------------ 1 file changed, 136 insertions(+), 459 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index cbc52cdd..4d097805 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -1,8 +1,6 @@ #!/usr/bin/env node import { join } from 'node:path'; -import chalk from 'chalk'; -import ora from 'ora'; import { BaseCompactOperation, BaseCompactService, @@ -18,20 +16,17 @@ import { } from './types/errors.ts'; /** - * Environment validator specialized for Compact formatting operations. + * Environment validator for Compact formatting operations. * - * Extends the base validator with formatting-specific requirements including - * formatter availability checking and version compatibility validation. Ensures - * the Compact formatter is available and properly configured before attempting - * formatting operations. - * - * The formatter requires Compact compiler version 0.25.0 or later to be available. + * Validates that both the Compact CLI and formatter are available before + * attempting formatting operations. The formatter requires Compact compiler + * version 0.25.0 or later to be installed and accessible. * * @example * ```typescript * const validator = new FormatterEnvironmentValidator(); * const { devToolsVersion } = await validator.validate(); - * console.log(`Formatter available with dev tools ${devToolsVersion}`); + * console.log(`Formatter ready with ${devToolsVersion}`); * ``` */ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { @@ -39,20 +34,19 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { * Verifies that the Compact formatter is available and accessible. * * Tests formatter availability by attempting to access the format help command. - * The formatter requires Compact compiler version 0.25.0 or later, and this - * method provides clear error messaging when the formatter is not available. + * Throws a specific error with recovery instructions when the formatter is not + * available in the current toolchain. * - * @throws FormatterNotAvailableError if formatter is not available in current toolchain + * @throws FormatterNotAvailableError if formatter requires compiler update * @throws Error if help command fails for other reasons * * @example * ```typescript * try { * await validator.checkFormatterAvailable(); - * console.log('Formatter is ready for use'); * } catch (error) { * if (error instanceof FormatterNotAvailableError) { - * console.error('Please update Compact compiler to use formatter'); + * console.error('Run: compact update'); * } * } * ``` @@ -74,11 +68,10 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { } /** - * Performs comprehensive environment validation for formatting operations. + * Performs complete environment validation for formatting operations. * - * Validates both the base Compact CLI environment and formatting-specific - * requirements. Ensures the formatter is available and accessible before - * proceeding with formatting operations. + * Validates both base CLI environment and formatter-specific requirements. + * Must be called before attempting any formatting operations. * * @returns Promise resolving to validation results with dev tools version * @throws CompactCliNotFoundError if CLI is not available @@ -87,7 +80,7 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { * @example * ```typescript * const { devToolsVersion } = await validator.validate(); - * console.log(`Environment validated with ${devToolsVersion}`); + * console.log(`Environment ready: ${devToolsVersion}`); * ``` */ async validate(): Promise<{ devToolsVersion: string }> { @@ -100,185 +93,79 @@ export class FormatterEnvironmentValidator extends BaseEnvironmentValidator { /** * Service for executing Compact formatting commands. * - * Handles the construction and execution of formatting commands for both check - * and write operations. Manages path resolution, command flag application, and - * provides specialized error handling for formatting failures. Supports both - * directory-wide and individual file formatting operations. + * Lightweight wrapper around `compact format` that constructs commands with + * appropriate flags and target paths, then delegates all formatting work and + * user feedback to the underlying tool. * * @example * ```typescript - * const formatter = new FormatterService(); - * + * const service = new FormatterService(); + * * // Check formatting without modifications - * const checkResult = await formatter.checkFormatting('src/contracts'); - * console.log('Is formatted:', checkResult.isFormatted); + * await service.format(['src/contracts'], true); * * // Format and write changes - * await formatter.formatAndWrite('src/contracts'); + * await service.format(['src/contracts'], false); * ``` */ export class FormatterService extends BaseCompactService { /** - * Formats files and writes the changes to disk. - * - * Executes the format command in write mode, applying formatting changes - * directly to the source files. Can target a specific directory path or - * operate on the entire source tree when no path is specified. - * - * @param targetPath - Optional path to target for formatting (directory or file) - * @returns Promise resolving to command execution results with stdout and stderr - * @throws FormatterError if formatting operation fails - * - * @example - * ```typescript - * // Format all files in the project - * await formatter.formatAndWrite(); - * - * // Format specific directory - * await formatter.formatAndWrite('src/contracts/security'); - * - * // Format specific file - * await formatter.formatAndWrite('src/Token.compact'); - * ``` - */ - async formatAndWrite( - targetPath?: string, - ): Promise<{ stdout: string; stderr: string }> { - const pathArg = targetPath ? ` "${targetPath}"` : ''; - const command = `compact format${pathArg}`; - return this.executeCompactCommand(command, 'Failed to format'); - } - - /** - * Checks if files are properly formatted without modifying them. - * - * Executes the format command in check mode to validate formatting without - * making changes. Returns both the execution results and a boolean indicating - * whether the files are properly formatted. Exit code 1 with output indicates - * formatting differences, while other errors represent actual failures. - * - * @param targetPath - Optional path to check for formatting (directory or file) - * @returns Promise resolving to check results including formatting status - * @throws FormatterError if check operation fails (excluding formatting differences) - * - * @example - * ```typescript - * // Check all files - * const result = await formatter.checkFormatting(); - * if (!result.isFormatted) { - * console.log('Formatting differences:', result.stdout); - * } - * - * // Check specific directory - * const result = await formatter.checkFormatting('src/contracts'); - * console.log('Directory is formatted:', result.isFormatted); - * ``` - */ - async checkFormatting(targetPath?: string): Promise<{ - stdout: string; - stderr: string; - isFormatted: boolean; - }> { - const pathArg = targetPath ? ` "${targetPath}"` : ''; - const command = `compact format --check${pathArg}`; - - try { - const result = await this.executeCompactCommand( - command, - 'Failed to check formatting', - ); - return { ...result, isFormatted: true }; - } catch (error: unknown) { - if ( - error instanceof FormatterError && - isPromisifiedChildProcessError(error.cause) - ) { - const childProcessError = error.cause; - if (childProcessError.code === 1 && childProcessError.stdout) { - return { - stdout: childProcessError.stdout, - stderr: childProcessError.stderr || '', - isFormatted: false, - }; - } - } - throw error; - } - } - - /** - * Formats a specific list of .compact files. + * Executes compact format command with specified targets and mode. * - * Applies formatting to the provided list of files, resolving their paths - * relative to the SRC_DIR. Useful for formatting only specific files rather - * than entire directories, such as when processing files from a git diff - * or user selection. + * Constructs the appropriate `compact format` command and executes it, + * allowing the underlying tool to handle all user feedback, progress + * reporting, and error messaging. * - * @param files - Array of relative file paths from SRC_DIR to format + * @param targets - Array of target paths (files or directories) to format + * @param checkMode - If true, uses --check flag to validate without writing * @returns Promise resolving to command execution results - * @throws FormatterError if any file fails to format + * @throws FormatterError if the formatting command fails * * @example * ```typescript + * // Check all files in src/ + * await service.format(['src'], true); + * * // Format specific files - * await formatter.formatFiles([ - * 'Token.compact', - * 'contracts/security/AccessControl.compact' - * ]); + * await service.format(['src/Token.compact', 'src/Utils.compact'], false); * - * // Handle empty file list gracefully - * await formatter.formatFiles([]); // Returns empty results + * // Format entire project + * await service.format([], false); * ``` */ - async formatFiles( - files: string[], + async format( + targets: string[] = [], + checkMode = true, ): Promise<{ stdout: string; stderr: string }> { - if (files.length === 0) { - return { stdout: '', stderr: '' }; - } + const checkFlag = checkMode ? ' --check' : ''; + const targetArgs = targets.length > 0 + ? ` ${targets.map(t => `"${t}"`).join(' ')}` + : ''; - const fileArgs = files.map((file) => `"${join(SRC_DIR, file)}"`).join(' '); - const command = `compact format ${fileArgs}`; - return this.executeCompactCommand( - command, - `Failed to format files: ${files.join(', ')}`, - ); + const command = `compact format${checkFlag}${targetArgs}`; + return this.executeCompactCommand(command, 'Formatting failed'); } /** * Creates formatting-specific error instances. * - * Wraps formatting failures in FormatterError instances that provide - * additional context including the target that failed to format. Extracts - * the target (file or directory) from error messages when possible for - * better error reporting and debugging. + * Wraps formatting failures in FormatterError instances for consistent + * error handling and reporting throughout the application. * * @param message - Error message describing the formatting failure * @param cause - Original error that caused the formatting failure (optional) - * @returns FormatterError instance with target context and cause information - * - * @example - * ```typescript - * // This method is called automatically by executeCompactCommand - * // when formatting fails, creating errors like: - * // FormatterError: Failed to format contracts/Token.compact - * ``` + * @returns FormatterError instance with cause information */ protected createError(message: string, cause?: unknown): Error { - // Extract target from error message for FormatterError - const match = message.match(/Failed to format(?: files:)? (.+)/); - const target = match ? match[1] : undefined; - return new FormatterError(message, target, cause); + return new FormatterError(message, undefined, cause); } } /** - * UI service specialized for formatting operations. + * UI service for formatting operations. * - * Provides formatting-specific user interface elements and messaging. - * Extends the shared UI service with formatting-focused information display, - * check result reporting, and operation status messaging. Ensures consistent - * visual presentation across formatting operations. + * Provides minimal UI elements specific to the formatting wrapper, + * since most user feedback is handled by the underlying `compact format` tool. */ export const FormatterUIService = { ...SharedUIService, @@ -287,21 +174,17 @@ export const FormatterUIService = { * Displays formatting environment information. * * Shows developer tools version and optional target directory information - * for formatting operations. Provides users with clear visibility into - * the formatting environment configuration. + * to provide context about the formatting environment. * * @param devToolsVersion - Version of the installed Compact developer tools - * @param targetDir - Optional target directory being formatted (relative to src/) + * @param targetDir - Optional target directory being formatted * * @example * ```typescript - * FormatterUIService.displayEnvInfo( - * 'compact-dev-tools 2.1.0', - * 'contracts' - * ); + * FormatterUIService.displayEnvInfo('compact 0.2.0', 'contracts'); * // Output: * // ℹ [FORMAT] TARGET_DIR: contracts - * // ℹ [FORMAT] Compact developer tools: compact-dev-tools 2.1.0 + * // ℹ [FORMAT] Compact developer tools: compact 0.2.0 * ``` */ displayEnvInfo(devToolsVersion: string, targetDir?: string): void { @@ -309,41 +192,10 @@ export const FormatterUIService = { }, /** - * Displays formatting start message with operation context. - * - * Informs users about the scope of the formatting operation, including - * the number of files found, the mode of operation (check vs write), - * and the directory being processed. Provides clear expectations about - * the work to be performed. + * Displays warning when no .compact files are found. * - * @param fileCount - Number of .compact files discovered for formatting - * @param mode - Operation mode: 'check' for validation, 'write' for formatting - * @param targetDir - Optional target directory being processed - * - * @example - * ```typescript - * FormatterUIService.showFormattingStart(3, 'check', 'contracts'); - * // Output: ℹ [FORMAT] Found 3 .compact file(s) to check formatting for in contracts/ - * - * FormatterUIService.showFormattingStart(5, 'write'); - * // Output: ℹ [FORMAT] Found 5 .compact file(s) to format - * ``` - */ - showFormattingStart( - fileCount: number, - mode: 'check' | 'write', - targetDir?: string, - ): void { - const action = mode === 'check' ? 'check formatting for' : 'format'; - SharedUIService.showOperationStart('FORMAT', action, fileCount, targetDir); - }, - - /** - * Displays warning when no .compact files are found for formatting. - * - * Provides clear feedback when the formatting operation cannot proceed - * because no source files were discovered in the target location. - * Helps users understand where files are expected to be located. + * Provides feedback when the formatting operation cannot proceed because + * no source files were discovered in the target location. * * @param targetDir - Optional target directory that was searched * @@ -351,204 +203,124 @@ export const FormatterUIService = { * ```typescript * FormatterUIService.showNoFiles('contracts'); * // Output: ⚠ [FORMAT] No .compact files found in contracts/. - * - * FormatterUIService.showNoFiles(); - * // Output: ⚠ [FORMAT] No .compact files found in src/. * ``` */ showNoFiles(targetDir?: string): void { SharedUIService.showNoFiles('FORMAT', targetDir); }, - - /** - * Displays formatting check results with appropriate visual feedback. - * - * Shows the outcome of formatting checks with success/failure indicators - * and optional formatting differences. Provides clear visual distinction - * between properly formatted code and code that needs formatting changes. - * - * @param isFormatted - Whether the checked files are properly formatted - * @param differences - Optional formatting differences to display - * - * @example - * ```typescript - * // Show success for properly formatted files - * FormatterUIService.showCheckResults(true); - * // Output: ✓ [FORMAT] All files are properly formatted - * - * // Show failure with differences - * FormatterUIService.showCheckResults(false, 'Token.compact needs formatting'); - * // Output: ✗ [FORMAT] Some files are not properly formatted - * // Formatting differences: - * // Token.compact needs formatting - * ``` - */ - showCheckResults(isFormatted: boolean, differences?: string): void { - const spinner = ora(); - - if (isFormatted) { - spinner.succeed(chalk.green('[FORMAT] All files are properly formatted')); - } else { - spinner.fail(chalk.red('[FORMAT] Some files are not properly formatted')); - if (differences) { - console.log(chalk.yellow('\nFormatting differences:')); - SharedUIService.printOutput(differences, chalk.white); - } - } - }, }; /** - * Main formatter orchestrator for Compact formatting operations. + * Main formatter coordinator for Compact formatting operations. * - * Coordinates the complete formatting workflow from environment validation - * through file processing. Manages formatting configuration including check/write - * modes, target specifications (directories or individual files), and provides - * progress reporting and error handling for both batch and individual file - * formatting operations. + * Lightweight orchestrator that validates environment, discovers files within + * the project's src/ structure, then delegates to `compact format` for actual + * formatting work. Acts as a bridge between project-specific configuration + * and the underlying formatter tool. * * @example * ```typescript * // Check formatting of all files - * const formatter = new CompactFormatter(false); + * const formatter = new CompactFormatter(true); * await formatter.format(); * * // Format specific files - * const formatter = new CompactFormatter(true, ['Token.compact', 'AccessControl.compact']); + * const formatter = new CompactFormatter(false, ['Token.compact']); * await formatter.format(); * * // Format specific directory - * const formatter = new CompactFormatter(true, ['contracts']); + * const formatter = new CompactFormatter(false, [], 'contracts'); * await formatter.format(); * ``` */ export class CompactFormatter extends BaseCompactOperation { private readonly environmentValidator: FormatterEnvironmentValidator; private readonly formatterService: FormatterService; - private readonly writeMode: boolean; - private readonly targets: string[]; + private readonly checkMode: boolean; + private readonly specificFiles: string[]; /** - * Creates a new CompactFormatter instance with specified configuration. + * Creates a new CompactFormatter instance. * - * Initializes the formatter with operation mode (check vs write), target - * specifications (directories or files), and sets up the necessary services - * for environment validation and command execution. Handles both directory - * and individual file targeting scenarios. + * Initializes the formatter with operation mode and target configuration. + * Sets up environment validation and command execution services. * - * @param writeMode - Whether to write formatting changes (true) or just check (false) - * @param targets - Array of target directories or files to format - * @param execFn - Optional command execution function for testing/customization + * @param checkMode - If true, validates formatting without writing changes + * @param specificFiles - Array of specific .compact files to target + * @param targetDir - Optional directory within src/ to limit scope + * @param execFn - Optional command execution function for testing * * @example * ```typescript - * // Check formatting of all files (default) - * const formatter = new CompactFormatter(); - * - * // Format all files with changes written + * // Check mode for CI/CD * const formatter = new CompactFormatter(true); * - * // Check specific files without writing - * const formatter = new CompactFormatter(false, ['Token.compact', 'AccessControl.compact']); - * - * // Format specific directory - * const formatter = new CompactFormatter(true, ['contracts']); + * // Format specific files + * const formatter = new CompactFormatter(false, ['Token.compact']); * - * // For testing with custom execution function - * const formatter = new CompactFormatter(false, [], mockExecFn); + * // Format directory + * const formatter = new CompactFormatter(false, [], 'contracts'); * ``` */ constructor( - writeMode = false, - targets: string[] = [], + checkMode = true, + specificFiles: string[] = [], + targetDir?: string, execFn?: ExecFunction, ) { - // For single directory target, use it as targetDir - const targetDir = - targets.length === 1 && !targets[0].endsWith('.compact') - ? targets[0] - : undefined; - super(targetDir); - this.writeMode = writeMode; - this.targets = targets; + this.checkMode = checkMode; + this.specificFiles = specificFiles; this.environmentValidator = new FormatterEnvironmentValidator(execFn); this.formatterService = new FormatterService(execFn); } /** - * Factory method to create a CompactFormatter from command-line arguments. + * Factory method to create CompactFormatter from command-line arguments. * - * Parses command-line arguments to construct a properly configured - * CompactFormatter instance. Handles flag processing, target specification, - * and mode determination from command-line inputs. Provides the primary - * interface between CLI arguments and formatter configuration. + * Parses command-line arguments to construct a properly configured formatter. + * Handles --check flag, --dir targeting, and specific file arguments. * * @param args - Raw command-line arguments array - * @returns Configured CompactFormatter instance ready for execution - * @throws Error if arguments are malformed (e.g., --dir without directory name) + * @returns Configured CompactFormatter instance + * @throws Error if arguments are malformed * * @example * ```typescript - * // Parse from command line: ['--check', '--dir', 'contracts'] - * const formatter = CompactFormatter.fromArgs([ - * '--check', - * '--dir', 'contracts' - * ]); - * - * // Parse write mode with specific files: ['--write', 'Token.compact'] - * const formatter = CompactFormatter.fromArgs([ - * '--write', - * 'Token.compact' - * ]); - * - * // Parse from actual process arguments - * const formatter = CompactFormatter.fromArgs(process.argv.slice(2)); + * // Parse: ['--check', '--dir', 'contracts'] + * const formatter = CompactFormatter.fromArgs(['--check', '--dir', 'contracts']); + * + * // Parse: ['Token.compact', 'Utils.compact'] + * const formatter = CompactFormatter.fromArgs(['Token.compact', 'Utils.compact']); * ``` */ - static fromArgs(args: string[]): CompactFormatter { - const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); - - let writeMode = false; - const targets: string[] = []; - - // Add targetDir to targets if specified - if (targetDir) { - targets.push(targetDir); - } - - for (const arg of remainingArgs) { - if (arg === '--write') { - writeMode = true; - } else if (!arg.startsWith('--')) { - targets.push(arg); - } +static fromArgs(args: string[]): CompactFormatter { + const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); + + let checkMode = true; // Default to check mode + const specificFiles: string[] = []; + + for (const arg of remainingArgs) { + if (arg === '--check') { + checkMode = true; // Explicit check mode (though it's already default) + } else if (arg === '--write') { + checkMode = false; // Write mode + } else if (!arg.startsWith('--')) { + specificFiles.push(arg); } - - return new CompactFormatter(writeMode, targets); } + return new CompactFormatter(checkMode, specificFiles, targetDir); +} + /** - * Validates the formatting environment and displays configuration information. + * Validates formatting environment and displays configuration. * - * Performs comprehensive environment validation including CLI availability, - * formatter availability verification, and configuration display. Must be - * called before attempting formatting operations. + * Ensures both CLI and formatter are available before proceeding with + * formatting operations. Displays environment information for user feedback. * - * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws CompactCliNotFoundError if CLI is not available * @throws FormatterNotAvailableError if formatter is not available - * - * @example - * ```typescript - * try { - * await formatter.validateEnvironment(); - * // Environment is valid, proceed with formatting - * } catch (error) { - * if (error instanceof FormatterNotAvailableError) { - * console.error('Please update Compact compiler to use formatter'); - * } - * } - * ``` */ async validateEnvironment(): Promise { const { devToolsVersion } = await this.environmentValidator.validate(); @@ -556,38 +328,33 @@ export class CompactFormatter extends BaseCompactOperation { } /** - * Displays warning message when no .compact files are found. + * Displays warning when no .compact files are found. * - * Shows operation-specific messaging when file discovery returns no results. - * Provides clear feedback about the search location and expected file locations. + * Provides user feedback when file discovery returns no results. */ showNoFiles(): void { FormatterUIService.showNoFiles(this.targetDir); } /** - * Executes the complete formatting workflow. + * Executes the formatting workflow. * - * Orchestrates the full formatting process: validates environment, determines - * operation mode (specific files vs directory), and executes the appropriate - * formatting strategy. Handles both check and write operations with progress - * reporting and error handling. + * Validates environment, then either formats specific files or discovers + * and formats files within the target directory. Delegates actual formatting + * to the underlying `compact format` command. * - * @throws CompactCliNotFoundError if Compact CLI is not available + * @throws CompactCliNotFoundError if CLI is not available * @throws FormatterNotAvailableError if formatter is not available * @throws DirectoryNotFoundError if target directory doesn't exist - * @throws FormatterError if any file fails to format + * @throws FormatterError if formatting command fails * * @example * ```typescript - * const formatter = new CompactFormatter(false, ['contracts']); - * * try { * await formatter.format(); - * console.log('Formatting check completed successfully'); * } catch (error) { - * if (error instanceof FormatterError) { - * console.error(`Formatting failed: ${error.message}`); + * if (error instanceof FormatterNotAvailableError) { + * console.error('Update compiler: compact update'); * } * } * ``` @@ -595,117 +362,27 @@ export class CompactFormatter extends BaseCompactOperation { async format(): Promise { await this.validateEnvironment(); - // Handle specific file targets - if ( - this.targets.length > 0 && - this.targets.every((target) => target.endsWith('.compact')) - ) { - return this.formatSpecificFiles(); - } - - // Handle directory target or current directory - return this.formatDirectory(); - } - - /** - * Formats or checks specific files provided as command-line arguments. - * - * Handles formatting operations when specific .compact files are provided - * as targets. In check mode, validates each file individually with separate - * status reporting. In write mode, formats all specified files in a single - * operation for efficiency. - * - * @throws FormatterError if any file fails to format or check - * - * @example - * ```typescript - * // This method is called internally when targets are specific files: - * // compact-formatter Token.compact AccessControl.compact - * ``` - */ - private async formatSpecificFiles(): Promise { - if (!this.writeMode) { - for (const file of this.targets) { - await this.checkFile(file); - } - } else { - const result = await this.formatterService.formatFiles(this.targets); - SharedUIService.printOutput(result.stdout, chalk.cyan); - SharedUIService.printOutput(result.stderr, chalk.yellow); + // Handle specific files + if (this.specificFiles.length > 0) { + const filePaths = this.specificFiles.map(file => join(SRC_DIR, file)); + await this.formatterService.format(filePaths, this.checkMode); + return; } - } - /** - * Formats or checks all files in a directory or the entire source tree. - * - * Handles batch formatting operations for directory targets or the entire - * project when no specific targets are provided. Discovers files, reports - * progress, and executes the appropriate formatting strategy based on the - * operation mode. - * - * @throws FormatterError if directory formatting fails - * - * @example - * ```typescript - * // This method is called internally for directory operations: - * // compact-formatter --dir contracts - * // compact-formatter # formats entire src/ directory - * ``` - */ - private async formatDirectory(): Promise { - const { files, searchDir } = await this.discoverFiles(); + // Handle directory or entire project + const { files } = await this.discoverFiles(); if (files.length === 0) return; - const mode = this.writeMode ? 'write' : 'check'; - FormatterUIService.showFormattingStart(files.length, mode, this.targetDir); - - if (!this.writeMode) { - const result = await this.formatterService.checkFormatting(searchDir); - FormatterUIService.showCheckResults(result.isFormatted, result.stdout); - } else { - const result = await this.formatterService.formatAndWrite(searchDir); - - // Successful formatting typically produces no output - if (result.stdout.trim()) { - SharedUIService.printOutput(result.stdout, chalk.cyan); - } - if (result.stderr.trim()) { - SharedUIService.printOutput(result.stderr, chalk.yellow); - } - - const spinner = ora(); - spinner.succeed( - chalk.green(`[FORMAT] Processed ${files.length} file(s)`), - ); - } - } - - /** - * Checks formatting for a specific file. - */ - private async checkFile(file: string): Promise { - const result = await this.formatterService.checkFormatting(file); + const mode = this.checkMode ? 'check formatting for' : 'format'; + SharedUIService.showOperationStart('FORMAT', mode, files.length, this.targetDir); - if (result.isFormatted) { - const spinner = ora(); - spinner.succeed(chalk.green(`[FORMAT] ${file} is properly formatted`)); - } else { - const spinner = ora(); - spinner.fail(chalk.red(`[FORMAT] ${file} is not properly formatted`)); - if (result.stdout) { - SharedUIService.printOutput(result.stdout, chalk.white); - } - } + const targetPath = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; + await this.formatterService.format([targetPath], this.checkMode); } /** * For testing - expose internal state */ - get testWriteMode(): boolean { - return this.writeMode; - } - - get testTargets(): string[] { - return this.targets; - } + get testCheckMode(): boolean { return this.checkMode; } + get testSpecificFiles(): string[] { return this.specificFiles; } } From 64bb99fe8faa179e5981e839103699730bebacb0 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 00:39:48 -0300 Subject: [PATCH 35/44] improve docs --- compact/src/runFormatter.ts | 145 +++++++++++++++++------------------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts index 1346a785..358e5f90 100644 --- a/compact/src/runFormatter.ts +++ b/compact/src/runFormatter.ts @@ -10,25 +10,34 @@ import { } from './types/errors.js'; /** - * Main entry point for the Compact formatter CLI application. + * Main entry point for the Compact formatter CLI binary. * - * Coordinates the complete formatting workflow from command-line argument - * parsing through execution and error handling. Provides comprehensive user - * feedback and detailed error reporting for both check and write formatting - * operations. + * This file serves as the executable binary defined in package.json and is + * invoked through build scripts via Turbo, Yarn, or direct command execution. + * Acts as a lightweight wrapper around the `compact format` command, providing + * environment validation and project-specific file discovery before delegating + * to the underlying formatter tool. * - * The function manages the full application lifecycle: + * The function manages the wrapper lifecycle: * - * 1. Parses command-line arguments into formatter configuration. - * 2. Executes formatting operations with visual progress indicators. - * 3. Handles all error scenarios with actionable user guidance. - * 4. Exits with appropriate status codes for automated workflows. + * 1. Validates environment (CLI availability, formatter compatibility). + * 2. Discovers files within the project's src/ structure. + * 3. Constructs and executes appropriate `compact format` commands. + * 4. Handles environment errors while letting format errors pass through. * * @example * ```bash - * # Called from command line as: - * compact-formatter --check --dir ./contracts/src/security - * compact-formatter --write ./contracts/src/access/AccessControl.compact + * # Direct binary execution: + * ./node_modules/.bin/compact-formatter --check --dir security + * ./node_modules/.bin/compact-formatter Token.compact AccessControl.compact + * + * # Via package.json scripts: + * yarn format + * yarn format:fix + * + * # Via Turbo: + * turbo format + * turbo format:fix * ``` */ async function runFormatter(): Promise { @@ -45,29 +54,31 @@ async function runFormatter(): Promise { } /** - * Specialized error handler for formatting operation failures. + * Streamlined error handler focused on environment and setup issues. * - * Implements multi-layered error handling that addresses both common infrastructure - * issues and formatting-specific problems. Provides detailed diagnostic information - * and recovery suggestions tailored to formatting workflows. + * Since the underlying `compact format` command handles most user-facing errors + * and feedback (including formatting differences and file processing failures), + * this handler primarily focuses on environment validation errors and setup + * issues that prevent the formatter from running. * - * Error handling hierarchy: + * Error handling priority: * - * 1. Common errors (CLI availability, directory validation). - * 2. Formatter availability errors (toolchain compatibility issues). - * 3. Formatting operation errors (file processing failures). - * 4. Argument parsing errors (command-line usage problems). - * 5. Unexpected errors (with comprehensive troubleshooting). + * 1. Common errors (CLI not found, directory issues, permissions). + * 2. Formatter availability errors (toolchain version compatibility). + * 3. Argument parsing errors (malformed command-line usage). + * 4. Formatting errors (let the underlying tool's output show through). * - * @param error - The error that occurred during formatting operations - * @param spinner - Ora spinner instance for consistent visual feedback + * @param error - The error that occurred during formatter execution + * @param spinner - Ora spinner instance for consistent UI feedback * * @example * ```typescript - * // This function handles errors such as: - * // - FormatterNotAvailableError: Formatter not available in current toolchain - * // - FormatterError: Failed to format Token.compact - * // - DirectoryNotFoundError: Target directory contracts/ does not exist + * // This function primarily handles setup errors like: + * // - FormatterNotAvailableError: Formatter requires compiler 0.25.0+ + * // - CompactCliNotFoundError: 'compact' CLI not found in PATH + * // - DirectoryNotFoundError: Target directory security/ does not exist + * + * // Formatting errors from `compact format` are displayed directly * ``` */ function handleError(error: unknown, spinner: Ora): void { @@ -86,24 +97,24 @@ function handleError(error: unknown, spinner: Ora): void { return; } - // FormatterError - specific to formatting + // FormatterError - let the underlying tool's output show through if (error instanceof Error && error.name === 'FormatterError') { const formatterError = error as FormatterError; - spinner.fail( - chalk.red( - `[FORMAT] Formatting failed${formatterError.target ? ` for: ${formatterError.target}` : ''}`, - ), - ); + // For most formatting errors, the underlying `compact format` command + // already provides good user feedback, so we just show a simple failure message + spinner.fail(chalk.red('[FORMAT] Formatting operation failed')); + + // Show additional details if available if (isPromisifiedChildProcessError(formatterError.cause)) { const execError = formatterError.cause; - if (execError.stderr && !execError.stderr.includes('stdout')) { - console.log( - chalk.red(` Additional error details: ${execError.stderr}`), - ); + + // The underlying compact format command output is usually sufficient, + // but show additional details if they're helpful + if (execError.stderr && !execError.stderr.includes('compact format')) { + console.log(chalk.red(` ${execError.stderr}`)); } - if (execError.stdout) { - console.log(chalk.yellow(' Output:')); + if (execError.stdout && execError.stdout.trim()) { console.log(chalk.yellow(` ${execError.stdout}`)); } } @@ -122,35 +133,33 @@ function handleError(error: unknown, spinner: Ora): void { } /** - * Displays comprehensive usage documentation for the Compact formatter CLI. + * Displays comprehensive usage documentation for the Compact formatter CLI binary. * - * Provides complete reference documentation including all command-line options, - * practical usage patterns, and integration examples. Covers both basic formatting - * operations and advanced workflows including check mode, directory targeting, - * and specific file processing. + * Provides complete reference documentation for the package.json binary, + * including all command-line options and integration examples. Emphasizes that + * this is a wrapper around `compact format` that adds project-specific file + * discovery and environment validation. * * The help documentation includes: * - * - Detailed option descriptions with behavior explanations. - * - Comprehensive examples for common formatting scenarios. - * - Integration patterns with build systems and CI/CD workflows. - * - Best practices for different development workflows. + * - Wrapper-specific options (--dir for project structure). + * - Direct binary execution examples. + * - Package.json script integration patterns. + * - Turbo and Yarn workflow examples. + * - Reference to underlying `compact format` capabilities. * * @example * ```typescript * // Automatically displayed when argument parsing fails: * // compact-formatter --dir # Missing directory name - * // Shows complete usage guide to assist proper command construction + * // Shows complete usage guide including script integration examples * ``` */ function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); console.log(chalk.yellow('\nOptions:')); - console.log( - chalk.yellow( - ' --check Check if files are properly formatted (no modifications)', - ), - ); + console.log(chalk.yellow(' --check Check if files are properly formatted (default)')); + console.log(chalk.yellow(' --write Write formatting changes to files')); console.log( chalk.yellow( ' --dir Format specific directory (access, archive, security, token, utils)', @@ -159,17 +168,17 @@ function showUsageHelp(): void { console.log(chalk.yellow('\nExamples:')); console.log( chalk.yellow( - ' compact-formatter # Format all files', + ' compact-formatter # Check all files (default)', ), ); console.log( chalk.yellow( - ' compact-formatter --check # Check all files', + ' compact-formatter --write # Format all files', ), ); console.log( chalk.yellow( - ' compact-formatter --dir security # Format security directory', + ' compact-formatter --write --dir security # Format security directory', ), ); console.log( @@ -179,7 +188,7 @@ function showUsageHelp(): void { ); console.log( chalk.yellow( - ' compact-formatter file1.compact file2.compact # Format specific files', + ' compact-formatter --write f1.compact f2.compact # Format specific files', ), ); console.log( @@ -187,22 +196,6 @@ function showUsageHelp(): void { ' compact-formatter --check file1.compact # Check specific file', ), ); - console.log(chalk.yellow('\nIntegration examples:')); - console.log( - chalk.yellow( - ' turbo format # Full formatting', - ), - ); - console.log( - chalk.yellow( - ' turbo format:security # Directory formatting', - ), - ); - console.log( - chalk.yellow( - ' turbo format:check # Check formatting', - ), - ); } runFormatter(); From 0feb0bbdc36b7b5771b88eb50d5831a232f7d5d8 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 00:39:56 -0300 Subject: [PATCH 36/44] update tests --- compact/test/Formatter.test.ts | 408 ++++++++------------------------- 1 file changed, 97 insertions(+), 311 deletions(-) diff --git a/compact/test/Formatter.test.ts b/compact/test/Formatter.test.ts index cae9e840..879580fe 100644 --- a/compact/test/Formatter.test.ts +++ b/compact/test/Formatter.test.ts @@ -46,59 +46,38 @@ describe('FormatterEnvironmentValidator', () => { describe('checkFormatterAvailable', () => { it('succeeds when formatter is available', async () => { - const testData = { - expectedCommand: 'compact help format', - response: { stdout: 'Format help text', stderr: '' }, - }; - - mockExec.mockResolvedValue(testData.response); + mockExec.mockResolvedValue({ stdout: 'Format help text', stderr: '' }); await expect(validator.checkFormatterAvailable()).resolves.not.toThrow(); - expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + expect(mockExec).toHaveBeenCalledWith('compact help format'); }); it('throws FormatterNotAvailableError when formatter not available', async () => { - const testData = { - error: Object.assign(new Error('Command failed'), { - stderr: 'formatter not available', - stdout: '', - }), - }; - - mockExec.mockRejectedValue(testData.error); - await expect(validator.checkFormatterAvailable()).rejects.toThrow( - FormatterNotAvailableError, - ); - }); - - it('re-throws other errors', async () => { - const testData = { - error: new Error('Different error'), - }; - - mockExec.mockRejectedValue(testData.error); + const error = Object.assign(new Error('Command failed'), { + stderr: 'formatter not available', + stdout: '', + code: 1, + }); + mockExec.mockRejectedValue(error); await expect(validator.checkFormatterAvailable()).rejects.toThrow( - testData.error, + 'Formatter not available' ); }); }); describe('validate', () => { it('returns dev tools version when validation succeeds', async () => { - const testData = { - devToolsVersion: 'compact 0.2.0', - expectedResult: { devToolsVersion: 'compact 0.2.0' }, - }; + const devToolsVersion = 'compact 0.2.0'; mockExec - .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) // checkCompactAvailable - .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) // getDevToolsVersion + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) // checkCompactAvailable + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) // getDevToolsVersion .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); // checkFormatterAvailable const result = await validator.validate(); - expect(result).toEqual(testData.expectedResult); + expect(result).toEqual({ devToolsVersion }); }); }); }); @@ -113,197 +92,63 @@ describe('FormatterService', () => { service = new FormatterService(mockExec); }); - describe('formatAndWrite', () => { - it('constructs correct command with target path', async () => { - const testData = { - targetPath: 'security', - expectedCommand: 'compact format "security"', - response: { stdout: 'Formatted successfully', stderr: '' }, - }; + describe('format', () => { + it('constructs command for check mode', async () => { + const targets = ['security']; + const response = { stdout: 'Check complete', stderr: '' }; - mockExec.mockResolvedValue(testData.response); + mockExec.mockResolvedValue(response); - const result = await service.formatAndWrite(testData.targetPath); + const result = await service.format(targets, true); - expect(result).toEqual(testData.response); - expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + expect(result).toEqual(response); + expect(mockExec).toHaveBeenCalledWith('compact format --check "security"'); }); - it('constructs command without target path', async () => { - const testData = { - expectedCommand: 'compact format', - response: { stdout: 'Formatted successfully', stderr: '' }, - }; + it('constructs command for write mode', async () => { + const targets = ['security']; + const response = { stdout: 'Format complete', stderr: '' }; - mockExec.mockResolvedValue(testData.response); + mockExec.mockResolvedValue(response); - await service.formatAndWrite(); + await service.format(targets, false); - expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); + expect(mockExec).toHaveBeenCalledWith('compact format "security"'); }); - it('throws FormatterError on failure', async () => { - const testData = { - targetPath: 'security', - error: new Error('Format failed'), - }; + it('constructs command without targets', async () => { + mockExec.mockResolvedValue({ stdout: '', stderr: '' }); - mockExec.mockRejectedValue(testData.error); + await service.format([], false); - await expect(service.formatAndWrite(testData.targetPath)).rejects.toThrow( - FormatterError, - ); + expect(mockExec).toHaveBeenCalledWith('compact format'); }); - }); - describe('checkFormatting', () => { - it('returns true when no formatting needed', async () => { - const testData = { - targetPath: 'security', - expectedCommand: 'compact format --check "security"', - response: { stdout: 'All files formatted', stderr: '' }, - expectedResult: { - stdout: 'All files formatted', - stderr: '', - isFormatted: true, - }, - }; - - mockExec.mockResolvedValue(testData.response); - - const result = await service.checkFormatting(testData.targetPath); - - expect(result).toEqual(testData.expectedResult); - expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); - }); - - it('returns false when formatting differences exist', async () => { - const testData = { - error: Object.assign(new Error('Formatting differences'), { - code: 1, - stdout: 'Differences found', - stderr: 'Formatting failed', - }), - expectedResult: { - stdout: 'Differences found', - stderr: 'Formatting failed', - isFormatted: false, - }, - }; - - mockExec.mockRejectedValue(testData.error); - const result = await service.checkFormatting(); - - expect(result).toEqual(testData.expectedResult); - }); + it('constructs command with multiple targets', async () => { + const targets = ['src/contracts', 'src/utils']; + mockExec.mockResolvedValue({ stdout: '', stderr: '' }); - it('throws FormatterError for unexpected failures', async () => { - const testData = { - error: new Error('Unexpected error'), - }; + await service.format(targets, false); - mockExec.mockRejectedValue(testData.error); - - await expect(service.checkFormatting()).rejects.toThrow(FormatterError); + expect(mockExec).toHaveBeenCalledWith('compact format "src/contracts" "src/utils"'); }); - it('handles FormatterError with PromisifiedChildProcessError cause', async () => { - const childProcessError = Object.assign( - new Error('Format check failed'), - { - code: 1, - stdout: 'Differences found', - stderr: 'Formatting failed', - }, - ); + it('throws FormatterError on failure', async () => { + mockExec.mockRejectedValue(new Error('Format failed')); - const formatterError = new FormatterError( - 'Failed to check formatting', - undefined, - childProcessError, + await expect(service.format(['security'], false)).rejects.toThrow( + FormatterError, ); - - // Mock executeCompactCommand to throw the FormatterError - const executeSpy = vi.spyOn(service as any, 'executeCompactCommand'); - executeSpy.mockRejectedValue(formatterError); - - const result = await service.checkFormatting(); - - expect(result).toEqual({ - stdout: 'Differences found', - stderr: 'Formatting failed', - isFormatted: false, - }); - - executeSpy.mockRestore(); - }); - }); - - describe('formatFiles', () => { - it('formats multiple files correctly', async () => { - const testData = { - files: ['MyToken.compact', 'Security.compact'], - expectedCommand: `compact format "${join(SRC_DIR, 'MyToken.compact')}" "${join(SRC_DIR, 'Security.compact')}"`, - response: { stdout: 'Files formatted', stderr: '' }, - }; - - mockExec.mockResolvedValue(testData.response); - - const result = await service.formatFiles(testData.files); - - expect(result).toEqual(testData.response); - expect(mockExec).toHaveBeenCalledWith(testData.expectedCommand); - }); - - it('returns empty result for empty file list', async () => { - const testData = { - files: [], - expectedResult: { stdout: '', stderr: '' }, - }; - - const result = await service.formatFiles(testData.files); - - expect(result).toEqual(testData.expectedResult); - expect(mockExec).not.toHaveBeenCalled(); }); }); describe('createError', () => { - it('extracts target from error message', () => { - const testData = { - message: 'Failed to format security', - expectedTarget: 'security', - }; - - // Access protected method using bracket notation - const error = service['createError'](testData.message); - - expect(error).toBeInstanceOf(FormatterError); - expect((error as FormatterError).target).toBe(testData.expectedTarget); - }); - - it('extracts target from file error message', () => { - const testData = { - message: 'Failed to format files: MyToken.compact, Security.compact', - expectedTarget: 'MyToken.compact, Security.compact', - }; - - const error = service['createError'](testData.message); - - expect(error).toBeInstanceOf(FormatterError); - expect((error as FormatterError).target).toBe(testData.expectedTarget); - }); - - it('handles message without target', () => { - const testData = { - message: 'Some generic error', - expectedTarget: undefined, - }; - - const error = service['createError'](testData.message); + it('creates FormatterError', () => { + const message = 'Failed to format'; + const error = service['createError'](message); expect(error).toBeInstanceOf(FormatterError); - expect((error as FormatterError).target).toBe(testData.expectedTarget); + expect(error.message).toBe(message); }); }); }); @@ -322,16 +167,11 @@ describe('FormatterUIService', () => { }); describe('displayEnvInfo', () => { - it('displays environment information with target directory', () => { - const testData = { - devToolsVersion: 'compact 0.2.0', - targetDir: 'security', - }; - - FormatterUIService.displayEnvInfo( - testData.devToolsVersion, - testData.targetDir, - ); + it('displays environment information', () => { + const devToolsVersion = 'compact 0.2.0'; + const targetDir = 'security'; + + FormatterUIService.displayEnvInfo(devToolsVersion, targetDir); expect(mockSpinner.info).toHaveBeenCalledWith( '[FORMAT] TARGET_DIR: security', @@ -340,58 +180,6 @@ describe('FormatterUIService', () => { '[FORMAT] Compact developer tools: compact 0.2.0', ); }); - - it('displays environment information without target directory', () => { - const testData = { - devToolsVersion: 'compact 0.2.0', - }; - - FormatterUIService.displayEnvInfo(testData.devToolsVersion); - - expect(mockSpinner.info).toHaveBeenCalledWith( - '[FORMAT] Compact developer tools: compact 0.2.0', - ); - expect(mockSpinner.info).not.toHaveBeenCalledWith( - expect.stringContaining('TARGET_DIR'), - ); - }); - }); - - describe('showCheckResults', () => { - it('shows success when files are formatted', () => { - const testData = { - isFormatted: true, - }; - - FormatterUIService.showCheckResults(testData.isFormatted); - - expect(mockSpinner.succeed).toHaveBeenCalledWith( - '[FORMAT] All files are properly formatted', - ); - }); - - it('shows failure with differences when files need formatting', () => { - const testData = { - isFormatted: false, - differences: 'Some formatting differences', - }; - - const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - - FormatterUIService.showCheckResults( - testData.isFormatted, - testData.differences, - ); - - expect(mockSpinner.fail).toHaveBeenCalledWith( - '[FORMAT] Some files are not properly formatted', - ); - expect(consoleSpy).toHaveBeenCalledWith( - expect.stringContaining('Formatting differences'), - ); - - consoleSpy.mockRestore(); - }); }); }); @@ -408,86 +196,84 @@ describe('CompactFormatter', () => { it('creates instance with default parameters', () => { formatter = new CompactFormatter(); - expect(formatter).toBeInstanceOf(CompactFormatter); - expect(formatter.testWriteMode).toBe(false); - expect(formatter.testTargets).toEqual([]); + expect(formatter.testCheckMode).toBe(true); + expect(formatter.testSpecificFiles).toEqual([]); }); - it('creates instance with all parameters', () => { - const testData = { - writeMode: true, - targets: ['security', 'MyToken.compact'], - }; + it('creates instance with parameters', () => { + const checkMode = false; + const specificFiles = ['Token.compact']; + const targetDir = 'security'; - formatter = new CompactFormatter( - testData.writeMode, - testData.targets, - mockExec, - ); + formatter = new CompactFormatter(checkMode, specificFiles, targetDir, mockExec); - expect(formatter.testWriteMode).toBe(testData.writeMode); - expect(formatter.testTargets).toEqual(testData.targets); + expect(formatter.testCheckMode).toBe(checkMode); + expect(formatter.testSpecificFiles).toEqual(specificFiles); }); }); describe('fromArgs', () => { - it('parses empty arguments', () => { - formatter = CompactFormatter.fromArgs([]); + it('parses check mode', () => { + formatter = CompactFormatter.fromArgs(['--check']); - expect(formatter.testWriteMode).toBe(false); - expect(formatter.testTargets).toEqual([]); + expect(formatter.testCheckMode).toBe(true); }); - it('parses --write flag', () => { - const testData = { - args: ['--write'], - expectedWriteMode: true, - }; + it('parses specific files', () => { + const args = ['Token.compact', 'AccessControl.compact']; + formatter = CompactFormatter.fromArgs(args); - formatter = CompactFormatter.fromArgs(testData.args); - - expect(formatter.testWriteMode).toBe(testData.expectedWriteMode); + expect(formatter.testSpecificFiles).toEqual(args); }); - it('parses complex arguments', () => { - const testData = { - args: ['--dir', 'security', '--write', 'MyToken.compact'], - expectedTargets: ['security', 'MyToken.compact'], - expectedWriteMode: true, - }; - - formatter = CompactFormatter.fromArgs(testData.args); + it('parses directory and check mode', () => { + formatter = CompactFormatter.fromArgs(['--dir', 'security', '--check']); - expect(formatter.testWriteMode).toBe(testData.expectedWriteMode); - expect(formatter.testTargets).toEqual(testData.expectedTargets); + expect(formatter.testCheckMode).toBe(true); }); }); describe('validateEnvironment', () => { - it('calls validator and displays environment info', async () => { - const testData = { - devToolsVersion: 'compact 0.2.0', - targetDir: 'security', - }; - + it('validates environment successfully', async () => { + const devToolsVersion = 'compact 0.2.0'; + mockExec - .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) - .mockResolvedValueOnce({ stdout: testData.devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) + .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }); const displaySpy = vi .spyOn(FormatterUIService, 'displayEnvInfo') .mockImplementation(() => {}); - formatter = new CompactFormatter(false, [testData.targetDir], mockExec); + formatter = new CompactFormatter(false, [], 'security', mockExec); await formatter.validateEnvironment(); - expect(displaySpy).toHaveBeenCalledWith( - testData.devToolsVersion, - testData.targetDir, - ); + expect(displaySpy).toHaveBeenCalledWith(devToolsVersion, 'security'); + displaySpy.mockRestore(); + }); + }); + + describe('format', () => { + it('formats specific files', async () => { + const specificFiles = ['Token.compact']; + formatter = new CompactFormatter(false, specificFiles, undefined, mockExec); + + // Mock environment validation + mockExec + .mockResolvedValueOnce({ stdout: 'compact 0.2.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'compact 0.2.0', stderr: '' }) + .mockResolvedValueOnce({ stdout: 'Format help', stderr: '' }) + // Mock format command + .mockResolvedValueOnce({ stdout: 'Formatted', stderr: '' }); + + const displaySpy = vi.spyOn(FormatterUIService, 'displayEnvInfo').mockImplementation(() => {}); + + await formatter.format(); + // Should call format with the specific file path + expect(mockExec).toHaveBeenCalledWith(`compact format "${join(SRC_DIR, 'Token.compact')}"`); displaySpy.mockRestore(); }); }); From 6623ae89fbb36a88abeab3272a3661d82083dab6 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 00:47:49 -0300 Subject: [PATCH 37/44] fix fmt --- compact/src/Formatter.ts | 50 ++++++++++++++++++++-------------- compact/src/runFormatter.ts | 12 ++++++-- compact/test/Formatter.test.ts | 39 ++++++++++++++++++-------- 3 files changed, 65 insertions(+), 36 deletions(-) diff --git a/compact/src/Formatter.ts b/compact/src/Formatter.ts index 4d097805..ecb70751 100644 --- a/compact/src/Formatter.ts +++ b/compact/src/Formatter.ts @@ -138,9 +138,8 @@ export class FormatterService extends BaseCompactService { checkMode = true, ): Promise<{ stdout: string; stderr: string }> { const checkFlag = checkMode ? ' --check' : ''; - const targetArgs = targets.length > 0 - ? ` ${targets.map(t => `"${t}"`).join(' ')}` - : ''; + const targetArgs = + targets.length > 0 ? ` ${targets.map((t) => `"${t}"`).join(' ')}` : ''; const command = `compact format${checkFlag}${targetArgs}`; return this.executeCompactCommand(command, 'Formatting failed'); @@ -294,24 +293,24 @@ export class CompactFormatter extends BaseCompactOperation { * const formatter = CompactFormatter.fromArgs(['Token.compact', 'Utils.compact']); * ``` */ -static fromArgs(args: string[]): CompactFormatter { - const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); + static fromArgs(args: string[]): CompactFormatter { + const { targetDir, remainingArgs } = CompactFormatter.parseBaseArgs(args); - let checkMode = true; // Default to check mode - const specificFiles: string[] = []; + let checkMode = true; // Default to check mode + const specificFiles: string[] = []; - for (const arg of remainingArgs) { - if (arg === '--check') { - checkMode = true; // Explicit check mode (though it's already default) - } else if (arg === '--write') { - checkMode = false; // Write mode - } else if (!arg.startsWith('--')) { - specificFiles.push(arg); + for (const arg of remainingArgs) { + if (arg === '--check') { + checkMode = true; + } else if (arg === '--write') { + checkMode = false; // Write mode + } else if (!arg.startsWith('--')) { + specificFiles.push(arg); + } } - } - return new CompactFormatter(checkMode, specificFiles, targetDir); -} + return new CompactFormatter(checkMode, specificFiles, targetDir); + } /** * Validates formatting environment and displays configuration. @@ -364,7 +363,7 @@ static fromArgs(args: string[]): CompactFormatter { // Handle specific files if (this.specificFiles.length > 0) { - const filePaths = this.specificFiles.map(file => join(SRC_DIR, file)); + const filePaths = this.specificFiles.map((file) => join(SRC_DIR, file)); await this.formatterService.format(filePaths, this.checkMode); return; } @@ -374,7 +373,12 @@ static fromArgs(args: string[]): CompactFormatter { if (files.length === 0) return; const mode = this.checkMode ? 'check formatting for' : 'format'; - SharedUIService.showOperationStart('FORMAT', mode, files.length, this.targetDir); + SharedUIService.showOperationStart( + 'FORMAT', + mode, + files.length, + this.targetDir, + ); const targetPath = this.targetDir ? join(SRC_DIR, this.targetDir) : SRC_DIR; await this.formatterService.format([targetPath], this.checkMode); @@ -383,6 +387,10 @@ static fromArgs(args: string[]): CompactFormatter { /** * For testing - expose internal state */ - get testCheckMode(): boolean { return this.checkMode; } - get testSpecificFiles(): string[] { return this.specificFiles; } + get testCheckMode(): boolean { + return this.checkMode; + } + get testSpecificFiles(): string[] { + return this.specificFiles; + } } diff --git a/compact/src/runFormatter.ts b/compact/src/runFormatter.ts index 358e5f90..6b9d6846 100644 --- a/compact/src/runFormatter.ts +++ b/compact/src/runFormatter.ts @@ -114,7 +114,7 @@ function handleError(error: unknown, spinner: Ora): void { if (execError.stderr && !execError.stderr.includes('compact format')) { console.log(chalk.red(` ${execError.stderr}`)); } - if (execError.stdout && execError.stdout.trim()) { + if (execError.stdout?.trim()) { console.log(chalk.yellow(` ${execError.stdout}`)); } } @@ -158,8 +158,14 @@ function handleError(error: unknown, spinner: Ora): void { function showUsageHelp(): void { console.log(chalk.yellow('\nUsage: compact-formatter [options] [files...]')); console.log(chalk.yellow('\nOptions:')); - console.log(chalk.yellow(' --check Check if files are properly formatted (default)')); - console.log(chalk.yellow(' --write Write formatting changes to files')); + console.log( + chalk.yellow( + ' --check Check if files are properly formatted (default)', + ), + ); + console.log( + chalk.yellow(' --write Write formatting changes to files'), + ); console.log( chalk.yellow( ' --dir Format specific directory (access, archive, security, token, utils)', diff --git a/compact/test/Formatter.test.ts b/compact/test/Formatter.test.ts index 879580fe..40f7cc45 100644 --- a/compact/test/Formatter.test.ts +++ b/compact/test/Formatter.test.ts @@ -8,10 +8,7 @@ import { FormatterService, FormatterUIService, } from '../src/Formatter.js'; -import { - FormatterError, - FormatterNotAvailableError, -} from '../src/types/errors.js'; +import { FormatterError } from '../src/types/errors.js'; // Mock dependencies vi.mock('chalk', () => ({ @@ -61,7 +58,7 @@ describe('FormatterEnvironmentValidator', () => { mockExec.mockRejectedValue(error); await expect(validator.checkFormatterAvailable()).rejects.toThrow( - 'Formatter not available' + 'Formatter not available', ); }); }); @@ -102,7 +99,9 @@ describe('FormatterService', () => { const result = await service.format(targets, true); expect(result).toEqual(response); - expect(mockExec).toHaveBeenCalledWith('compact format --check "security"'); + expect(mockExec).toHaveBeenCalledWith( + 'compact format --check "security"', + ); }); it('constructs command for write mode', async () => { @@ -130,7 +129,9 @@ describe('FormatterService', () => { await service.format(targets, false); - expect(mockExec).toHaveBeenCalledWith('compact format "src/contracts" "src/utils"'); + expect(mockExec).toHaveBeenCalledWith( + 'compact format "src/contracts" "src/utils"', + ); }); it('throws FormatterError on failure', async () => { @@ -205,7 +206,12 @@ describe('CompactFormatter', () => { const specificFiles = ['Token.compact']; const targetDir = 'security'; - formatter = new CompactFormatter(checkMode, specificFiles, targetDir, mockExec); + formatter = new CompactFormatter( + checkMode, + specificFiles, + targetDir, + mockExec, + ); expect(formatter.testCheckMode).toBe(checkMode); expect(formatter.testSpecificFiles).toEqual(specificFiles); @@ -236,7 +242,7 @@ describe('CompactFormatter', () => { describe('validateEnvironment', () => { it('validates environment successfully', async () => { const devToolsVersion = 'compact 0.2.0'; - + mockExec .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) .mockResolvedValueOnce({ stdout: devToolsVersion, stderr: '' }) @@ -258,7 +264,12 @@ describe('CompactFormatter', () => { describe('format', () => { it('formats specific files', async () => { const specificFiles = ['Token.compact']; - formatter = new CompactFormatter(false, specificFiles, undefined, mockExec); + formatter = new CompactFormatter( + false, + specificFiles, + undefined, + mockExec, + ); // Mock environment validation mockExec @@ -268,12 +279,16 @@ describe('CompactFormatter', () => { // Mock format command .mockResolvedValueOnce({ stdout: 'Formatted', stderr: '' }); - const displaySpy = vi.spyOn(FormatterUIService, 'displayEnvInfo').mockImplementation(() => {}); + const displaySpy = vi + .spyOn(FormatterUIService, 'displayEnvInfo') + .mockImplementation(() => {}); await formatter.format(); // Should call format with the specific file path - expect(mockExec).toHaveBeenCalledWith(`compact format "${join(SRC_DIR, 'Token.compact')}"`); + expect(mockExec).toHaveBeenCalledWith( + `compact format "${join(SRC_DIR, 'Token.compact')}"`, + ); displaySpy.mockRestore(); }); }); From 2f8eaf6ca3bef8b68d7b2efbada1d6b6c31bd35a Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 01:00:24 -0300 Subject: [PATCH 38/44] add runFormatter tests --- compact/test/runFormatter.test.ts | 492 ++++++++++++++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 compact/test/runFormatter.test.ts diff --git a/compact/test/runFormatter.test.ts b/compact/test/runFormatter.test.ts new file mode 100644 index 00000000..2b0c0fdf --- /dev/null +++ b/compact/test/runFormatter.test.ts @@ -0,0 +1,492 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { BaseErrorHandler } from '../src/BaseServices.js'; +import { CompactFormatter } from '../src/Formatter.js'; +import { + FormatterError, + FormatterNotAvailableError, + isPromisifiedChildProcessError, +} from '../src/types/errors.js'; + +// Mock dependencies +vi.mock('../src/Formatter.js', () => ({ + CompactFormatter: { + fromArgs: vi.fn(), + }, +})); + +vi.mock('../src/BaseServices.js', () => ({ + BaseErrorHandler: { + handleCommonErrors: vi.fn(), + handleUnexpectedError: vi.fn(), + }, +})); + +vi.mock('../src/types/errors.js', async () => { + const actual = await vi.importActual('../src/types/errors.js'); + return { + ...actual, + isPromisifiedChildProcessError: vi.fn(), + }; +}); + +// Mock chalk +vi.mock('chalk', () => ({ + default: { + blue: (text: string) => text, + red: (text: string) => text, + yellow: (text: string) => text, + gray: (text: string) => text, + }, +})); + +// Mock ora +const mockSpinner = { + info: vi.fn().mockReturnThis(), + fail: vi.fn().mockReturnThis(), + succeed: vi.fn().mockReturnThis(), +}; +vi.mock('ora', () => ({ + default: vi.fn(() => mockSpinner), +})); + +// Mock process.exit +const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation(() => undefined as never); + +// Mock console methods +const mockConsoleLog = vi.spyOn(console, 'log').mockImplementation(() => {}); + +describe('runFormatter CLI', () => { + let mockFormat: ReturnType; + let mockFromArgs: ReturnType; + let mockHandleCommonErrors: ReturnType; + let mockHandleUnexpectedError: ReturnType; + let originalArgv: string[]; + + beforeEach(() => { + // Store original argv + originalArgv = [...process.argv]; + + vi.clearAllMocks(); + vi.resetModules(); + + mockFormat = vi.fn(); + mockFromArgs = vi.mocked(CompactFormatter.fromArgs); + mockHandleCommonErrors = vi.mocked(BaseErrorHandler.handleCommonErrors); + mockHandleUnexpectedError = vi.mocked( + BaseErrorHandler.handleUnexpectedError, + ); + + // Mock CompactFormatter instance + mockFromArgs.mockReturnValue({ + format: mockFormat, + } as any); + + // Clear all mock calls + mockSpinner.info.mockClear(); + mockSpinner.fail.mockClear(); + mockSpinner.succeed.mockClear(); + mockConsoleLog.mockClear(); + mockExit.mockClear(); + }); + + afterEach(() => { + // Restore original argv + process.argv = originalArgv; + }); + + describe('successful formatting', () => { + it('formats successfully with no arguments', async () => { + const testData = { + expectedArgs: [], + }; + + mockFormat.mockResolvedValue(undefined); + + // Import and run the CLI + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + expect(mockFormat).toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + + it('formats successfully with arguments', async () => { + const testData = { + args: ['--dir', 'security', '--check'], + processArgv: [ + 'node', + 'runFormatter.js', + '--dir', + 'security', + '--check', + ], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); + expect(mockFormat).toHaveBeenCalled(); + expect(mockExit).not.toHaveBeenCalled(); + }); + }); + + describe('error handling delegation', () => { + it('delegates to BaseErrorHandler.handleCommonErrors first', async () => { + const testData = { + error: new Error('Directory not found'), + operation: 'FORMAT', + }; + + mockHandleCommonErrors.mockReturnValue(true); // Indicates error was handled + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('handles FormatterNotAvailableError when BaseErrorHandler returns false', async () => { + const testData = { + error: new FormatterNotAvailableError('Formatter not available'), + expectedFailMessage: '[FORMAT] Error: Formatter not available', + expectedUpdateMessages: [ + '[FORMAT] Update compiler with: compact update', + '[FORMAT] Update dev tools with: compact self update', + ], + }; + + mockHandleCommonErrors.mockReturnValue(false); // Not handled by base + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalled(); + expect(mockSpinner.fail).toHaveBeenCalledWith( + testData.expectedFailMessage, + ); + testData.expectedUpdateMessages.forEach((message) => { + expect(mockSpinner.info).toHaveBeenCalledWith(message); + }); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('handles FormatterError with child process details', async () => { + const testData = { + execError: { + stderr: 'Formatting error details', + stdout: 'Some output', + }, + expectedFailMessage: '[FORMAT] Formatting operation failed', + }; + + const formatterError = new FormatterError( + 'Formatting failed', + 'Token.compact', + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + testData.expectedFailMessage, + ); + expect(mockConsoleLog).toHaveBeenCalledWith( + ' Formatting error details', + ); + expect(mockConsoleLog).toHaveBeenCalledWith(' Some output'); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('skips stderr output when it contains compact format keyword', async () => { + const testData = { + execError: { + stderr: 'Error: compact format failed', + stdout: 'some output', + }, + }; + + const formatterError = new FormatterError( + 'Formatting failed', + undefined, + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockConsoleLog).not.toHaveBeenCalledWith( + expect.stringContaining('Error: compact format failed'), + ); + expect(mockConsoleLog).toHaveBeenCalledWith(' some output'); + }); + + it('shows usage help for argument parsing errors', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockConsoleLog).toHaveBeenCalledWith( + '\nUsage: compact-formatter [options] [files...]', + ); + expect(mockConsoleLog).toHaveBeenCalledWith('\nOptions:'); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('delegates unexpected errors to BaseErrorHandler', async () => { + const testData = { + error: new Error('Unexpected error'), + operation: 'FORMAT', + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockHandleUnexpectedError).toHaveBeenCalledWith( + testData.error, + expect.any(Object), // spinner + testData.operation, + ); + }); + }); + + describe('FormatterError handling details', () => { + it('handles FormatterError without child process details', async () => { + const testData = { + error: new FormatterError('Simple formatting error'), + expectedMessage: '[FORMAT] Formatting operation failed', + }; + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith(testData.expectedMessage); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + + it('handles empty stdout and stderr gracefully', async () => { + const testData = { + execError: { + stderr: '', + stdout: '', + }, + }; + + const formatterError = new FormatterError( + 'Formatting failed', + undefined, + testData.execError, + ); + + mockHandleCommonErrors.mockReturnValue(false); + vi.mocked(isPromisifiedChildProcessError).mockReturnValue(true); + mockFormat.mockRejectedValue(formatterError); + + await import('../src/runFormatter.js'); + + expect(mockSpinner.fail).toHaveBeenCalledWith( + '[FORMAT] Formatting operation failed', + ); + expect(mockConsoleLog).not.toHaveBeenCalled(); + }); + }); + + describe('argument parsing error handling', () => { + it('shows complete usage help', async () => { + const testData = { + error: new Error('--dir flag requires a directory name'), + expectedSections: [ + '\nUsage: compact-formatter [options] [files...]', + '\nOptions:', + ' --check Check if files are properly formatted (default)', + ' --write Write formatting changes to files', + ' --dir Format specific directory (access, archive, security, token, utils)', + '\nExamples:', + ' compact-formatter # Check all files (default)', + ' compact-formatter --write # Format all files', + ' compact-formatter --write --dir security # Format security directory', + ' compact-formatter --write f1.compact f2.compact # Format specific files', + ], + }; + + mockHandleCommonErrors.mockReturnValue(false); + mockFormat.mockRejectedValue(testData.error); + + await import('../src/runFormatter.js'); + + testData.expectedSections.forEach((section) => { + expect(mockConsoleLog).toHaveBeenCalledWith(section); + }); + }); + }); + + describe('real-world command scenarios', () => { + beforeEach(() => { + mockFormat.mockResolvedValue(undefined); + }); + + it('handles yarn format (check mode)', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js'], + expectedArgs: [], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles yarn format:fix (write mode)', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--write'], + expectedArgs: ['--write'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles directory-specific formatting', async () => { + const testData = { + processArgv: [ + 'node', + 'runFormatter.js', + '--write', + '--dir', + 'security', + ], + expectedArgs: ['--write', '--dir', 'security'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles specific file formatting', async () => { + const testData = { + processArgv: [ + 'node', + 'runFormatter.js', + '--write', + 'Token.compact', + 'AccessControl.compact', + ], + expectedArgs: ['--write', 'Token.compact', 'AccessControl.compact'], + }; + + process.argv = testData.processArgv; + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + }); + + describe('integration with CompactFormatter', () => { + it('passes arguments correctly to CompactFormatter.fromArgs', async () => { + const testData = { + args: ['--dir', 'contracts', '--check'], + processArgv: [ + 'node', + 'runFormatter.js', + '--dir', + 'contracts', + '--check', + ], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.args); + expect(mockFromArgs).toHaveBeenCalledTimes(1); + expect(mockFormat).toHaveBeenCalledTimes(1); + }); + + it('handles fromArgs throwing errors', async () => { + const testData = { + error: new Error('Invalid arguments'), + }; + + mockFromArgs.mockImplementation(() => { + throw testData.error; + }); + + await import('../src/runFormatter.js'); + + expect(mockHandleCommonErrors).toHaveBeenCalledWith( + testData.error, + expect.any(Object), + 'FORMAT', + ); + expect(mockExit).toHaveBeenCalledWith(1); + }); + }); + + describe('edge cases', () => { + it('handles mixed check and write flags', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--check', '--write'], + expectedArgs: ['--check', '--write'], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + }); + + it('handles empty file list', async () => { + const testData = { + processArgv: ['node', 'runFormatter.js', '--write'], + expectedArgs: ['--write'], + }; + + process.argv = testData.processArgv; + mockFormat.mockResolvedValue(undefined); + + await import('../src/runFormatter.js'); + + expect(mockFromArgs).toHaveBeenCalledWith(testData.expectedArgs); + expect(mockFormat).toHaveBeenCalled(); + }); + }); +}); From 862fd30906e63293d91544d300209a0156818cd9 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 01:04:56 -0300 Subject: [PATCH 39/44] move format:check before compile in CI --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bccd2fd0..e74f3b15 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,6 +61,9 @@ jobs: fi echo "✅ Language version matches: $COMPUTED_LANGUAGE_VERSION" + - name: Run Compact formatter + run: turbo format:check + - name: Compile contracts (with retry on hash mismatch) shell: bash run: | @@ -79,9 +82,6 @@ jobs: } compile - - name: Run Compact formatter - run: turbo format:check - - name: Run type checks run: turbo types From 2525b7c8ea63a7113ad54da381a22f5c723e7469 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 01:06:41 -0300 Subject: [PATCH 40/44] fix bin path --- compact/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compact/package.json b/compact/package.json index c6e1a617..23b54570 100644 --- a/compact/package.json +++ b/compact/package.json @@ -18,7 +18,7 @@ "bin": { "compact-builder": "dist/runBuilder.js", "compact-compiler": "dist/runCompiler.js", - "compact-formatter": "./dist/runFormatter.js" + "compact-formatter": "dist/runFormatter.js" }, "scripts": { "build": "tsc -p .", From bd6711677c039abee057fb0174cdf6924d45a916 Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 01:06:54 -0300 Subject: [PATCH 41/44] update yarn.lock --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index a0e3e118..ef8cf30b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -409,7 +409,7 @@ __metadata: bin: compact-builder: dist/runBuilder.js compact-compiler: dist/runCompiler.js - compact-formatter: ./dist/runFormatter.js + compact-formatter: dist/runFormatter.js languageName: unknown linkType: soft From b357c80e967c7d67fe0331c67ad782d83aa695bb Mon Sep 17 00:00:00 2001 From: andrew Date: Thu, 11 Sep 2025 01:21:49 -0300 Subject: [PATCH 42/44] fix fmt check --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e74f3b15..0f1093bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -61,8 +61,8 @@ jobs: fi echo "✅ Language version matches: $COMPUTED_LANGUAGE_VERSION" - - name: Run Compact formatter - run: turbo format:check + - name: Run Compact format check + run: turbo format - name: Compile contracts (with retry on hash mismatch) shell: bash From f5f096fae78d462aecf3cd85bc0e4e527a995ab2 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 10 Oct 2025 01:53:42 -0300 Subject: [PATCH 43/44] fix fmt --- compact/test/runCompiler.test.ts | 1 - .../src/archive/test/simulators/ShieldedTokenSimulator.ts | 3 ++- contracts/src/token/test/simulators/MultiTokenSimulator.ts | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/compact/test/runCompiler.test.ts b/compact/test/runCompiler.test.ts index a6e38154..26338f81 100644 --- a/compact/test/runCompiler.test.ts +++ b/compact/test/runCompiler.test.ts @@ -5,7 +5,6 @@ import { CompactCliNotFoundError, CompilationError, isPromisifiedChildProcessError, - type PromisifiedChildProcessError, } from '../src/types/errors.js'; // Mock dependencies diff --git a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts index 13f93026..6abf905d 100644 --- a/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts +++ b/contracts/src/archive/test/simulators/ShieldedTokenSimulator.ts @@ -31,7 +31,8 @@ import type { IContractSimulator } from '../types/test.js'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class ShieldedTokenSimulator - implements IContractSimulator { + implements IContractSimulator +{ /** @description The underlying contract instance managing contract logic. */ readonly contract: MockShielded; diff --git a/contracts/src/token/test/simulators/MultiTokenSimulator.ts b/contracts/src/token/test/simulators/MultiTokenSimulator.ts index b15779ef..34b559ee 100644 --- a/contracts/src/token/test/simulators/MultiTokenSimulator.ts +++ b/contracts/src/token/test/simulators/MultiTokenSimulator.ts @@ -28,7 +28,8 @@ import type { IContractSimulator } from '../types/test.js'; * @template L - The ledger type, fixed to Contract.Ledger. */ export class MultiTokenSimulator - implements IContractSimulator { + implements IContractSimulator +{ /** @description The underlying contract instance managing contract logic. */ readonly contract: MockMultiToken; From 975ae963403c4c3b935a125e18dbe219b8c20588 Mon Sep 17 00:00:00 2001 From: andrew Date: Fri, 10 Oct 2025 04:15:00 -0300 Subject: [PATCH 44/44] fix format --- contracts/src/token/MultiToken.compact | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/contracts/src/token/MultiToken.compact b/contracts/src/token/MultiToken.compact index 5a14a311..b72d0dc3 100644 --- a/contracts/src/token/MultiToken.compact +++ b/contracts/src/token/MultiToken.compact @@ -318,12 +318,14 @@ module MultiToken { if (!Utils_isKeyOrAddressZero(disclose(to))) { // id not initialized if (!_balances.member(disclose(id))) { - _balances.insert(disclose(id), default, Uint<128>>>); - _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); - } else { - const toBalance = balanceOf(to, id), MAX_UINT128 = 340282366920938463463374607431768211455; - assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); - _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); + _balances.insert( + disclose(id), default, Uint<128>>>); + _balances.lookup(id).insert(disclose(to), disclose(value as Uint<128>)); + } + else { + const toBalance = balanceOf(to, id), MAX_UINT128 = 340282366920938463463374607431768211455; + assert(MAX_UINT128 - toBalance >= value, "MultiToken: arithmetic overflow"); + _balances.lookup(id).insert(disclose(to), disclose(toBalance + value as Uint<128>)); } } }