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

132 lines
5.7 KiB
Markdown

# Guidelines
## MR/Commit Messages
NOTE: Not necessary to follow this until V1.0 release.
Use a [Conventional Commit](https://www.conventionalcommits.org/en/v1.0.0/) 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](https://semver.org/) 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:
```ts
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:
```ts
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](https://www.npmjs.com/package/zod).
An example of working with Zod:
```ts
// 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>;
```