Files
xo-stack/CONTRIBUTING.md
T
2025-04-18 05:17:20 +00:00

5.8 KiB

Guidelines

MR/Commit Messages

NOTE: Not necessary to follow this until V1.0 release.

Use a Conventional Commit format when creating MR's. For example:

feat(Primitives): Add Transaction Primitive

... or to indicate a breaking change (using !):

chore!: Drop support for Node 16

This will ensure that we can automatically generate correct Semantic Versions and changelogs between releases (using Lerna).

A table of common prefixes (conventions) is below:

Change Description
feat A new feature
fix A bug fix
docs Documentation only changes
style Changes that do not affect the meaning of the code (formatting)
refactor A code change that neither fixes a bug nor adds a feature
perf A code change that improves performance
test Adding missing tests or correct existing tests
build Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
chore Other changes that don't modify src or test files
revert Reverts a previous commit

Patterns

Dependency Injection

We use Dependency Injection (DI) for several key reasons:

  • Improved testability: Dependencies can be easily mocked during unit tests
  • Reduced coupling: Classes depend on abstractions rather than concrete implementations
  • Greater flexibility: Implementations can be swapped without modifying dependent classes
  • Clearer dependencies: Constructor signatures explicitly document required dependencies
  • Easier refactoring: Component relationships are clearly defined
  • Simpler dependency management: Easier control over resource initialization and lifecycle

Try not to over-abstract, but if there is already a Base Class (or interface) available, always try to use that (via dependency injection) in favor of a concrete implementation.

For example:

class SomeClass {
  protected blockchain: BaseBlockchain;
  protected store: BaseStore;

  constructor(
    // Dependency Injection first
    dependencies: {
      blockchain: BaseBlockchain,
      store: BaseStore,
    },
    // Then main (required) initialization params
    initParams,
    // Then optional params (if applicable).
    opts,
  ) {
    this.blockchain = dependencies.blockchain;
    this.store = dependencies.store;
  }
}

Factory Patterns

When creating classes, create static from(...) (or fromSpecificThing(...)) methods.

In Javascript, a constructor cannot be asynchronous and we want to ensure that we can await class instantiation to mitigate against stateful errors.

For example:

class SomeClass {
  // Static "from" factory method.
  // NOTE: The below args are shorthand for using the same arguments as the constructor.
  static async from(...args: ConstructorParameters<typeof SomeClass>) {
    // Create the instance.
    const someClass = new SomeClass(...args);

    // Execute the asynchronous initialization function.
    await someClass.start();

    // Return the instance.
    return someClass;
  }
}

Although this isn't always necessary (i.e. not all instantiations need to be async), it keeps instantiation of classes consistent.

Semantically, think of "from" as meaning "instantiate from": This is more succinct than a createFrom... factory method which, if we have multiple ways of instantiation, can become verbose. E.g. PrivateKey.fromWIF() as opposed to PrivateKey.createFromWIF(). It also allows us to differentiate between actually "creating" something (e.g. a Filesystem.createFile(...)) versus "instantiating" a representation of a "thing".

Validations

Check types with TypeScript - Runtime type-checking is a code smell. If the function accepts a string, assume it's been given a string. TypeScript definitions should expose improperly called functions to the developer at compile time, don't re-implement it at runtime.

There are a few exceptions to this:

  • If TypeScript's lack of dependant types makes validating certain properties of a type (e.g. the length of a Uint8Array), then runtime type-checking should be used to detect and return errors.
  • If we are working with a network (remote/untrusted) payload, Type Validation should be done with Zod.

An example of working with Zod:

// Define a schema so that we can validate the payload.
export const SomeRemotePayload = z.object({
  publicKey: z.string(),
  action: z.enum(['set', 'delete', 'clear']),
  key: z.string(),
  value: z.string(),
  timestamp: z.number(),
});

// Run Zod's validation against this payload.
// NOTE: This will throw if it fails.
const parsedPayload = SomeRemotePayload.parse(rawPayload);

// (Optional) If this type is needed elsewhere, we can infer it from Zod and export it.
export type StoreRemoteHTTPEvent = z.infer<typeof StoreRemoteHTTPEventSchema>;