Files
xo-stack/CONTRIBUTING.md
T
2025-03-10 05:36:16 +00:00

5.7 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(
    //
    dependencies: {
      blockchain: BaseBlockchain,
      store: BaseStore,
    }
  ) {
    this.blockchain = dependencies.blockchain;
    this.store = dependencies.store;
  }
}

Factory Patterns

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

Semantically, think of "from" as meaning "instantiate from": This is more succinct than a create 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".

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.

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>;